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.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 = 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 ScopedValueContext.snapshot() 111 .where(bootstrapProject.context()::startRequestChain) 112 .where(scopedBuildContext, bootstrapProject.context()).call(() -> { 113 URL[] cpUrls = buildProjectClasses(bootstrapProject); 114 logger.atFine().log("Build project launcher with classpath: %s", 115 Arrays.toString(cpUrls)); 116 return new BuildProjectLauncher( 117 new URLClassLoader(cpUrls, getClass().getClassLoader()), 118 buildRootDirectory, args); 119 }); 120 } 121 122 private URL[] buildProjectClasses(RootProject rootProject) { 123 // Add build extensions to the build project. 124 var mvnLookup = new MvnRepoLookup(); 125 Optional.ofNullable(jdbldProps 126 .getProperty(BuildContext.EXTENSIONS_SNAPSHOT_REPOSITORY, null)) 127 .map(URI::create).ifPresent(mvnLookup::snapshotRepository); 128 var buildCoords = Arrays.asList(jdbldProps 129 .getProperty(BuildContext.BUILD_EXTENSIONS, "").split(",")) 130 .stream().map(String::trim).filter(c -> !c.isBlank()).toList(); 131 logger.atFine().log("Adding build extensions: %s" 132 + " to classpath for builder project compilation", buildCoords); 133 buildCoords.forEach(mvnLookup::resolve); 134 rootProject.project(BootstrapBuild.class).dependency(Expose, 135 mvnLookup); 136 var extCp = System.getenv("JDBLD_EXTS"); 137 if (extCp != null) { 138 rootProject.project(BootstrapBuild.class) 139 .dependency(Expose, ClasspathScanner::new).path(extCp); 140 } 141 return rootProject.resources(rootProject 142 .of(ClasspathElementType).using(Supply, Expose)).map(cpe -> { 143 try { 144 if (cpe instanceof FileTree tree) { 145 return tree.root().toFile().toURI().toURL(); 146 } 147 return ((FileResource) cpe).path().toFile().toURI() 148 .toURL(); 149 } catch (MalformedURLException e) { 150 // Cannot happen 151 throw new BuildException().from(rootProject).cause(e); 152 } 153 }).toArray(URL[]::new); 154 } 155 156 @Override 157 public AbstractRootProject rootProject() { 158 return bootstrapProject; 159 } 160 161 @Override 162 public RootProject regenerateRootProject() { 163 throw new UnsupportedOperationException( 164 "The bootstrap launcher does not support regenerate"); 165 } 166 167 /// The main method. 168 /// 169 /// @param args the arguments 170 /// 171 @SuppressWarnings("PMD.SystemPrintln") 172 public static void main(String[] args) { 173 try { 174 if (!reportBuildException(() -> { 175 BuildProjectLauncher buildPl; 176 try (var bootPl = new BootstrapProjectLauncher( 177 BootstrapRoot.class, args)) { 178 buildPl = bootPl.buildBuildProjectLauncher( 179 BootstrapRoot.class, args); 180 } 181 try (buildPl) { 182 return buildPl.runCommands(); 183 } 184 })) { 185 System.exit(1); 186 } 187 } catch (BuildException e) { 188 if (e.getCause() == null) { 189 logger.atSevere().log("Build failed: %s", 190 formatter().summary(e)); 191 } else { 192 logger.atSevere().withCause(e).log("Build failed: %s", 193 formatter().summary(e)); 194 } 195 System.out.println(formatter().summary(e)); 196 if (!e.details().isBlank()) { 197 System.out.println(e.details()); 198 } 199 System.exit(2); 200 } 201 } 202}