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