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 eu.maveniverse.maven.mima.context.Context; 023import eu.maveniverse.maven.mima.context.ContextOverrides; 024import eu.maveniverse.maven.mima.context.Runtime; 025import eu.maveniverse.maven.mima.context.Runtimes; 026import java.net.MalformedURLException; 027import java.net.URL; 028import java.net.URLClassLoader; 029import java.nio.file.Path; 030import java.util.ArrayList; 031import java.util.Arrays; 032import java.util.List; 033import java.util.Properties; 034import java.util.stream.Collectors; 035import java.util.stream.Stream; 036import org.apache.commons.cli.CommandLine; 037import org.apache.commons.cli.DefaultParser; 038import org.apache.commons.cli.ParseException; 039import org.eclipse.aether.artifact.DefaultArtifact; 040import org.eclipse.aether.collection.CollectRequest; 041import org.eclipse.aether.graph.Dependency; 042import org.eclipse.aether.graph.DependencyNode; 043import org.eclipse.aether.resolution.DependencyRequest; 044import org.eclipse.aether.resolution.DependencyResolutionException; 045import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator; 046import org.jdrupes.builder.api.BuildException; 047import org.jdrupes.builder.api.ConfigurationException; 048import org.jdrupes.builder.api.FaultAware; 049import org.jdrupes.builder.api.Launcher; 050import org.jdrupes.builder.api.Masked; 051import org.jdrupes.builder.api.Project; 052import org.jdrupes.builder.api.ResourceFactory; 053import org.jdrupes.builder.api.RootProject; 054import org.jdrupes.builder.api.UnavailableException; 055import org.jdrupes.builder.core.AbstractRootProject; 056import org.jdrupes.builder.core.DefaultBuildContext; 057import org.jdrupes.builder.java.JarFile; 058import static org.jdrupes.builder.java.JavaTypes.*; 059 060/// An implementation of a [Launcher] that launches the build configuration. 061/// It expects that the JDrupes Builder project has already been compiled 062/// and its classes are available on the classpath. 063/// 064public class BuildProjectLauncher extends AbstractLauncher { 065 066 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 067 /// The JDrupes Builder properties read from the file 068 /// `.jdbld.properties` in the root project. 069 protected Properties jdbldProps; 070 /// The command line. 071 protected CommandLine commandLine; 072 private final Path buildRoot; 073 private static final String RUNTIME_EXTENSIONS = "runtimeExtensions"; 074 private final ClassLoader extClsLdr; 075 private AbstractRootProject rootProject; 076 077 /// Instantiates a new build project launcher. The classpath is scanned 078 /// for classes that implement [Project] but do not implement [Masked]. 079 /// One of these must also implement the [RootProject] interface. 080 /// The latter is instantiated and registered as root project with all 081 /// other classes found as direct sub projects. 082 /// 083 /// @param classloader the classloader for the build project 084 /// @param buildRoot the build root 085 /// @param args the arguments. Flags are processed in the constructor, 086 /// command line arguments are processed in [runCommands]. 087 /// 088 @SuppressWarnings({ "PMD.UseVarargs", 089 "PMD.ConstructorCallsOverridableMethod" }) 090 public BuildProjectLauncher(ClassLoader classloader, Path buildRoot, 091 String[] args) { 092 this.buildRoot = buildRoot; 093 jdbldProps = propertiesFromFiles(buildRoot); 094 try { 095 commandLine = new DefaultParser().parse(baseOptions(), args); 096 } catch (ParseException e) { 097 configureLogging(buildRoot, jdbldProps); 098 throw new ConfigurationException().cause(e); 099 } 100 addCliProperties(jdbldProps, commandLine); 101 configureLogging(buildRoot, jdbldProps); 102 extClsLdr = addRuntimeExts(classloader); 103 regenerateRootProject(); 104 } 105 106 @Override 107 public AbstractRootProject regenerateRootProject() { 108 var rootProjects = new ArrayList<Class<? extends RootProject>>(); 109 var subprojects = new ArrayList<Class<? extends Project>>(); 110 findProjects(extClsLdr, rootProjects, subprojects); 111 @SuppressWarnings("PMD.CloseResource") 112 var newRootProject = DefaultBuildContext.createProjects(buildRoot, 113 rootProjects.get(0), subprojects, jdbldProps, commandLine); 114 if (rootProject != null) { 115 rootProject.close(); 116 } 117 rootProject = newRootProject; 118 return rootProject; 119 } 120 121 private ClassLoader addRuntimeExts(ClassLoader classloader) { 122 String[] coordinates = Arrays 123 .asList(jdbldProps.getProperty(RUNTIME_EXTENSIONS, "").split(",")) 124 .stream() 125 .map(String::trim).filter(c -> !c.isBlank()).toArray(String[]::new); 126 if (coordinates.length == 0) { 127 return classloader; 128 } 129 130 // Resolve using maven repo 131 var cpUrls = resolveRequested(coordinates).mapMulti((jf, consumer) -> { 132 try { 133 consumer.accept(jf.path().toFile().toURI().toURL()); 134 } catch (MalformedURLException e) { 135 logger.atWarning().withCause(e).log("Cannot convert %s to URL", 136 jf); 137 } 138 }).toArray(URL[]::new); 139 140 // Return augmented classloader 141 return new URLClassLoader(cpUrls, classloader); 142 } 143 144 @Override 145 public void close() { 146 rootProject.close(); 147 } 148 149 @SuppressWarnings({ "PMD.UseVarargs", 150 "PMD.AvoidInstantiatingObjectsInLoops" }) 151 private Stream<JarFile> resolveRequested(String[] coordinates) { 152 ContextOverrides overrides = ContextOverrides.create() 153 .withUserSettings(true).build(); 154 Runtime runtime = Runtimes.INSTANCE.getRuntime(); 155 try (Context context = runtime.create(overrides)) { 156 CollectRequest collectRequest = new CollectRequest() 157 .setRepositories(context.remoteRepositories()); 158 for (var coord : coordinates) { 159 collectRequest.addDependency( 160 new Dependency(new DefaultArtifact(coord), "runtime")); 161 } 162 163 DependencyRequest dependencyRequest 164 = new DependencyRequest(collectRequest, null); 165 DependencyNode rootNode; 166 try { 167 rootNode = context.repositorySystem() 168 .resolveDependencies(context.repositorySystemSession(), 169 dependencyRequest) 170 .getRoot(); 171 PreorderNodeListGenerator nlg = new PreorderNodeListGenerator(); 172 rootNode.accept(nlg); 173 List<DependencyNode> dependencyNodes = nlg.getNodes(); 174 return dependencyNodes.stream() 175 .filter(d -> d.getArtifact() != null) 176 .map(d -> d.getArtifact().getFile().toPath()) 177 .map(p -> ResourceFactory 178 .create(JarFileType, p)); 179 } catch (DependencyResolutionException e) { 180 throw new ConfigurationException() 181 .message("Cannot resolve: %s", e).cause(e); 182 } 183 } 184 } 185 186 @Override 187 public AbstractRootProject rootProject() { 188 return rootProject; 189 } 190 191 /// Execute the commands from the command line. 192 /// 193 /// @return true, if successful 194 /// 195 @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", 196 "PMD.AvoidInstantiatingObjectsInLoops" }) 197 public boolean runCommands() { 198 for (var arg : commandLine.getArgs()) { 199 var parts = arg.split(":"); 200 String resource = parts[parts.length - 1]; 201 var cmdData = rootProject.lookupCommand(resource); 202 if (cmdData.requests().length == 0) { 203 rootProject.context().out() 204 .println("Unknown command: " + resource); 205 throw new UnavailableException().from(rootProject); 206 } 207 String pattern = cmdData.pattern(); 208 if (parts.length > 1) { 209 pattern = parts[0]; 210 } 211 for (var req : cmdData.requests()) { 212 if (!resources(rootProject.projects(pattern), req) 213 // eliminate duplicates 214 .collect(Collectors.toSet()).stream() 215 // output generated resources 216 .peek(r -> rootProject.context().out() 217 .println(r.toString())) 218 .map(r -> !(r instanceof FaultAware far) 219 || !far.isFaulty()) 220 .reduce(true, (r1, r2) -> r1 && r2)) { 221 return false; 222 } 223 } 224 } 225 return true; 226 } 227 228 /// This main can be used to start the user's JDrupes Builder 229 /// project from an IDE for debugging purposes. It expects that 230 /// the JDrupes Builder project has already been compiled (typically 231 /// by the IDE) and is available on the classpath. 232 /// 233 /// @param args the arguments 234 /// 235 @SuppressWarnings("PMD.SystemPrintln") 236 public static void main(String[] args) { 237 try { 238 if (!reportBuildException(() -> { 239 try (var bpl = new BuildProjectLauncher( 240 Thread.currentThread().getContextClassLoader(), 241 Path.of("").toAbsolutePath(), args)) { 242 return bpl.runCommands(); 243 } 244 })) { 245 java.lang.Runtime.getRuntime().exit(1); 246 } 247 } catch (BuildException e) { 248 if (e.getCause() == null) { 249 logger.atSevere().log("Build failed: %s", 250 formatter().summary(e)); 251 } else { 252 logger.atSevere().withCause(e).log("Build failed: %s", 253 formatter().summary(e)); 254 } 255 System.out.println(formatter().summary(e)); 256 System.out.println(e.details()); 257 java.lang.Runtime.getRuntime().exit(2); 258 } 259 } 260}