001/* 002 * JDrupes Builder 003 * Copyright (C) 2025, 2026 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.builder.startup; 020 021import com.google.common.flogger.FluentLogger; 022import java.net.MalformedURLException; 023import java.net.URI; 024import java.net.URL; 025import java.net.URLClassLoader; 026import java.nio.file.Path; 027import java.util.Arrays; 028import java.util.Collections; 029import java.util.Optional; 030import java.util.Properties; 031import org.apache.commons.cli.CommandLine; 032import org.apache.commons.cli.DefaultParser; 033import org.apache.commons.cli.ParseException; 034import org.jdrupes.builder.api.BuildException; 035import org.jdrupes.builder.api.FileResource; 036import org.jdrupes.builder.api.FileTree; 037import static org.jdrupes.builder.api.Intent.*; 038import org.jdrupes.builder.api.Launcher; 039import org.jdrupes.builder.api.Project; 040import org.jdrupes.builder.api.RootProject; 041import org.jdrupes.builder.core.LauncherSupport; 042import org.jdrupes.builder.java.ClasspathElement; 043import org.jdrupes.builder.java.JavaCompiler; 044import org.jdrupes.builder.mvnrepo.MvnRepoLookup; 045 046/// An implementation of a [Launcher] that bootstraps the build. 047/// The [BootstrapProjectLauncher] uses the built-in [BootstrapRoot] and 048/// [BootstrapBuild] to assemble a JDrupes Builder [Project] (the 049/// bootstrap project) that includes the [JavaCompiler] for compiling 050/// the JDrupes Builder configuration provided by the user. 051/// 052/// The launcher then requests the *supplied* and *exposed* classes from 053/// the bootstrap project, including in particular the [RootProject] of 054/// the user's build configuration. The launcher uses these classes as 055/// classpath for creating the [BuildProjectLauncher] 056/// 057public class BootstrapProjectLauncher extends AbstractLauncher { 058 059 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 060 /// The JDrupes Builder properties read from the file 061 /// `.jdbld.properties` in the root project. 062 protected Properties jdbldProps; 063 /// The command line. 064 protected CommandLine commandLine; 065 private RootProject rootProject; 066 067 /// Initializes a new bootstrap launcher. 068 /// 069 public BootstrapProjectLauncher() { 070 // Make javadoc happy. 071 } 072 073 /// Executes a build. An instance of the class passed as argument is 074 /// created and used as root project for the build. 075 /// 076 /// Unless the root project is the only project, the root project 077 /// must declare dependencies, else the subprojects won't be 078 /// instantiated. 079 /// 080 /// @param rootPrjCls the root project 081 /// @param args the args 082 /// @return the builds the project launcher 083 /// 084 @SuppressWarnings("PMD.UseVarargs") 085 public BuildProjectLauncher buildBuildProjectLauncher( 086 Class<? extends RootProject> rootPrjCls, String[] args) { 087 Path buildRootDirectory = Path.of("").toAbsolutePath(); 088 jdbldProps = propertiesFromFiles(buildRootDirectory); 089 try { 090 commandLine = new DefaultParser().parse(baseOptions(), args); 091 } catch (ParseException e) { 092 configureLogging(buildRootDirectory, jdbldProps); 093 throw new BuildException().cause(e); 094 } 095 addCliProperties(jdbldProps, commandLine); 096 configureLogging(buildRootDirectory, jdbldProps); 097 098 rootProject = LauncherSupport.createProjects(buildRootDirectory, 099 rootPrjCls, Collections.emptyList(), jdbldProps, commandLine); 100 101 // Add build extensions to the build project. 102 var mvnLookup = new MvnRepoLookup(); 103 Optional.ofNullable(jdbldProps 104 .getProperty(BootstrapBuild.EXTENSIONS_SNAPSHOT_REPO, null)) 105 .map(URI::create).ifPresent(mvnLookup::snapshotRepository); 106 var buildCoords = Arrays.asList(jdbldProps 107 .getProperty(BootstrapBuild.BUILD_EXTENSIONS, "").split(",")) 108 .stream().map(String::trim).filter(c -> !c.isBlank()).toList(); 109 logger.atFine().log("Adding build extensions: %s" 110 + " to classpath for builder project compilation", buildCoords); 111 buildCoords.forEach(mvnLookup::resolve); 112 rootProject.project(BootstrapBuild.class).dependency(Expose, 113 mvnLookup); 114 var cpUrls = rootProject.resources(rootProject 115 .of(ClasspathElement.class).using(Supply, Expose)).map(cpe -> { 116 try { 117 if (cpe instanceof FileTree tree) { 118 return tree.root().toFile().toURI().toURL(); 119 } 120 return ((FileResource) cpe).path().toFile().toURI() 121 .toURL(); 122 } catch (MalformedURLException e) { 123 // Cannot happen 124 throw new BuildException().from(rootProject).cause(e); 125 } 126 }).toArray(URL[]::new); 127 logger.atFine().log("Launching build project with classpath: %s", 128 Arrays.toString(cpUrls)); 129 return new BuildProjectLauncher( 130 new URLClassLoader(cpUrls, getClass().getClassLoader()), 131 buildRootDirectory, args); 132 } 133 134 /// Root project. 135 /// 136 /// @return the root project 137 /// 138 @Override 139 public RootProject rootProject() { 140 return rootProject; 141 } 142 143 /// The main method. 144 /// 145 /// @param args the arguments 146 /// 147 @SuppressWarnings("PMD.SystemPrintln") 148 public static void main(String[] args) { 149 try { 150 if (!reportBuildException( 151 () -> new BootstrapProjectLauncher().buildBuildProjectLauncher( 152 BootstrapRoot.class, args).runCommands())) { 153 Runtime.getRuntime().exit(1); 154 } 155 } catch (BuildException e) { 156 if (e.getCause() == null) { 157 logger.atSevere().log("Build failed: %s", 158 formatter().summary(e)); 159 } else { 160 logger.atSevere().withCause(e).log("Build failed: %s", 161 formatter().summary(e)); 162 } 163 System.out.println(formatter().summary(e)); 164 Runtime.getRuntime().exit(2); 165 } 166 } 167}