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