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