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 mvnLookup 127 = new MvnRepoLookup().addRepositories(MavenContext.mavenCentral()); 128 Optional.ofNullable(jdbldProps 129 .getProperty(BuildContext.EXTENSIONS_SNAPSHOT_REPOSITORY, null)) 130 .map(URI::create).ifPresent(esr -> mvnLookup.addRepository( 131 "extensionSnapshots", esr, MvnVersionType.SNAPSHOT)); 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 var extCp = System.getenv("JDBLD_EXTS"); 141 if (extCp != null) { 142 rootProject.project(BootstrapBuild.class) 143 .dependency(Expose, ClasspathScanner::new).path(extCp); 144 } 145 return rootProject.resources(rootProject 146 .of(ClasspathElementType).using(Supply, Expose)).map(cpe -> { 147 try { 148 if (cpe instanceof FileTree tree) { 149 return tree.root().toFile().toURI().toURL(); 150 } 151 return ((FileResource) cpe).path().toFile().toURI() 152 .toURL(); 153 } catch (MalformedURLException e) { 154 // Cannot happen 155 throw new BuildException().from(rootProject).cause(e); 156 } 157 }).toArray(URL[]::new); 158 } 159 160 @Override 161 public AbstractRootProject rootProject() { 162 return bootstrapProject; 163 } 164 165 @Override 166 public RootProject regenerateRootProject() { 167 throw new UnsupportedOperationException( 168 "The bootstrap launcher does not support regenerate"); 169 } 170 171 /// The main method. 172 /// 173 /// @param args the arguments 174 /// 175 @SuppressWarnings("PMD.SystemPrintln") 176 public static void main(String[] args) { 177 try { 178 if (!reportBuildException(() -> { 179 BuildProjectLauncher buildPl; 180 try (var bootPl = new BootstrapProjectLauncher( 181 BootstrapRoot.class, args)) { 182 buildPl = bootPl.buildBuildProjectLauncher( 183 BootstrapRoot.class, args); 184 } 185 try (buildPl) { 186 return buildPl.runCommands(); 187 } 188 })) { 189 System.exit(1); 190 } 191 } catch (BuildException e) { 192 if (e.getCause() == null) { 193 logger.atSevere().log("Build failed: %s", 194 formatter().summary(e)); 195 } else { 196 logger.atSevere().withCause(e).log("Build failed: %s", 197 formatter().summary(e)); 198 } 199 System.out.println(formatter().summary(e)); 200 if (!e.details().isBlank()) { 201 System.out.println(e.details()); 202 } 203 System.exit(2); 204 } 205 } 206}