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.TreeMap; 030import java.util.stream.Collectors; 031import java.util.stream.Stream; 032import org.apache.commons.cli.CommandLine; 033import org.apache.commons.cli.DefaultParser; 034import org.apache.commons.cli.ParseException; 035import org.jdrupes.builder.api.BuildException; 036import org.jdrupes.builder.api.ConfigurationException; 037import org.jdrupes.builder.api.FaultAware; 038import org.jdrupes.builder.api.Launcher; 039import org.jdrupes.builder.api.Masked; 040import org.jdrupes.builder.api.Project; 041import org.jdrupes.builder.api.RootProject; 042import org.jdrupes.builder.api.UnavailableException; 043import org.jdrupes.builder.core.AbstractRootProject; 044import org.jdrupes.builder.java.JarFile; 045import org.jdrupes.builder.mvnrepo.MvnRepoLookup; 046import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*; 047 048/// An implementation of a [Launcher] that launches the build configuration. 049/// It expects that the JDrupes Builder project has already been compiled 050/// and its classes are available on the classpath. 051/// 052public class BuildProjectLauncher extends AbstractLauncher { 053 054 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 055 /// The JDrupes Builder properties read from the file 056 /// `.jdbld.properties` in the root project. 057 protected Properties jdbldProps; 058 /// The command line. 059 protected CommandLine commandLine; 060 private final Path buildRoot; 061 private static final String RUNTIME_EXTENSIONS = "runtimeExtensions"; 062 private final ClassLoader extClsLdr; 063 private AbstractRootProject rootProject; 064 065 /// Instantiates a new build project launcher. The classpath is scanned 066 /// for classes that implement [Project] but do not implement [Masked]. 067 /// One of these must also implement the [RootProject] interface. 068 /// The latter is instantiated and registered as root project with all 069 /// other classes found as direct sub projects. 070 /// 071 /// @param classloader the classloader for the build project 072 /// @param buildRoot the build root 073 /// @param args the arguments. Flags are processed in the constructor, 074 /// command line arguments are processed in [runCommands]. 075 /// 076 @SuppressWarnings({ "PMD.UseVarargs", 077 "PMD.ConstructorCallsOverridableMethod" }) 078 public BuildProjectLauncher(ClassLoader classloader, Path buildRoot, 079 String[] args) { 080 this.buildRoot = buildRoot; 081 jdbldProps = propertiesFromFiles(buildRoot); 082 try { 083 commandLine = new DefaultParser().parse(baseOptions(), args); 084 } catch (ParseException e) { 085 doConfigureLogging(buildRoot, jdbldProps); 086 throw new ConfigurationException().cause(e); 087 } 088 addCliProperties(jdbldProps, commandLine); 089 doConfigureLogging(buildRoot, jdbldProps); 090 extClsLdr = addRuntimeExts(classloader); 091 regenerateRootProject(); 092 } 093 094 /// Calls [configureLogging] with [buildRoot] and [jdbldProps]. 095 /// Can be overridden by a derived class (usually with a noop) 096 /// to avoid the reconfiguration when launching a build from 097 /// jdbld. 098 /// 099 /// @param buildRoot the build root 100 /// @param jdbldProps the jdbld props 101 /// 102 protected void doConfigureLogging(Path buildRoot, Properties jdbldProps) { 103 configureLogging(buildRoot, jdbldProps); 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 = 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 private Stream<JarFile> resolveRequested(String[] coordinates) { 151 var lookup = new MvnRepoLookup().resolve(coordinates); 152 return lookup.resources(lookup.of(MvnRepoLibraryJarFileType)); 153 } 154 155 @Override 156 public AbstractRootProject rootProject() { 157 return rootProject; 158 } 159 160 /// Execute the commands from the command line. 161 /// 162 /// @return true, if successful 163 /// 164 @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", 165 "PMD.AvoidInstantiatingObjectsInLoops" }) 166 public boolean runCommands() { 167 if (commandLine.hasOption("help") 168 || commandLine.getArgs().length == 0) { 169 printHelp(); 170 return true; 171 } 172 for (var arg : commandLine.getArgs()) { 173 var parts = arg.split(":"); 174 String resource = parts[parts.length - 1]; 175 var cmdData = rootProject.lookupCommand(resource); 176 if (cmdData.requests().length == 0) { 177 rootProject.context().out() 178 .println("Unknown command: " + resource); 179 throw new UnavailableException().from(rootProject); 180 } 181 String[] patterns = cmdData.patterns(); 182 String[] without = cmdData.without(); 183 if (parts.length > 1) { 184 patterns = new String[] { parts[0] }; 185 without = new String[0]; 186 } 187 for (var req : cmdData.requests()) { 188 if (!resources(rootProject.projects(patterns, without), req) 189 // eliminate duplicates 190 .collect(Collectors.toSet()).stream() 191 // output generated resources 192 .peek(r -> rootProject.context().out() 193 .println(r.toString())) 194 .map(r -> !(r instanceof FaultAware far) 195 || !far.isFaulty()) 196 .reduce(true, (r1, r2) -> r1 && r2)) { 197 return false; 198 } 199 } 200 } 201 return true; 202 } 203 204 private void printHelp() { 205 rootProject.context().out() 206 .println("Usage: jdbld [Options] command ..."); 207 rootProject.context().out().println(); 208 var cmds = new TreeMap<>(rootProject.commands()); 209 int maxNameLen = cmds.keySet().stream() 210 .mapToInt(String::length).max().orElse(0); 211 String fmt = " - %-" + maxNameLen + "s %s"; 212 rootProject.context().out().println("Available commands:"); 213 cmds.forEach((name, desc) -> rootProject.context().out() 214 .println(String.format(fmt, name, desc))); 215 } 216 217 /// This main can be used to start the user's JDrupes Builder 218 /// project from an IDE for debugging purposes. It expects that 219 /// the JDrupes Builder project has already been compiled (typically 220 /// by the IDE) and is available on the classpath. 221 /// 222 /// @param args the arguments 223 /// 224 @SuppressWarnings("PMD.SystemPrintln") 225 public static void main(String[] args) { 226 try { 227 if (!reportBuildException(() -> { 228 try (var bpl = new BuildProjectLauncher( 229 Thread.currentThread().getContextClassLoader(), 230 Path.of("").toAbsolutePath(), args)) { 231 return bpl.runCommands(); 232 } 233 })) { 234 Runtime.getRuntime().exit(1); 235 } 236 } catch (BuildException e) { 237 if (e.getCause() == null) { 238 logger.atSevere().log("Build failed: %s", 239 formatter().summary(e)); 240 } else { 241 logger.atSevere().withCause(e).log("Build failed: %s", 242 formatter().summary(e)); 243 } 244 System.out.println(formatter().summary(e)); 245 System.out.println(e.details()); 246 Runtime.getRuntime().exit(2); 247 } 248 } 249}