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