001/* 002 * JDrupes Builder 003 * Copyright (C) 2025 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 eu.maveniverse.maven.mima.context.Context; 022import eu.maveniverse.maven.mima.context.ContextOverrides; 023import eu.maveniverse.maven.mima.context.Runtime; 024import eu.maveniverse.maven.mima.context.Runtimes; 025import java.net.MalformedURLException; 026import java.net.URL; 027import java.net.URLClassLoader; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.List; 031import java.util.logging.Level; 032import java.util.stream.Stream; 033import org.eclipse.aether.artifact.DefaultArtifact; 034import org.eclipse.aether.collection.CollectRequest; 035import org.eclipse.aether.graph.Dependency; 036import org.eclipse.aether.graph.DependencyNode; 037import org.eclipse.aether.resolution.DependencyRequest; 038import org.eclipse.aether.resolution.DependencyResolutionException; 039import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator; 040import org.jdrupes.builder.api.BuildException; 041import org.jdrupes.builder.api.Launcher; 042import org.jdrupes.builder.api.Masked; 043import org.jdrupes.builder.api.Project; 044import org.jdrupes.builder.api.Resource; 045import org.jdrupes.builder.api.ResourceFactory; 046import org.jdrupes.builder.api.ResourceRequest; 047import org.jdrupes.builder.api.RootProject; 048import org.jdrupes.builder.core.LauncherSupport; 049import org.jdrupes.builder.java.JarFile; 050import static org.jdrupes.builder.java.JavaTypes.*; 051 052/// An implementation of a [Launcher] that expects that the JDrupes 053/// Builder project already been compiled and its classes are available 054/// on the classpath. 055/// 056public class DirectLauncher extends AbstractLauncher { 057 058 private static final String RUNTIME_EXTENSIONS = "runtimeExtensions"; 059 private RootProject rootProject; 060 061 /// Instantiates a new direct launcher. The classpath is scanned for 062 /// classes that implement [Project] but do not implement [Masked]. 063 /// One of these must also implement the [RootProject] interface. 064 /// The latter is instantiated and registered as root project with all 065 /// other classes found as direct sub projects. 066 /// 067 /// @param classloader the classloader 068 /// @param args the arguments 069 /// 070 @SuppressWarnings({ "PMD.UseVarargs", 071 "PMD.AvoidInstantiatingObjectsInLoops", "PMD.SystemPrintln" }) 072 public DirectLauncher(ClassLoader classloader, String[] args) { 073 super(args); 074 unwrapBuildException(() -> { 075 final var extClsLdr = addExtensions(classloader); 076 var rootProjects = new ArrayList<Class<? extends RootProject>>(); 077 var subprojects = new ArrayList<Class<? extends Project>>(); 078 findProjects(extClsLdr, rootProjects, subprojects); 079 rootProject = LauncherSupport.createProjects(rootProjects.get(0), 080 subprojects, jdbldProps, commandLine); 081 for (var arg : args) { 082 var reqs = LauncherSupport.lookupCommand(rootProject, arg); 083 if (reqs.length == 0) { 084 new BuildException("Unknown command: " + arg); 085 } 086 for (var req : reqs) { 087 rootProject.get(req) 088 .forEach(r -> System.out.println(r.toString())); 089 } 090 } 091 return null; 092 }); 093 } 094 095 private ClassLoader addExtensions(ClassLoader classloader) { 096 String[] coordinates = Stream.concat(Arrays.asList(jdbldProps 097 .getProperty(BootstrapBuild.BUILD_EXTENSIONS, "").split(",")) 098 .stream(), 099 Arrays.asList(jdbldProps.getProperty(RUNTIME_EXTENSIONS, "") 100 .split(",")).stream()) 101 .map(String::trim).filter(c -> !c.isBlank()).toArray(String[]::new); 102 if (coordinates.length == 0) { 103 return classloader; 104 } 105 106 // Resolve using maven repo 107 var cpUrls = resolveRequested(coordinates).mapMulti((jf, consumer) -> { 108 try { 109 consumer.accept(jf.path().toFile().toURI().toURL()); 110 } catch (MalformedURLException e) { 111 log.log(Level.WARNING, e, () -> "Cannot convert " + jf 112 + " to URL: " + e.getMessage()); 113 } 114 }).toArray(URL[]::new); 115 116 // Return augmented classloader 117 return new URLClassLoader(cpUrls, classloader); 118 } 119 120 @SuppressWarnings({ "PMD.UseVarargs", 121 "PMD.AvoidInstantiatingObjectsInLoops" }) 122 private Stream<JarFile> resolveRequested(String[] coordinates) { 123 ContextOverrides overrides = ContextOverrides.create() 124 .withUserSettings(true).build(); 125 Runtime runtime = Runtimes.INSTANCE.getRuntime(); 126 try (Context context = runtime.create(overrides)) { 127 CollectRequest collectRequest = new CollectRequest() 128 .setRepositories(context.remoteRepositories()); 129 for (var coord : coordinates) { 130 collectRequest.addDependency( 131 new Dependency(new DefaultArtifact(coord), "runtime")); 132 } 133 134 DependencyRequest dependencyRequest 135 = new DependencyRequest(collectRequest, null); 136 DependencyNode rootNode; 137 try { 138 rootNode = context.repositorySystem() 139 .resolveDependencies(context.repositorySystemSession(), 140 dependencyRequest) 141 .getRoot(); 142 PreorderNodeListGenerator nlg = new PreorderNodeListGenerator(); 143 rootNode.accept(nlg); 144 List<DependencyNode> dependencyNodes = nlg.getNodes(); 145 return dependencyNodes.stream() 146 .filter(d -> d.getArtifact() != null) 147 .map(d -> d.getArtifact().getFile().toPath()) 148 .map(p -> ResourceFactory 149 .create(JarFileType, p)); 150 } catch (DependencyResolutionException e) { 151 throw new BuildException( 152 "Cannot resolve: " + e.getMessage(), e); 153 } 154 } 155 } 156 157 @Override 158 public <T extends Resource> Stream<T> provide(ResourceRequest<T> request) { 159 return unwrapBuildException(() -> { 160 // Provide requested resource, handling all exceptions here 161 var result = rootProject.get(request).toList(); 162 return result.stream(); 163 }); 164 } 165 166 /// This main can be used to start the user's JDrupes Builder 167 /// project from an IDE for debugging purposes. It expects that 168 /// the JDrupes Builder project has already been compiled (typically 169 /// by the IDE) and is available on the classpath. 170 /// 171 /// @param args the arguments 172 /// 173 public static void main(String[] args) { 174 new DirectLauncher(Thread.currentThread().getContextClassLoader(), 175 args); 176 } 177}