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.BuildContext; 035import org.jdrupes.builder.api.BuildException; 036import org.jdrupes.builder.api.ConfigurationException; 037import org.jdrupes.builder.api.FileResource; 038import org.jdrupes.builder.api.FileTree; 039import static org.jdrupes.builder.api.Intent.*; 040import org.jdrupes.builder.api.Launcher; 041import org.jdrupes.builder.api.Project; 042import org.jdrupes.builder.api.RootProject; 043import org.jdrupes.builder.core.AbstractRootProject; 044import org.jdrupes.builder.core.DefaultBuildContext; 045import org.jdrupes.builder.java.ClasspathElement; 046import org.jdrupes.builder.java.ClasspathScanner; 047import org.jdrupes.builder.java.JavaCompiler; 048import org.jdrupes.builder.mvnrepo.MvnRepoLookup; 049 050/// An implementation of a [Launcher] that bootstraps the build. 051/// The [BootstrapProjectLauncher] uses the built-in [BootstrapRoot] and 052/// [BootstrapBuild] to assemble a JDrupes Builder [Project] (the 053/// bootstrap project) that includes the [JavaCompiler] for compiling 054/// the JDrupes Builder configuration provided by the user. 055/// 056/// The launcher then requests the *supplied* and *exposed* classes from 057/// the bootstrap project, including in particular the [RootProject] of 058/// the user's build configuration. The launcher uses these classes as 059/// classpath for creating the [BuildProjectLauncher] 060/// 061public class BootstrapProjectLauncher extends AbstractLauncher { 062 063 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 064 /// The JDrupes Builder properties read from the file 065 /// `.jdbld.properties` in the root project. 066 protected Properties jdbldProps; 067 /// The command line. 068 protected CommandLine commandLine; 069 private final AbstractRootProject bootstrapProject; 070 private final Path buildRootDirectory; 071 072 /// Initializes a new bootstrap launcher. 073 /// 074 /// @param rootPrjCls the root project class 075 /// @param args the arguments 076 /// 077 @SuppressWarnings("PMD.UseVarargs") 078 public BootstrapProjectLauncher( 079 Class<? extends RootProject> rootPrjCls, String[] args) { 080 buildRootDirectory = Path.of("").toAbsolutePath(); 081 jdbldProps = propertiesFromFiles(buildRootDirectory); 082 try { 083 commandLine = new DefaultParser().parse(baseOptions(), args); 084 } catch (ParseException e) { 085 configureLogging(buildRootDirectory, jdbldProps); 086 throw new ConfigurationException().cause(e); 087 } 088 addCliProperties(jdbldProps, commandLine); 089 configureLogging(buildRootDirectory, jdbldProps); 090 091 bootstrapProject = DefaultBuildContext.createProjects( 092 buildRootDirectory, rootPrjCls, Collections.emptyList(), jdbldProps, 093 commandLine); 094 } 095 096 @Override 097 public void close() { 098 bootstrapProject.close(); 099 } 100 101 /// Builds the build project launcher. 102 /// 103 /// @param rootPrjCls the root project 104 /// @param args the args 105 /// @return the builds the project launcher 106 /// 107 @SuppressWarnings("PMD.UseVarargs") 108 public BuildProjectLauncher buildBuildProjectLauncher( 109 Class<? extends RootProject> rootPrjCls, String[] args) { 110 return bootstrapProject.context().call(() -> { 111 URL[] cpUrls = buildProjectClasses(bootstrapProject); 112 logger.atFine().log("Build project launcher with classpath: %s", 113 Arrays.toString(cpUrls)); 114 return new BuildProjectLauncher( 115 new URLClassLoader(cpUrls, getClass().getClassLoader()), 116 buildRootDirectory, args); 117 }); 118 } 119 120 private URL[] buildProjectClasses(RootProject rootProject) { 121 // Add build extensions to the build project. 122 var mvnLookup = new MvnRepoLookup(); 123 Optional.ofNullable(jdbldProps 124 .getProperty(BuildContext.EXTENSIONS_SNAPSHOT_REPOSITORY, null)) 125 .map(URI::create).ifPresent(mvnLookup::snapshotRepository); 126 var buildCoords = Arrays.asList(jdbldProps 127 .getProperty(BuildContext.BUILD_EXTENSIONS, "").split(",")) 128 .stream().map(String::trim).filter(c -> !c.isBlank()).toList(); 129 logger.atFine().log("Adding build extensions: %s" 130 + " to classpath for builder project compilation", buildCoords); 131 buildCoords.forEach(mvnLookup::resolve); 132 rootProject.project(BootstrapBuild.class).dependency(Expose, 133 mvnLookup); 134 var extCp = System.getenv("JDBLD_EXTS"); 135 if (extCp != null) { 136 rootProject.project(BootstrapBuild.class) 137 .dependency(Expose, ClasspathScanner::new).path(extCp); 138 } 139 return rootProject.resources(rootProject 140 .of(ClasspathElement.class).using(Supply, Expose)).map(cpe -> { 141 try { 142 if (cpe instanceof FileTree tree) { 143 return tree.root().toFile().toURI().toURL(); 144 } 145 return ((FileResource) cpe).path().toFile().toURI() 146 .toURL(); 147 } catch (MalformedURLException e) { 148 // Cannot happen 149 throw new BuildException().from(rootProject).cause(e); 150 } 151 }).toArray(URL[]::new); 152 } 153 154 @Override 155 public AbstractRootProject rootProject() { 156 return bootstrapProject; 157 } 158 159 @Override 160 public RootProject regenerateRootProject() { 161 throw new UnsupportedOperationException( 162 "The bootstrap launcher does not support regenerate"); 163 } 164 165 /// The main method. 166 /// 167 /// @param args the arguments 168 /// 169 @SuppressWarnings("PMD.SystemPrintln") 170 public static void main(String[] args) { 171 try { 172 if (!reportBuildException(() -> { 173 BuildProjectLauncher buildPl; 174 try (var bootPl = new BootstrapProjectLauncher( 175 BootstrapRoot.class, args)) { 176 buildPl = bootPl.buildBuildProjectLauncher( 177 BootstrapRoot.class, args); 178 } 179 try (buildPl) { 180 return buildPl.runCommands(); 181 } 182 })) { 183 System.exit(1); 184 } 185 } catch (BuildException e) { 186 if (e.getCause() == null) { 187 logger.atSevere().log("Build failed: %s", 188 formatter().summary(e)); 189 } else { 190 logger.atSevere().withCause(e).log("Build failed: %s", 191 formatter().summary(e)); 192 } 193 System.out.println(formatter().summary(e)); 194 if (!e.details().isBlank()) { 195 System.out.println(e.details()); 196 } 197 System.exit(2); 198 } 199 } 200}