From d9efd1ea16c78380387ec73e0ffc5ac22a5a6d00 Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Tue, 1 Sep 2026 22:20:38 +0200 Subject: [PATCH 01/12] Implement bootstrap module for plugin descriptor generation --- maven-plugin-plugin-bootstrap/pom.xml | 202 ++++++ .../plugin/StandaloneDescriptorGenerator.java | 641 ++++++++++++++++++ .../StandaloneDescriptorGeneratorTest.java | 226 ++++++ maven-plugin-plugin/pom.xml | 42 ++ pom.xml | 17 + 5 files changed, 1128 insertions(+) create mode 100644 maven-plugin-plugin-bootstrap/pom.xml create mode 100644 maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java create mode 100644 maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java diff --git a/maven-plugin-plugin-bootstrap/pom.xml b/maven-plugin-plugin-bootstrap/pom.xml new file mode 100644 index 000000000..f20395052 --- /dev/null +++ b/maven-plugin-plugin-bootstrap/pom.xml @@ -0,0 +1,202 @@ + + + + 4.0.0 + + + org.apache.maven.plugin-tools + maven-plugin-tools + 4.0.0-beta-4-SNAPSHOT + + + org.apache.maven.plugins + maven-plugin-plugin-bootstrap + jar + + Maven Plugin Plugin Bootstrap + Bootstrap module for generating Maven plugin descriptors outside Sisu/Plexus container. + + + true + true + true + + + + + org.apache.maven + maven-core + provided + + + org.apache.maven.plugin-tools + maven-plugin-tools-api + + + org.apache.maven.plugin-tools + maven-plugin-tools-generators + + + org.ow2.asm + asm + + + + + org.apache.maven.plugin-tools + maven-plugin-tools-annotations + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + + + + + org.apache.maven + maven-plugin-api + provided + + + org.apache.maven + maven-model + + + org.apache.maven + maven-settings + provided + + + org.apache.maven + maven-repository-metadata + ${maven3Version} + provided + + + org.apache.maven + maven-artifact + provided + + + org.apache.maven.resolver + maven-resolver-api + + + org.apache.maven.resolver + maven-resolver-util + + + javax.inject + javax.inject + 1 + provided + + + + + org.codehaus.plexus + plexus-utils + + + org.eclipse.sisu + org.eclipse.sisu.plexus + provided + + + org.codehaus.plexus + plexus-velocity + + + + + org.sonatype.plexus + plexus-build-api + 0.0.7 + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + + + ${project.basedir}/src/main/java + ${project.basedir}/../maven-plugin-plugin/src/main/java + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + + generate-bootstrap-descriptor + + java + + process-classes + + org.apache.maven.plugin.plugin.StandaloneDescriptorGenerator + test + + ${project.basedir}/pom.xml + + + + + + + org.apache.maven.plugins + maven-install-plugin + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java new file mode 100644 index 000000000..8661510ff --- /dev/null +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -0,0 +1,641 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin.plugin; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.handler.ArtifactHandler; +import org.apache.maven.artifact.handler.DefaultArtifactHandler; +import org.apache.maven.model.Build; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Model; +import org.apache.maven.model.Parent; +import org.apache.maven.model.Plugin; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.plugin.descriptor.MojoDescriptor; +import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.apache.maven.project.MavenProject; +import org.apache.maven.tools.plugin.DefaultPluginToolsRequest; +import org.apache.maven.tools.plugin.ExtendedMojoDescriptor; +import org.apache.maven.tools.plugin.extractor.annotations.JavaAnnotationsMojoDescriptorExtractor; +import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocBlockTagsToXhtmlConverter; +import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocInlineTagsToXhtmlConverter; +import org.apache.maven.tools.plugin.extractor.annotations.scanner.DefaultMojoAnnotationsScanner; +import org.apache.maven.tools.plugin.generator.GeneratorUtils; +import org.apache.maven.tools.plugin.generator.PluginDescriptorFilesGenerator; +import org.apache.maven.tools.plugin.scanner.DefaultMojoScanner; +import org.apache.maven.tools.plugin.scanner.MojoScanner; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.LocalRepositoryManager; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResult; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Minimal standalone runner to generate the bootstrap plugin descriptor container-free. + */ +public class StandaloneDescriptorGenerator { + + public static void main(String[] args) { + try { + File pomFile = + (args.length == 0) ? new File("pom.xml").getAbsoluteFile() : new File(args[0]).getAbsoluteFile(); + run(pomFile); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + + public static void run(File pomFile) throws Exception { + Model model = readModel(pomFile); + + Map properties = new LinkedHashMap<>(); + Map managedVersions = new LinkedHashMap<>(); + gatherPomChainInfo(pomFile, model, properties, managedVersions); + + File baseDir = pomFile.getParentFile(); + File sibling = new File(baseDir, "../maven-plugin-plugin/src/main/java").getAbsoluteFile(); + File sourceDirectory = sibling.isDirectory() ? sibling : new File(baseDir, "src/main/java").getAbsoluteFile(); + File classesDirectory = new File(baseDir, "target/classes").getAbsoluteFile(); + File outputDirectory = new File(classesDirectory, "META-INF/maven").getAbsoluteFile(); + + RepositorySystem repoSystem = createMinimalRepositorySystem(); + DefaultRepositorySystemSession repoSession = createRepositorySession(repoSystem); + + MavenProject project = newProject(model, pomFile, properties); + + ArtifactHandler projectHandler = new DefaultArtifactHandler("maven-plugin"); + Artifact projectArtifact = new DefaultArtifact( + project.getGroupId(), + project.getArtifactId(), + project.getVersion(), + null, + "maven-plugin", + "", + projectHandler); + project.setArtifact(projectArtifact); + + Build build = new Build(); + File targetDirectory = + classesDirectory.getParentFile() != null ? classesDirectory.getParentFile() : classesDirectory; + build.setDirectory(targetDirectory.getAbsolutePath()); + build.setOutputDirectory(classesDirectory.getAbsolutePath()); + project.setBuild(build); + project.addCompileSourceRoot(sourceDirectory.getAbsolutePath()); + File localSrc = new File(baseDir, "src/main/java"); + if (localSrc.isDirectory() && !localSrc.equals(sourceDirectory)) { + project.addCompileSourceRoot(localSrc.getAbsolutePath()); + } + + project.setArtifacts(populateDependencies(model, project, properties, managedVersions)); + + JavadocInlineTagsToXhtmlConverter inlineTagsConverter = + new JavadocInlineTagsToXhtmlConverter(Collections.emptyMap()); + JavadocBlockTagsToXhtmlConverter blockTagsConverter = + new JavadocBlockTagsToXhtmlConverter(inlineTagsConverter, Collections.emptyMap()); + + JavaAnnotationsMojoDescriptorExtractor extractor = new JavaAnnotationsMojoDescriptorExtractor(); + setField(extractor, "mojoAnnotationsScanner", new DefaultMojoAnnotationsScanner()); + setField(extractor, "javadocInlineTagsToHtmlConverter", inlineTagsConverter); + setField(extractor, "javadocBlockTagsToHtmlConverter", blockTagsConverter); + setField(extractor, "repositorySystem", repoSystem); + setField(extractor, "archiverManager", createArchiverManager()); + + MojoScanner mojoScanner = new DefaultMojoScanner(Collections.singletonMap("java-annotations", extractor)); + + PluginDescriptor pluginDescriptor = buildPluginDescriptor(project, model, properties); + + DefaultPluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); + request.setRepoSession(repoSession); + request.setEncoding("UTF-8"); + request.setSkipErrorNoDescriptorsFound(true); + request.setDependencies(buildScanArtifacts()); + + mojoScanner.populatePluginDescriptor(request); + + outputDirectory.mkdirs(); + for (MojoDescriptor md : pluginDescriptor.getMojos()) { + if (md instanceof ExtendedMojoDescriptor && ((ExtendedMojoDescriptor) md).isV4Api()) { + generateV4Factory(md, classesDirectory); + } + } + generateV4DiIndex(classesDirectory, outputDirectory); + + PluginDescriptorFilesGenerator generator = new PluginDescriptorFilesGenerator(); + generator.execute(outputDirectory, request); + + File generatedFile = new File(outputDirectory, "plugin.xml"); + if (!generatedFile.exists()) { + throw new IllegalStateException("Descriptor was not generated: " + generatedFile.getAbsolutePath()); + } + } + + private static void generateV4Factory(MojoDescriptor md, File classesDirectory) throws Exception { + String mojoClassName = md.getImplementation(); + String packageName = mojoClassName.substring(0, mojoClassName.lastIndexOf('.')); + String generatorClassName = mojoClassName.substring(mojoClassName.lastIndexOf('.') + 1) + "Factory"; + String mojoName = md.getId(); + + byte[] bin = DescriptorGeneratorMojo.computeGeneratorClassBytes( + packageName, generatorClassName, mojoName, mojoClassName); + + File packageDir = new File(classesDirectory, packageName.replace('.', '/')); + packageDir.mkdirs(); + File classFile = new File(packageDir, generatorClassName + ".class"); + Files.write(classFile.toPath(), bin); + } + + static void generateV4DiIndex(File classesDirectory, File outputDirectory) throws Exception { + Set diBeans = new TreeSet<>(); + try (Stream paths = Files.walk(classesDirectory.toPath())) { + List classFiles = paths.filter(p -> p.getFileName().toString().endsWith(".class")) + .collect(Collectors.toList()); + for (Path classFile : classFiles) { + try (InputStream is = Files.newInputStream(classFile)) { + ClassReader reader = new ClassReader(is); + reader.accept( + new ClassVisitor(Opcodes.ASM9) { + private String internalName; + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + internalName = name; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if ("Lorg/apache/maven/api/di/Named;".equals(descriptor)) { + diBeans.add(internalName.replace('/', '.')); + } + return null; + } + }, + ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); + } + } + } + + File indexFile = new File(outputDirectory, "org.apache.maven.api.di.Inject"); + if (diBeans.isEmpty()) { + Files.deleteIfExists(indexFile.toPath()); + } else { + StringBuilder content = new StringBuilder(); + for (String bean : diBeans) { + content.append(bean).append(System.lineSeparator()); + } + Files.write(indexFile.toPath(), content.toString().getBytes(StandardCharsets.UTF_8)); + } + } + + private static MavenProject newProject(Model model, File pomFile, Map properties) { + MavenProject project = new MavenProject(model); + project.setFile(pomFile); + if (project.getGroupId() == null && model.getParent() != null) { + project.setGroupId(model.getParent().getGroupId()); + } + if (project.getVersion() == null && model.getParent() != null) { + project.setVersion(model.getParent().getVersion()); + } + properties.put("project.groupId", project.getGroupId()); + properties.put("project.artifactId", project.getArtifactId()); + properties.put("project.version", project.getVersion()); + return project; + } + + private static String determineGoalPrefix(Model model, MavenProject project, Map properties) { + String goalPrefix = interpolate(findGoalPrefix(model), properties); + if (goalPrefix == null || goalPrefix.isEmpty()) { + goalPrefix = AbstractGeneratorMojo.getDefaultGoalPrefix(project); + } + return (goalPrefix == null || goalPrefix.isEmpty()) ? "plugin" : goalPrefix; + } + + private static void setField(Object target, String fieldName, Object value) throws Exception { + java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + private static PluginDescriptor buildPluginDescriptor( + MavenProject project, Model model, Map properties) { + PluginDescriptor pluginDescriptor = new PluginDescriptor(); + pluginDescriptor.setGroupId(project.getGroupId()); + pluginDescriptor.setArtifactId(project.getArtifactId()); + pluginDescriptor.setVersion(project.getVersion()); + pluginDescriptor.setGoalPrefix(determineGoalPrefix(model, project, properties)); + pluginDescriptor.setName(project.getName()); + pluginDescriptor.setDescription(project.getDescription()); + pluginDescriptor.setDependencies(GeneratorUtils.toComponentDependencies(project.getArtifacts())); + + if (project.getPrerequisites() != null) { + String requiredMavenVersion = interpolate(project.getPrerequisites().getMaven(), properties); + if (requiredMavenVersion != null) { + pluginDescriptor.setRequiredMavenVersion(requiredMavenVersion); + } + } + String javaVersion = properties.get("javaVersion"); + if (javaVersion != null) { + if ("8".equals(javaVersion)) { + javaVersion = "1.8"; + } + pluginDescriptor.setRequiredJavaVersion(javaVersion); + } + return pluginDescriptor; + } + + private static Model readModel(File pomFile) throws Exception { + MavenXpp3Reader pomReader = new MavenXpp3Reader(); + try (InputStream is = Files.newInputStream(pomFile.toPath())) { + return pomReader.read(is); + } + } + + static void gatherPomChainInfo( + File pomFile, Model model, Map properties, Map managedVersions) { + walkParentChain(pomFile, model, m -> { + putAllIfAbsent(properties, propertiesOf(m)); + addManagedVersions(managedVersions, m); + }); + } + + private static void addManagedVersions(Map managedVersions, Model model) { + if (model.getDependencyManagement() == null) { + return; + } + for (Dependency managed : model.getDependencyManagement().getDependencies()) { + managedVersions.putIfAbsent(managed.getGroupId() + ":" + managed.getArtifactId(), managed.getVersion()); + } + } + + private static void walkParentChain(File pomFile, Model model, java.util.function.Consumer visitor) { + visitor.accept(model); + + Set visitedPoms = new HashSet<>(); + visitedPoms.add(canonicalPath(pomFile)); + + File currentPomFile = pomFile; + Parent parent = model.getParent(); + while (parent != null) { + File parentPomFile = resolveParentPomFile(currentPomFile, parent); + if (parentPomFile == null || !visitedPoms.add(canonicalPath(parentPomFile))) { + break; + } + Model parentModel; + try { + parentModel = readModel(parentPomFile); + } catch (Exception e) { + break; + } + visitor.accept(parentModel); + currentPomFile = parentPomFile; + parent = parentModel.getParent(); + } + } + + private static String canonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (java.io.IOException e) { + return file.getAbsolutePath(); + } + } + + private static Map propertiesOf(Model model) { + Map result = new LinkedHashMap<>(); + if (model.getProperties() != null) { + for (String name : model.getProperties().stringPropertyNames()) { + result.put(name, model.getProperties().getProperty(name)); + } + } + return result; + } + + private static void putAllIfAbsent(Map target, Map source) { + for (Map.Entry entry : source.entrySet()) { + target.putIfAbsent(entry.getKey(), entry.getValue()); + } + } + + private static File resolveParentPomFile(File childPomFile, Parent parent) { + String relativePath = parent.getRelativePath(); + if (relativePath == null || relativePath.isEmpty()) { + relativePath = "../pom.xml"; + } + File parentDir = childPomFile.getAbsoluteFile().getParentFile(); + File candidate = new File(parentDir, relativePath); + if (candidate.isDirectory()) { + candidate = new File(candidate, "pom.xml"); + } + return candidate.isFile() ? candidate : null; + } + + static String interpolate(String value, Map properties) { + if (value == null || !value.contains("${")) { + return value; + } + String result = value; + for (int pass = 0; pass < 5 && result.contains("${"); pass++) { + boolean changed = false; + for (Map.Entry entry : properties.entrySet()) { + if (entry.getValue() == null) { + continue; + } + String placeholder = "${" + entry.getKey() + "}"; + if (result.contains(placeholder)) { + result = result.replace(placeholder, entry.getValue()); + changed = true; + } + } + if (!changed) { + break; + } + } + return result; + } + + static String findGoalPrefix(Model model) { + if (model.getBuild() == null) { + return null; + } + String prefix = findGoalPrefixInPlugins(model.getBuild().getPlugins()); + if (prefix != null) { + return prefix; + } + if (model.getBuild().getPluginManagement() != null) { + return findGoalPrefixInPlugins( + model.getBuild().getPluginManagement().getPlugins()); + } + return null; + } + + private static String findGoalPrefixInPlugins(List plugins) { + if (plugins == null) { + return null; + } + for (Plugin plugin : plugins) { + if ("maven-plugin-plugin".equals(plugin.getArtifactId())) { + Object configuration = plugin.getConfiguration(); + if (configuration instanceof Xpp3Dom) { + Xpp3Dom dom = (Xpp3Dom) configuration; + Xpp3Dom goalPrefixNode = dom.getChild("goalPrefix"); + if (goalPrefixNode != null) { + return goalPrefixNode.getValue(); + } + } + } + } + return null; + } + + private static Set populateDependencies( + Model model, MavenProject project, Map properties, Map managedVersions) { + Set artifacts = new HashSet<>(); + for (Dependency dep : model.getDependencies()) { + String scope = dep.getScope() != null ? dep.getScope() : "compile"; + if ("test".equals(scope) || "provided".equals(scope)) { + continue; + } + String depGroupId = interpolate(dep.getGroupId(), properties); + String depVersion = interpolate(dep.getVersion(), properties); + if (depVersion == null) { + depVersion = interpolate(managedVersions.get(depGroupId + ":" + dep.getArtifactId()), properties); + } + if (depVersion == null) { + depVersion = project.getVersion(); + } + String depType = dep.getType() != null ? dep.getType() : "jar"; + ArtifactHandler depHandler = new DefaultArtifactHandler(depType); + Artifact art = new DefaultArtifact( + depGroupId, dep.getArtifactId(), depVersion, scope, depType, dep.getClassifier(), depHandler); + artifacts.add(art); + } + return artifacts; + } + + static Set buildScanArtifacts() { + Set scanArtifacts = new HashSet<>(); + String classpath = System.getProperty("java.class.path", ""); + int index = 0; + for (String entry : classpath.split(File.pathSeparator)) { + if (entry.isEmpty()) { + continue; + } + File file = new File(entry); + if (!file.exists()) { + continue; + } + index++; + ArtifactHandler handler = new DefaultArtifactHandler("jar"); + Artifact syntheticArt = + new DefaultArtifact("classpath-scan-" + index, file.getName(), "0", "compile", "jar", "", handler); + syntheticArt.setFile(file); + scanArtifacts.add(syntheticArt); + } + return scanArtifacts; + } + + private static org.codehaus.plexus.archiver.manager.ArchiverManager createArchiverManager() { + return new org.codehaus.plexus.archiver.manager.ArchiverManager() { + @Override + public org.codehaus.plexus.archiver.Archiver getArchiver(String name) + throws org.codehaus.plexus.archiver.manager.NoSuchArchiverException { + if ("jar".equalsIgnoreCase(name)) { + return new org.codehaus.plexus.archiver.jar.JarArchiver(); + } + throw new org.codehaus.plexus.archiver.manager.NoSuchArchiverException(name); + } + + @Override + public org.codehaus.plexus.archiver.Archiver getArchiver(File file) + throws org.codehaus.plexus.archiver.manager.NoSuchArchiverException { + return getArchiver("jar"); + } + + @Override + public java.util.Collection getAvailableArchivers() { + return Collections.emptyList(); + } + + @Override + public org.codehaus.plexus.archiver.UnArchiver getUnArchiver(String name) + throws org.codehaus.plexus.archiver.manager.NoSuchArchiverException { + if ("jar".equalsIgnoreCase(name)) { + return new org.codehaus.plexus.archiver.zip.ZipUnArchiver(); + } + throw new org.codehaus.plexus.archiver.manager.NoSuchArchiverException(name); + } + + @Override + public org.codehaus.plexus.archiver.UnArchiver getUnArchiver(File file) + throws org.codehaus.plexus.archiver.manager.NoSuchArchiverException { + return getUnArchiver("jar"); + } + + @Override + public java.util.Collection getAvailableUnArchivers() { + return Collections.emptyList(); + } + + @Override + public org.codehaus.plexus.components.io.resources.PlexusIoResourceCollection getResourceCollection( + File file) { + return null; + } + + @Override + public org.codehaus.plexus.components.io.resources.PlexusIoResourceCollection getResourceCollection( + String name) { + return null; + } + + @Override + public java.util.Collection getAvailableResourceCollections() { + return Collections.emptyList(); + } + }; + } + + private static DefaultRepositorySystemSession createRepositorySession(RepositorySystem repoSystem) + throws Exception { + DefaultRepositorySystemSession repoSession = + org.apache.maven.repository.internal.MavenRepositorySystemUtils.newSession(); + File localRepoFile = Files.createTempDirectory("standalone-repo").toFile(); + localRepoFile.deleteOnExit(); + LocalRepository localRepo = new LocalRepository(localRepoFile); + LocalRepositoryManager lrm = repoSystem.newLocalRepositoryManager(repoSession, localRepo); + repoSession.setLocalRepositoryManager(lrm); + return repoSession; + } + + private static RepositorySystem createMinimalRepositorySystem() { + return (RepositorySystem) java.lang.reflect.Proxy.newProxyInstance( + RepositorySystem.class.getClassLoader(), + new Class[] {RepositorySystem.class}, + (proxy, method, args) -> { + switch (method.getName()) { + case "newLocalRepositoryManager": + return createNoopLocalRepositoryManager((LocalRepository) args[1]); + case "resolveArtifact": + throw new org.eclipse.aether.resolution.ArtifactResolutionException( + Collections.singletonList(new ArtifactResult((ArtifactRequest) args[1]))); + case "shutdown": + case "addOnSystemEndedHandler": + return null; + default: + throw new UnsupportedOperationException(method.getName()); + } + }); + } + + private static LocalRepositoryManager createNoopLocalRepositoryManager(LocalRepository localRepo) { + return new LocalRepositoryManager() { + @Override + public LocalRepository getRepository() { + return localRepo; + } + + @Override + public String getPathForLocalArtifact(org.eclipse.aether.artifact.Artifact artifact) { + return pathFor(artifact); + } + + @Override + public String getPathForRemoteArtifact( + org.eclipse.aether.artifact.Artifact artifact, + org.eclipse.aether.repository.RemoteRepository repository, + String context) { + return pathFor(artifact); + } + + @Override + public String getPathForLocalMetadata(org.eclipse.aether.metadata.Metadata metadata) { + return metadata.getGroupId() + "/" + metadata.getArtifactId() + "/" + metadata.getVersion(); + } + + @Override + public String getPathForRemoteMetadata( + org.eclipse.aether.metadata.Metadata metadata, + org.eclipse.aether.repository.RemoteRepository repository, + String context) { + return getPathForLocalMetadata(metadata); + } + + @Override + public org.eclipse.aether.repository.LocalArtifactResult find( + org.eclipse.aether.RepositorySystemSession session, + org.eclipse.aether.repository.LocalArtifactRequest request) { + return new org.eclipse.aether.repository.LocalArtifactResult(request); + } + + @Override + public void add( + org.eclipse.aether.RepositorySystemSession session, + org.eclipse.aether.repository.LocalArtifactRegistration request) {} + + @Override + public org.eclipse.aether.repository.LocalMetadataResult find( + org.eclipse.aether.RepositorySystemSession session, + org.eclipse.aether.repository.LocalMetadataRequest request) { + return new org.eclipse.aether.repository.LocalMetadataResult(request); + } + + @Override + public void add( + org.eclipse.aether.RepositorySystemSession session, + org.eclipse.aether.repository.LocalMetadataRegistration request) {} + + private String pathFor(org.eclipse.aether.artifact.Artifact artifact) { + String classifier = artifact.getClassifier(); + return artifact.getGroupId().replace('.', '/') + "/" + artifact.getArtifactId() + "/" + + artifact.getVersion() + "/" + artifact.getArtifactId() + "-" + artifact.getVersion() + + (classifier == null || classifier.isEmpty() ? "" : "-" + classifier) + "." + + artifact.getExtension(); + } + }; + } +} diff --git a/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java new file mode 100644 index 000000000..eaddbe5af --- /dev/null +++ b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin.plugin; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Build; +import org.apache.maven.model.Model; +import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginManagement; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StandaloneDescriptorGeneratorTest { + + @Test + void testFindGoalPrefix() { + Model model = new Model(); + assertNull(StandaloneDescriptorGenerator.findGoalPrefix(model)); + + Build build = new Build(); + model.setBuild(build); + assertNull(StandaloneDescriptorGenerator.findGoalPrefix(model)); + + Plugin plugin = new Plugin(); + plugin.setArtifactId("maven-plugin-plugin"); + build.addPlugin(plugin); + assertNull(StandaloneDescriptorGenerator.findGoalPrefix(model)); + + Xpp3Dom configuration = new Xpp3Dom("configuration"); + Xpp3Dom goalPrefix = new Xpp3Dom("goalPrefix"); + goalPrefix.setValue("custom-prefix"); + configuration.addChild(goalPrefix); + plugin.setConfiguration(configuration); + + assertEquals("custom-prefix", StandaloneDescriptorGenerator.findGoalPrefix(model)); + } + + @Test + void testFindGoalPrefixInPluginManagement() { + Model model = new Model(); + Build build = new Build(); + model.setBuild(build); + + PluginManagement pluginManagement = new PluginManagement(); + build.setPluginManagement(pluginManagement); + + Plugin plugin = new Plugin(); + plugin.setArtifactId("maven-plugin-plugin"); + pluginManagement.addPlugin(plugin); + + Xpp3Dom configuration = new Xpp3Dom("configuration"); + Xpp3Dom goalPrefix = new Xpp3Dom("goalPrefix"); + goalPrefix.setValue("mgmt-prefix"); + configuration.addChild(goalPrefix); + plugin.setConfiguration(configuration); + + assertEquals("mgmt-prefix", StandaloneDescriptorGenerator.findGoalPrefix(model)); + } + + @Test + void testInterpolate() { + Map properties = new HashMap<>(); + properties.put("foo", "bar"); + properties.put("project.version", "1.2.3"); + + assertEquals("bar", StandaloneDescriptorGenerator.interpolate("${foo}", properties)); + assertEquals("v1.2.3!", StandaloneDescriptorGenerator.interpolate("v${project.version}!", properties)); + assertEquals("${unknown}", StandaloneDescriptorGenerator.interpolate("${unknown}", properties)); + assertNull(StandaloneDescriptorGenerator.interpolate(null, properties)); + assertEquals("plain", StandaloneDescriptorGenerator.interpolate("plain", properties)); + } + + @Test + void testGatherPomChainInfoWalksLocalParentChain() throws Exception { + File tempRoot = Files.createTempDirectory("parent-chain").toFile(); + tempRoot.deleteOnExit(); + + File parentDir = new File(tempRoot, "parent"); + parentDir.mkdirs(); + File parentPom = new File(parentDir, "pom.xml"); + Files.write( + parentPom.toPath(), + ("4.0.0" + + "testparent1.0" + + "pom" + + "barfrom-parent" + + "") + .getBytes()); + + File childDir = new File(tempRoot, "child"); + childDir.mkdirs(); + File childPom = new File(childDir, "pom.xml"); + Files.write( + childPom.toPath(), + ("4.0.0" + + "testparent" + + "1.0../parent/pom.xml" + + "child" + + "quxfrom-child" + + "") + .getBytes()); + + MavenXpp3Reader reader = new MavenXpp3Reader(); + Model childModel; + try (InputStream is = Files.newInputStream(childPom.toPath())) { + childModel = reader.read(is); + } + + Map properties = new HashMap<>(); + Map managedVersions = new HashMap<>(); + StandaloneDescriptorGenerator.gatherPomChainInfo(childPom, childModel, properties, managedVersions); + assertEquals("qux", properties.get("baz")); + assertEquals("bar", properties.get("foo")); + assertEquals("from-child", properties.get("shared")); + } + + @Test + void testBuildScanArtifactsIncludesClasspathEntries() throws Exception { + String originalClasspath = System.getProperty("java.class.path"); + try { + File tempDir = Files.createTempDirectory("scan-artifacts").toFile(); + tempDir.deleteOnExit(); + File classpathJar = new File(tempDir, "somelib.jar"); + classpathJar.createNewFile(); + + System.setProperty("java.class.path", classpathJar.getAbsolutePath()); + + Set scanArtifacts = StandaloneDescriptorGenerator.buildScanArtifacts(); + + assertTrue( + scanArtifacts.stream() + .anyMatch(a -> classpathJar.getAbsoluteFile().equals(a.getFile())), + "the real classpath jar should be included for scanning"); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testRun(@TempDir File tempDir) throws Exception { + File pomFile = new File(tempDir, "pom.xml"); + String pomContent = "\n" + " 4.0.0\n" + + " org.example\n" + + " test-plugin\n" + + " 1.0.0\n" + + " maven-plugin\n" + + ""; + Files.write(pomFile.toPath(), pomContent.getBytes()); + + File classesDir = new File(tempDir, "target/classes"); + classesDir.mkdirs(); + + StandaloneDescriptorGenerator.run(pomFile); + + File pluginXml = new File(classesDir, "META-INF/maven/plugin.xml"); + assertTrue(pluginXml.exists(), "plugin.xml should be generated by run() in classesDirectory/META-INF/maven"); + } + + @Test + void generateV4DiIndexFindsNamedBeans(@TempDir File tempDir) throws Exception { + File classesDir = new File(tempDir, "classes"); + File outputDir = new File(tempDir, "output"); + File packageDir = new File(classesDir, "org/example"); + packageDir.mkdirs(); + outputDir.mkdirs(); + + byte[] bytes = DescriptorGeneratorMojo.computeGeneratorClassBytes( + "org.example", "SomeMojoFactory", "grp:art:1.0:goal", "java.lang.Object"); + Files.write(new File(packageDir, "SomeMojoFactory.class").toPath(), bytes); + + StandaloneDescriptorGenerator.generateV4DiIndex(classesDir, outputDir); + + File indexFile = new File(outputDir, "org.apache.maven.api.di.Inject"); + assertTrue(indexFile.exists(), "index file should be created when a @Named v4 bean is present"); + List lines = Files.readAllLines(indexFile.toPath()); + assertEquals(Collections.singletonList("org.example.SomeMojoFactory"), lines); + } + + @Test + void generateV4DiIndexDeletesStaleFileWhenNoNamedBeans(@TempDir File tempDir) throws Exception { + File classesDir = new File(tempDir, "classes"); + File outputDir = new File(tempDir, "output"); + classesDir.mkdirs(); + outputDir.mkdirs(); + File indexFile = new File(outputDir, "org.apache.maven.api.di.Inject"); + Files.write(indexFile.toPath(), "stale".getBytes()); + + StandaloneDescriptorGenerator.generateV4DiIndex(classesDir, outputDir); + + assertFalse(indexFile.exists(), "stale index file should be removed when no @Named v4 beans remain"); + } +} diff --git a/maven-plugin-plugin/pom.xml b/maven-plugin-plugin/pom.xml index 2558f0068..efa6ffd1c 100644 --- a/maven-plugin-plugin/pom.xml +++ b/maven-plugin-plugin/pom.xml @@ -286,5 +286,47 @@ + + bootstrap + + + + org.apache.maven.plugins + maven-plugin-plugin + + + default-descriptor + none + + + generated-helpmojo + none + + + + + org.apache.maven.plugins + maven-plugin-plugin-bootstrap + ${project.version} + + + bootstrap-helpmojo + + helpmojo + + generate-sources + + + bootstrap-descriptor + + descriptor + + process-classes + + + + + + diff --git a/pom.xml b/pom.xml index f3ca3a03c..341598b39 100644 --- a/pom.xml +++ b/pom.xml @@ -435,5 +435,22 @@ + + bootstrap + + maven-plugin-plugin-bootstrap + + + + + + org.apache.maven.plugins + maven-plugin-plugin + ${project.version} + + + + + From 521af9c043d2fcedf0efb3d844d9034690317c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fridrich=20=C5=A0trba?= Date: Sat, 19 Sep 2026 16:58:31 +0200 Subject: [PATCH 02/12] CI for bootstrap mode --- .github/workflows/maven-verify.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index cbc2e09f4..ad58838fb 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -29,3 +29,16 @@ jobs: maven4-enabled: true # build by Maven 3.x and 4.x maven4-version: '4.0.0-rc-6' # the same as used in project + bootstrap: + name: Verify bootstrap profile + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: maven + - name: Build with bootstrap profile + run: mvn -B -Pbootstrap verify + From 22f9963e7dffedebb2c729a478fb768730503c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fridrich=20=C5=A0trba?= Date: Sat, 19 Sep 2026 16:59:08 +0200 Subject: [PATCH 03/12] Replace reflection with real constructor --- .../plugin/StandaloneDescriptorGenerator.java | 18 +++------ ...avaAnnotationsMojoDescriptorExtractor.java | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java index 8661510ff..b645ef73a 100644 --- a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -132,12 +132,12 @@ public static void run(File pomFile) throws Exception { JavadocBlockTagsToXhtmlConverter blockTagsConverter = new JavadocBlockTagsToXhtmlConverter(inlineTagsConverter, Collections.emptyMap()); - JavaAnnotationsMojoDescriptorExtractor extractor = new JavaAnnotationsMojoDescriptorExtractor(); - setField(extractor, "mojoAnnotationsScanner", new DefaultMojoAnnotationsScanner()); - setField(extractor, "javadocInlineTagsToHtmlConverter", inlineTagsConverter); - setField(extractor, "javadocBlockTagsToHtmlConverter", blockTagsConverter); - setField(extractor, "repositorySystem", repoSystem); - setField(extractor, "archiverManager", createArchiverManager()); + JavaAnnotationsMojoDescriptorExtractor extractor = JavaAnnotationsMojoDescriptorExtractor.createStandalone( + new DefaultMojoAnnotationsScanner(), + repoSystem, + createArchiverManager(), + inlineTagsConverter, + blockTagsConverter); MojoScanner mojoScanner = new DefaultMojoScanner(Collections.singletonMap("java-annotations", extractor)); @@ -255,12 +255,6 @@ private static String determineGoalPrefix(Model model, MavenProject project, Map return (goalPrefix == null || goalPrefix.isEmpty()) ? "plugin" : goalPrefix; } - private static void setField(Object target, String fieldName, Object value) throws Exception { - java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } - private static PluginDescriptor buildPluginDescriptor( MavenProject project, Model model, Map properties) { PluginDescriptor pluginDescriptor = new PluginDescriptor(); diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java index c8ab3c427..acd8ce872 100644 --- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java +++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java @@ -158,6 +158,45 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt @Inject private JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter; + @Inject + public JavaAnnotationsMojoDescriptorExtractor() {} + + /** + * Constructor for callers outside the DI container (e.g. the bootstrap module) that need to + * assemble an instance without reflecting into private fields. + */ + JavaAnnotationsMojoDescriptorExtractor( + MojoAnnotationsScanner mojoAnnotationsScanner, + RepositorySystem repositorySystem, + ArchiverManager archiverManager, + JavadocInlineTagsToXhtmlConverter javadocInlineTagsToHtmlConverter, + JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter) { + this.mojoAnnotationsScanner = mojoAnnotationsScanner; + this.repositorySystem = repositorySystem; + this.archiverManager = archiverManager; + this.javadocInlineTagsToHtmlConverter = javadocInlineTagsToHtmlConverter; + this.javadocBlockTagsToHtmlConverter = javadocBlockTagsToHtmlConverter; + } + + /** + * Assembles an extractor outside the DI container, e.g. for the bootstrap module that + * generates the plugin descriptor of {@code maven-plugin-plugin} itself. Kept in this + * package so construction goes through the real constructor instead of field reflection. + */ + public static JavaAnnotationsMojoDescriptorExtractor createStandalone( + MojoAnnotationsScanner mojoAnnotationsScanner, + RepositorySystem repositorySystem, + ArchiverManager archiverManager, + JavadocInlineTagsToXhtmlConverter javadocInlineTagsToHtmlConverter, + JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter) { + return new JavaAnnotationsMojoDescriptorExtractor( + mojoAnnotationsScanner, + repositorySystem, + archiverManager, + javadocInlineTagsToHtmlConverter, + javadocBlockTagsToHtmlConverter); + } + @Override public String getName() { return NAME; From 3fc88fdbc09be55153a1a92780910218faaa4322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fridrich=20=C5=A0trba?= Date: Sat, 19 Sep 2026 17:25:44 +0200 Subject: [PATCH 04/12] Bootstrap descriptor generation through Sisu container --- .../plugin/StandaloneDescriptorGenerator.java | 225 +++--------------- ...avaAnnotationsMojoDescriptorExtractor.java | 39 --- 2 files changed, 35 insertions(+), 229 deletions(-) diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java index b645ef73a..c37cfa5c0 100644 --- a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -23,7 +23,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -48,28 +47,26 @@ import org.apache.maven.project.MavenProject; import org.apache.maven.tools.plugin.DefaultPluginToolsRequest; import org.apache.maven.tools.plugin.ExtendedMojoDescriptor; -import org.apache.maven.tools.plugin.extractor.annotations.JavaAnnotationsMojoDescriptorExtractor; -import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocBlockTagsToXhtmlConverter; -import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocInlineTagsToXhtmlConverter; -import org.apache.maven.tools.plugin.extractor.annotations.scanner.DefaultMojoAnnotationsScanner; import org.apache.maven.tools.plugin.generator.GeneratorUtils; import org.apache.maven.tools.plugin.generator.PluginDescriptorFilesGenerator; -import org.apache.maven.tools.plugin.scanner.DefaultMojoScanner; import org.apache.maven.tools.plugin.scanner.MojoScanner; +import org.codehaus.plexus.ContainerConfiguration; +import org.codehaus.plexus.DefaultContainerConfiguration; +import org.codehaus.plexus.DefaultPlexusContainer; +import org.codehaus.plexus.PlexusConstants; +import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.LocalRepositoryManager; -import org.eclipse.aether.resolution.ArtifactRequest; -import org.eclipse.aether.resolution.ArtifactResult; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; /** - * Minimal standalone runner to generate the bootstrap plugin descriptor container-free. + * Minimal standalone runner to generate the bootstrap plugin descriptor. */ public class StandaloneDescriptorGenerator { @@ -97,9 +94,6 @@ public static void run(File pomFile) throws Exception { File classesDirectory = new File(baseDir, "target/classes").getAbsoluteFile(); File outputDirectory = new File(classesDirectory, "META-INF/maven").getAbsoluteFile(); - RepositorySystem repoSystem = createMinimalRepositorySystem(); - DefaultRepositorySystemSession repoSession = createRepositorySession(repoSystem); - MavenProject project = newProject(model, pomFile, properties); ArtifactHandler projectHandler = new DefaultArtifactHandler("maven-plugin"); @@ -127,44 +121,43 @@ public static void run(File pomFile) throws Exception { project.setArtifacts(populateDependencies(model, project, properties, managedVersions)); - JavadocInlineTagsToXhtmlConverter inlineTagsConverter = - new JavadocInlineTagsToXhtmlConverter(Collections.emptyMap()); - JavadocBlockTagsToXhtmlConverter blockTagsConverter = - new JavadocBlockTagsToXhtmlConverter(inlineTagsConverter, Collections.emptyMap()); - - JavaAnnotationsMojoDescriptorExtractor extractor = JavaAnnotationsMojoDescriptorExtractor.createStandalone( - new DefaultMojoAnnotationsScanner(), - repoSystem, - createArchiverManager(), - inlineTagsConverter, - blockTagsConverter); + ContainerConfiguration containerConfiguration = new DefaultContainerConfiguration() + .setClassPathScanning(PlexusConstants.SCANNING_INDEX) + .setAutoWiring(true); + PlexusContainer container = new DefaultPlexusContainer(containerConfiguration); + try { + RepositorySystem repoSystem = container.lookup(RepositorySystem.class); + DefaultRepositorySystemSession repoSession = createRepositorySession(repoSystem); - MojoScanner mojoScanner = new DefaultMojoScanner(Collections.singletonMap("java-annotations", extractor)); + MojoScanner mojoScanner = container.lookup(MojoScanner.class); - PluginDescriptor pluginDescriptor = buildPluginDescriptor(project, model, properties); + PluginDescriptor pluginDescriptor = buildPluginDescriptor(project, model, properties); - DefaultPluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); - request.setRepoSession(repoSession); - request.setEncoding("UTF-8"); - request.setSkipErrorNoDescriptorsFound(true); - request.setDependencies(buildScanArtifacts()); + DefaultPluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); + request.setRepoSession(repoSession); + request.setEncoding("UTF-8"); + request.setSkipErrorNoDescriptorsFound(true); + request.setDependencies(buildScanArtifacts()); - mojoScanner.populatePluginDescriptor(request); + mojoScanner.populatePluginDescriptor(request); - outputDirectory.mkdirs(); - for (MojoDescriptor md : pluginDescriptor.getMojos()) { - if (md instanceof ExtendedMojoDescriptor && ((ExtendedMojoDescriptor) md).isV4Api()) { - generateV4Factory(md, classesDirectory); + outputDirectory.mkdirs(); + for (MojoDescriptor md : pluginDescriptor.getMojos()) { + if (md instanceof ExtendedMojoDescriptor && ((ExtendedMojoDescriptor) md).isV4Api()) { + generateV4Factory(md, classesDirectory); + } } - } - generateV4DiIndex(classesDirectory, outputDirectory); + generateV4DiIndex(classesDirectory, outputDirectory); - PluginDescriptorFilesGenerator generator = new PluginDescriptorFilesGenerator(); - generator.execute(outputDirectory, request); + PluginDescriptorFilesGenerator generator = new PluginDescriptorFilesGenerator(); + generator.execute(outputDirectory, request); - File generatedFile = new File(outputDirectory, "plugin.xml"); - if (!generatedFile.exists()) { - throw new IllegalStateException("Descriptor was not generated: " + generatedFile.getAbsolutePath()); + File generatedFile = new File(outputDirectory, "plugin.xml"); + if (!generatedFile.exists()) { + throw new IllegalStateException("Descriptor was not generated: " + generatedFile.getAbsolutePath()); + } + } finally { + container.dispose(); } } @@ -473,67 +466,6 @@ static Set buildScanArtifacts() { return scanArtifacts; } - private static org.codehaus.plexus.archiver.manager.ArchiverManager createArchiverManager() { - return new org.codehaus.plexus.archiver.manager.ArchiverManager() { - @Override - public org.codehaus.plexus.archiver.Archiver getArchiver(String name) - throws org.codehaus.plexus.archiver.manager.NoSuchArchiverException { - if ("jar".equalsIgnoreCase(name)) { - return new org.codehaus.plexus.archiver.jar.JarArchiver(); - } - throw new org.codehaus.plexus.archiver.manager.NoSuchArchiverException(name); - } - - @Override - public org.codehaus.plexus.archiver.Archiver getArchiver(File file) - throws org.codehaus.plexus.archiver.manager.NoSuchArchiverException { - return getArchiver("jar"); - } - - @Override - public java.util.Collection getAvailableArchivers() { - return Collections.emptyList(); - } - - @Override - public org.codehaus.plexus.archiver.UnArchiver getUnArchiver(String name) - throws org.codehaus.plexus.archiver.manager.NoSuchArchiverException { - if ("jar".equalsIgnoreCase(name)) { - return new org.codehaus.plexus.archiver.zip.ZipUnArchiver(); - } - throw new org.codehaus.plexus.archiver.manager.NoSuchArchiverException(name); - } - - @Override - public org.codehaus.plexus.archiver.UnArchiver getUnArchiver(File file) - throws org.codehaus.plexus.archiver.manager.NoSuchArchiverException { - return getUnArchiver("jar"); - } - - @Override - public java.util.Collection getAvailableUnArchivers() { - return Collections.emptyList(); - } - - @Override - public org.codehaus.plexus.components.io.resources.PlexusIoResourceCollection getResourceCollection( - File file) { - return null; - } - - @Override - public org.codehaus.plexus.components.io.resources.PlexusIoResourceCollection getResourceCollection( - String name) { - return null; - } - - @Override - public java.util.Collection getAvailableResourceCollections() { - return Collections.emptyList(); - } - }; - } - private static DefaultRepositorySystemSession createRepositorySession(RepositorySystem repoSystem) throws Exception { DefaultRepositorySystemSession repoSession = @@ -545,91 +477,4 @@ private static DefaultRepositorySystemSession createRepositorySession(Repository repoSession.setLocalRepositoryManager(lrm); return repoSession; } - - private static RepositorySystem createMinimalRepositorySystem() { - return (RepositorySystem) java.lang.reflect.Proxy.newProxyInstance( - RepositorySystem.class.getClassLoader(), - new Class[] {RepositorySystem.class}, - (proxy, method, args) -> { - switch (method.getName()) { - case "newLocalRepositoryManager": - return createNoopLocalRepositoryManager((LocalRepository) args[1]); - case "resolveArtifact": - throw new org.eclipse.aether.resolution.ArtifactResolutionException( - Collections.singletonList(new ArtifactResult((ArtifactRequest) args[1]))); - case "shutdown": - case "addOnSystemEndedHandler": - return null; - default: - throw new UnsupportedOperationException(method.getName()); - } - }); - } - - private static LocalRepositoryManager createNoopLocalRepositoryManager(LocalRepository localRepo) { - return new LocalRepositoryManager() { - @Override - public LocalRepository getRepository() { - return localRepo; - } - - @Override - public String getPathForLocalArtifact(org.eclipse.aether.artifact.Artifact artifact) { - return pathFor(artifact); - } - - @Override - public String getPathForRemoteArtifact( - org.eclipse.aether.artifact.Artifact artifact, - org.eclipse.aether.repository.RemoteRepository repository, - String context) { - return pathFor(artifact); - } - - @Override - public String getPathForLocalMetadata(org.eclipse.aether.metadata.Metadata metadata) { - return metadata.getGroupId() + "/" + metadata.getArtifactId() + "/" + metadata.getVersion(); - } - - @Override - public String getPathForRemoteMetadata( - org.eclipse.aether.metadata.Metadata metadata, - org.eclipse.aether.repository.RemoteRepository repository, - String context) { - return getPathForLocalMetadata(metadata); - } - - @Override - public org.eclipse.aether.repository.LocalArtifactResult find( - org.eclipse.aether.RepositorySystemSession session, - org.eclipse.aether.repository.LocalArtifactRequest request) { - return new org.eclipse.aether.repository.LocalArtifactResult(request); - } - - @Override - public void add( - org.eclipse.aether.RepositorySystemSession session, - org.eclipse.aether.repository.LocalArtifactRegistration request) {} - - @Override - public org.eclipse.aether.repository.LocalMetadataResult find( - org.eclipse.aether.RepositorySystemSession session, - org.eclipse.aether.repository.LocalMetadataRequest request) { - return new org.eclipse.aether.repository.LocalMetadataResult(request); - } - - @Override - public void add( - org.eclipse.aether.RepositorySystemSession session, - org.eclipse.aether.repository.LocalMetadataRegistration request) {} - - private String pathFor(org.eclipse.aether.artifact.Artifact artifact) { - String classifier = artifact.getClassifier(); - return artifact.getGroupId().replace('.', '/') + "/" + artifact.getArtifactId() + "/" - + artifact.getVersion() + "/" + artifact.getArtifactId() + "-" + artifact.getVersion() - + (classifier == null || classifier.isEmpty() ? "" : "-" + classifier) + "." - + artifact.getExtension(); - } - }; - } } diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java index acd8ce872..c8ab3c427 100644 --- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java +++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java @@ -158,45 +158,6 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt @Inject private JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter; - @Inject - public JavaAnnotationsMojoDescriptorExtractor() {} - - /** - * Constructor for callers outside the DI container (e.g. the bootstrap module) that need to - * assemble an instance without reflecting into private fields. - */ - JavaAnnotationsMojoDescriptorExtractor( - MojoAnnotationsScanner mojoAnnotationsScanner, - RepositorySystem repositorySystem, - ArchiverManager archiverManager, - JavadocInlineTagsToXhtmlConverter javadocInlineTagsToHtmlConverter, - JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter) { - this.mojoAnnotationsScanner = mojoAnnotationsScanner; - this.repositorySystem = repositorySystem; - this.archiverManager = archiverManager; - this.javadocInlineTagsToHtmlConverter = javadocInlineTagsToHtmlConverter; - this.javadocBlockTagsToHtmlConverter = javadocBlockTagsToHtmlConverter; - } - - /** - * Assembles an extractor outside the DI container, e.g. for the bootstrap module that - * generates the plugin descriptor of {@code maven-plugin-plugin} itself. Kept in this - * package so construction goes through the real constructor instead of field reflection. - */ - public static JavaAnnotationsMojoDescriptorExtractor createStandalone( - MojoAnnotationsScanner mojoAnnotationsScanner, - RepositorySystem repositorySystem, - ArchiverManager archiverManager, - JavadocInlineTagsToXhtmlConverter javadocInlineTagsToHtmlConverter, - JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter) { - return new JavaAnnotationsMojoDescriptorExtractor( - mojoAnnotationsScanner, - repositorySystem, - archiverManager, - javadocInlineTagsToHtmlConverter, - javadocBlockTagsToHtmlConverter); - } - @Override public String getName() { return NAME; From 899c869acc2d58158cc2c60641fb21f2ed918b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fridrich=20=C5=A0trba?= Date: Sun, 20 Sep 2026 14:32:45 +0200 Subject: [PATCH 05/12] Minimal repository system and get source roots from m-c-p --- maven-plugin-plugin-bootstrap/pom.xml | 5 +- .../plugin/StandaloneDescriptorGenerator.java | 104 +++++++++++++----- 2 files changed, 79 insertions(+), 30 deletions(-) diff --git a/maven-plugin-plugin-bootstrap/pom.xml b/maven-plugin-plugin-bootstrap/pom.xml index f20395052..72c21ed5f 100644 --- a/maven-plugin-plugin-bootstrap/pom.xml +++ b/maven-plugin-plugin-bootstrap/pom.xml @@ -98,10 +98,7 @@ org.apache.maven.resolver maven-resolver-api - - - org.apache.maven.resolver - maven-resolver-util + provided javax.inject diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java index c37cfa5c0..c4c1926d8 100644 --- a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -20,11 +20,15 @@ import java.io.File; import java.io.InputStream; +import java.lang.reflect.Proxy; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -41,6 +45,7 @@ import org.apache.maven.model.Model; import org.apache.maven.model.Parent; import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginExecution; import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; @@ -56,10 +61,8 @@ import org.codehaus.plexus.PlexusConstants; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.util.xml.Xpp3Dom; -import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; -import org.eclipse.aether.repository.LocalRepository; -import org.eclipse.aether.repository.LocalRepositoryManager; +import org.eclipse.aether.resolution.ArtifactResolutionException; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -74,14 +77,18 @@ public static void main(String[] args) { try { File pomFile = (args.length == 0) ? new File("pom.xml").getAbsoluteFile() : new File(args[0]).getAbsoluteFile(); - run(pomFile); + File[] extraSourceRoots = new File[Math.max(0, args.length - 1)]; + for (int i = 1; i < args.length; i++) { + extraSourceRoots[i - 1] = new File(args[i]).getAbsoluteFile(); + } + run(pomFile, extraSourceRoots); } catch (Exception e) { e.printStackTrace(); System.exit(1); } } - public static void run(File pomFile) throws Exception { + public static void run(File pomFile, File... extraSourceRoots) throws Exception { Model model = readModel(pomFile); Map properties = new LinkedHashMap<>(); @@ -89,9 +96,19 @@ public static void run(File pomFile) throws Exception { gatherPomChainInfo(pomFile, model, properties, managedVersions); File baseDir = pomFile.getParentFile(); - File sibling = new File(baseDir, "../maven-plugin-plugin/src/main/java").getAbsoluteFile(); - File sourceDirectory = sibling.isDirectory() ? sibling : new File(baseDir, "src/main/java").getAbsoluteFile(); - File classesDirectory = new File(baseDir, "target/classes").getAbsoluteFile(); + properties.putIfAbsent("basedir", baseDir.getAbsolutePath()); + properties.putIfAbsent("project.basedir", baseDir.getAbsolutePath()); + + List sourceRoots = findCompileSourceRoots(model, baseDir, properties); + if (extraSourceRoots != null) { + Collections.addAll(sourceRoots, extraSourceRoots); + } + + String outPath = (model.getBuild() != null && model.getBuild().getOutputDirectory() != null) + ? interpolate(model.getBuild().getOutputDirectory(), properties) + : "target/classes"; + File outDir = new File(outPath); + File classesDirectory = (outDir.isAbsolute() ? outDir : new File(baseDir, outPath)).getAbsoluteFile(); File outputDirectory = new File(classesDirectory, "META-INF/maven").getAbsoluteFile(); MavenProject project = newProject(model, pomFile, properties); @@ -113,10 +130,10 @@ public static void run(File pomFile) throws Exception { build.setDirectory(targetDirectory.getAbsolutePath()); build.setOutputDirectory(classesDirectory.getAbsolutePath()); project.setBuild(build); - project.addCompileSourceRoot(sourceDirectory.getAbsolutePath()); - File localSrc = new File(baseDir, "src/main/java"); - if (localSrc.isDirectory() && !localSrc.equals(sourceDirectory)) { - project.addCompileSourceRoot(localSrc.getAbsolutePath()); + for (File sourceRoot : sourceRoots) { + if (sourceRoot.isDirectory()) { + project.addCompileSourceRoot(sourceRoot.getAbsolutePath()); + } } project.setArtifacts(populateDependencies(model, project, properties, managedVersions)); @@ -126,15 +143,12 @@ public static void run(File pomFile) throws Exception { .setAutoWiring(true); PlexusContainer container = new DefaultPlexusContainer(containerConfiguration); try { - RepositorySystem repoSystem = container.lookup(RepositorySystem.class); - DefaultRepositorySystemSession repoSession = createRepositorySession(repoSystem); - + container.addComponent(createMinimalRepositorySystem(), RepositorySystem.class, ""); MojoScanner mojoScanner = container.lookup(MojoScanner.class); PluginDescriptor pluginDescriptor = buildPluginDescriptor(project, model, properties); DefaultPluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); - request.setRepoSession(repoSession); request.setEncoding("UTF-8"); request.setSkipErrorNoDescriptorsFound(true); request.setDependencies(buildScanArtifacts()); @@ -419,6 +433,44 @@ private static String findGoalPrefixInPlugins(List plugins) { return null; } + static List findCompileSourceRoots(Model model, File baseDir, Map properties) { + Set roots = new LinkedHashSet<>(); + String srcPath = (model.getBuild() != null && model.getBuild().getSourceDirectory() != null) + ? interpolate(model.getBuild().getSourceDirectory(), properties) + : "src/main/java"; + File defaultSrc = new File(srcPath); + roots.add((defaultSrc.isAbsolute() ? defaultSrc : new File(baseDir, srcPath)).getAbsoluteFile()); + + if (model.getBuild() != null && model.getBuild().getPlugins() != null) { + for (Plugin plugin : model.getBuild().getPlugins()) { + if ("maven-compiler-plugin".equals(plugin.getArtifactId())) { + collectCompileSourceRoots(plugin.getConfiguration(), baseDir, properties, roots); + for (PluginExecution exec : plugin.getExecutions()) { + collectCompileSourceRoots(exec.getConfiguration(), baseDir, properties, roots); + } + } + } + } + return new ArrayList<>(roots); + } + + private static void collectCompileSourceRoots( + Object config, File baseDir, Map properties, Set roots) { + if (config instanceof Xpp3Dom) { + Xpp3Dom compileSourceRoots = ((Xpp3Dom) config).getChild("compileSourceRoots"); + if (compileSourceRoots != null) { + for (Xpp3Dom child : compileSourceRoots.getChildren("compileSourceRoot")) { + String val = child.getValue(); + if (val != null && !val.trim().isEmpty()) { + String s = interpolate(val.trim(), properties); + File f = new File(s); + roots.add((f.isAbsolute() ? f : new File(baseDir, s)).getAbsoluteFile()); + } + } + } + } + } + private static Set populateDependencies( Model model, MavenProject project, Map properties, Map managedVersions) { Set artifacts = new HashSet<>(); @@ -466,15 +518,15 @@ static Set buildScanArtifacts() { return scanArtifacts; } - private static DefaultRepositorySystemSession createRepositorySession(RepositorySystem repoSystem) - throws Exception { - DefaultRepositorySystemSession repoSession = - org.apache.maven.repository.internal.MavenRepositorySystemUtils.newSession(); - File localRepoFile = Files.createTempDirectory("standalone-repo").toFile(); - localRepoFile.deleteOnExit(); - LocalRepository localRepo = new LocalRepository(localRepoFile); - LocalRepositoryManager lrm = repoSystem.newLocalRepositoryManager(repoSession, localRepo); - repoSession.setLocalRepositoryManager(lrm); - return repoSession; + private static RepositorySystem createMinimalRepositorySystem() { + return (RepositorySystem) Proxy.newProxyInstance( + RepositorySystem.class.getClassLoader(), + new Class[] {RepositorySystem.class}, + (proxy, method, args) -> { + if ("resolveArtifact".equals(method.getName())) { + throw new ArtifactResolutionException(Collections.emptyList()); + } + return null; + }); } } From 91f41090f6e6f39e2d29e4dff0c610261054631a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fridrich=20=C5=A0trba?= Date: Sun, 20 Sep 2026 18:08:14 +0200 Subject: [PATCH 06/12] Populate project.* properties before source-root/output-dir interpolation --- .../maven/plugin/plugin/StandaloneDescriptorGenerator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java index c4c1926d8..bd575915e 100644 --- a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -99,6 +99,8 @@ public static void run(File pomFile, File... extraSourceRoots) throws Exception properties.putIfAbsent("basedir", baseDir.getAbsolutePath()); properties.putIfAbsent("project.basedir", baseDir.getAbsolutePath()); + MavenProject project = newProject(model, pomFile, properties); + List sourceRoots = findCompileSourceRoots(model, baseDir, properties); if (extraSourceRoots != null) { Collections.addAll(sourceRoots, extraSourceRoots); @@ -111,8 +113,6 @@ public static void run(File pomFile, File... extraSourceRoots) throws Exception File classesDirectory = (outDir.isAbsolute() ? outDir : new File(baseDir, outPath)).getAbsoluteFile(); File outputDirectory = new File(classesDirectory, "META-INF/maven").getAbsoluteFile(); - MavenProject project = newProject(model, pomFile, properties); - ArtifactHandler projectHandler = new DefaultArtifactHandler("maven-plugin"); Artifact projectArtifact = new DefaultArtifact( project.getGroupId(), From 1d3f046c1886a4ecd34c8c21a1857d735c8c7f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fridrich=20=C5=A0trba?= Date: Sun, 20 Sep 2026 18:09:26 +0200 Subject: [PATCH 07/12] Strip stray newline jsoup inserts after HTML comments in javadoc conversion --- .../it/help-basic-deprecated-annotation-only/expected-help.txt | 2 +- maven-plugin-plugin/src/it/help-basic-jdk11/expected-help.txt | 2 +- maven-plugin-plugin/src/it/help-basic/expected-help.txt | 2 +- .../converter/JavadocInlineTagsToXhtmlConverter.java | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/maven-plugin-plugin/src/it/help-basic-deprecated-annotation-only/expected-help.txt b/maven-plugin-plugin/src/it/help-basic-deprecated-annotation-only/expected-help.txt index c3aeb824a..d05f112a2 100644 --- a/maven-plugin-plugin/src/it/help-basic-deprecated-annotation-only/expected-help.txt +++ b/maven-plugin-plugin/src/it/help-basic-deprecated-annotation-only/expected-help.txt @@ -6,7 +6,7 @@ help-deprecated-annotation-only:test MOJO-DESCRIPTION. Some "quotation" marks and backslashes '\\', some important javadoc - and an inline link to test.AnotherMojo . + and an inline link to test.AnotherMojo. Available parameters: diff --git a/maven-plugin-plugin/src/it/help-basic-jdk11/expected-help.txt b/maven-plugin-plugin/src/it/help-basic-jdk11/expected-help.txt index 069f241b3..25875a0eb 100644 --- a/maven-plugin-plugin/src/it/help-basic-jdk11/expected-help.txt +++ b/maven-plugin-plugin/src/it/help-basic-jdk11/expected-help.txt @@ -6,7 +6,7 @@ help-jdk11:test MOJO-DESCRIPTION. Some "quotation" marks and backslashes '\\', some important javadoc - and an inline link to test.AnotherMojo . + and an inline link to test.AnotherMojo. Available parameters: diff --git a/maven-plugin-plugin/src/it/help-basic/expected-help.txt b/maven-plugin-plugin/src/it/help-basic/expected-help.txt index 2bed0f0d1..68d99c2b3 100644 --- a/maven-plugin-plugin/src/it/help-basic/expected-help.txt +++ b/maven-plugin-plugin/src/it/help-basic/expected-help.txt @@ -6,7 +6,7 @@ help:test MOJO-DESCRIPTION. Some "quotation" marks and backslashes '\\', some important javadoc - and an inline link to test.AnotherMojo . + and an inline link to test.AnotherMojo. Available parameters: diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavadocInlineTagsToXhtmlConverter.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavadocInlineTagsToXhtmlConverter.java index 10553b4ad..03c8be712 100644 --- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavadocInlineTagsToXhtmlConverter.java +++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavadocInlineTagsToXhtmlConverter.java @@ -87,6 +87,7 @@ static String toXHTML(String bodySnippet) { String html = "" + bodySnippet + ""; // make it a valid HTML document final Document document = Jsoup.parse(html); document.outputSettings().syntax(Document.OutputSettings.Syntax.xml); - return document.body().html(); + // jsoup always inserts a newline after "-->", which later renders as a stray space + return document.body().html().replace("-->\n", "-->"); } } From 0d196be625a8542bbf7a4a1c5df999cff9615a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fridrich=20=C5=A0trba?= Date: Sun, 20 Sep 2026 19:43:01 +0200 Subject: [PATCH 08/12] Another attempt to remove manual pom walking --- maven-plugin-plugin-bootstrap/pom.xml | 6 + .../plugin/StandaloneDescriptorGenerator.java | 219 ++++++------------ .../StandaloneDescriptorGeneratorTest.java | 48 +--- 3 files changed, 91 insertions(+), 182 deletions(-) diff --git a/maven-plugin-plugin-bootstrap/pom.xml b/maven-plugin-plugin-bootstrap/pom.xml index 72c21ed5f..6b7441a29 100644 --- a/maven-plugin-plugin-bootstrap/pom.xml +++ b/maven-plugin-plugin-bootstrap/pom.xml @@ -173,6 +173,12 @@ org.apache.maven.plugin.plugin.StandaloneDescriptorGenerator test + + + maven.repo.local + ${settings.localRepository} + + ${project.basedir}/pom.xml diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java index bd575915e..461ffea8c 100644 --- a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -27,10 +27,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; @@ -46,7 +44,14 @@ import org.apache.maven.model.Parent; import org.apache.maven.model.Plugin; import org.apache.maven.model.PluginExecution; -import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.model.Repository; +import org.apache.maven.model.building.DefaultModelBuilderFactory; +import org.apache.maven.model.building.DefaultModelBuildingRequest; +import org.apache.maven.model.building.FileModelSource; +import org.apache.maven.model.building.ModelBuildingRequest; +import org.apache.maven.model.building.ModelSource; +import org.apache.maven.model.resolution.ModelResolver; +import org.apache.maven.model.resolution.UnresolvableModelException; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.project.MavenProject; @@ -89,25 +94,18 @@ public static void main(String[] args) { } public static void run(File pomFile, File... extraSourceRoots) throws Exception { - Model model = readModel(pomFile); - - Map properties = new LinkedHashMap<>(); - Map managedVersions = new LinkedHashMap<>(); - gatherPomChainInfo(pomFile, model, properties, managedVersions); - + Model model = buildEffectiveModel(pomFile); File baseDir = pomFile.getParentFile(); - properties.putIfAbsent("basedir", baseDir.getAbsolutePath()); - properties.putIfAbsent("project.basedir", baseDir.getAbsolutePath()); - MavenProject project = newProject(model, pomFile, properties); + MavenProject project = newProject(model, pomFile); - List sourceRoots = findCompileSourceRoots(model, baseDir, properties); + List sourceRoots = findCompileSourceRoots(model, baseDir); if (extraSourceRoots != null) { Collections.addAll(sourceRoots, extraSourceRoots); } String outPath = (model.getBuild() != null && model.getBuild().getOutputDirectory() != null) - ? interpolate(model.getBuild().getOutputDirectory(), properties) + ? model.getBuild().getOutputDirectory() : "target/classes"; File outDir = new File(outPath); File classesDirectory = (outDir.isAbsolute() ? outDir : new File(baseDir, outPath)).getAbsoluteFile(); @@ -136,7 +134,7 @@ public static void run(File pomFile, File... extraSourceRoots) throws Exception } } - project.setArtifacts(populateDependencies(model, project, properties, managedVersions)); + project.setArtifacts(populateDependencies(model)); ContainerConfiguration containerConfiguration = new DefaultContainerConfiguration() .setClassPathScanning(PlexusConstants.SCANNING_INDEX) @@ -146,7 +144,7 @@ public static void run(File pomFile, File... extraSourceRoots) throws Exception container.addComponent(createMinimalRepositorySystem(), RepositorySystem.class, ""); MojoScanner mojoScanner = container.lookup(MojoScanner.class); - PluginDescriptor pluginDescriptor = buildPluginDescriptor(project, model, properties); + PluginDescriptor pluginDescriptor = buildPluginDescriptor(project, model); DefaultPluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); request.setEncoding("UTF-8"); @@ -239,47 +237,37 @@ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { } } - private static MavenProject newProject(Model model, File pomFile, Map properties) { + private static MavenProject newProject(Model model, File pomFile) { MavenProject project = new MavenProject(model); project.setFile(pomFile); - if (project.getGroupId() == null && model.getParent() != null) { - project.setGroupId(model.getParent().getGroupId()); - } - if (project.getVersion() == null && model.getParent() != null) { - project.setVersion(model.getParent().getVersion()); - } - properties.put("project.groupId", project.getGroupId()); - properties.put("project.artifactId", project.getArtifactId()); - properties.put("project.version", project.getVersion()); return project; } - private static String determineGoalPrefix(Model model, MavenProject project, Map properties) { - String goalPrefix = interpolate(findGoalPrefix(model), properties); + private static String determineGoalPrefix(Model model, MavenProject project) { + String goalPrefix = findGoalPrefix(model); if (goalPrefix == null || goalPrefix.isEmpty()) { goalPrefix = AbstractGeneratorMojo.getDefaultGoalPrefix(project); } return (goalPrefix == null || goalPrefix.isEmpty()) ? "plugin" : goalPrefix; } - private static PluginDescriptor buildPluginDescriptor( - MavenProject project, Model model, Map properties) { + private static PluginDescriptor buildPluginDescriptor(MavenProject project, Model model) { PluginDescriptor pluginDescriptor = new PluginDescriptor(); pluginDescriptor.setGroupId(project.getGroupId()); pluginDescriptor.setArtifactId(project.getArtifactId()); pluginDescriptor.setVersion(project.getVersion()); - pluginDescriptor.setGoalPrefix(determineGoalPrefix(model, project, properties)); + pluginDescriptor.setGoalPrefix(determineGoalPrefix(model, project)); pluginDescriptor.setName(project.getName()); pluginDescriptor.setDescription(project.getDescription()); pluginDescriptor.setDependencies(GeneratorUtils.toComponentDependencies(project.getArtifacts())); if (project.getPrerequisites() != null) { - String requiredMavenVersion = interpolate(project.getPrerequisites().getMaven(), properties); + String requiredMavenVersion = project.getPrerequisites().getMaven(); if (requiredMavenVersion != null) { pluginDescriptor.setRequiredMavenVersion(requiredMavenVersion); } } - String javaVersion = properties.get("javaVersion"); + String javaVersion = model.getProperties().getProperty("javaVersion"); if (javaVersion != null) { if ("8".equals(javaVersion)) { javaVersion = "1.8"; @@ -289,114 +277,63 @@ private static PluginDescriptor buildPluginDescriptor( return pluginDescriptor; } - private static Model readModel(File pomFile) throws Exception { - MavenXpp3Reader pomReader = new MavenXpp3Reader(); - try (InputStream is = Files.newInputStream(pomFile.toPath())) { - return pomReader.read(is); - } - } - - static void gatherPomChainInfo( - File pomFile, Model model, Map properties, Map managedVersions) { - walkParentChain(pomFile, model, m -> { - putAllIfAbsent(properties, propertiesOf(m)); - addManagedVersions(managedVersions, m); - }); + /** Builds the fully inherited, interpolated model, the same way Maven itself would. */ + static Model buildEffectiveModel(File pomFile) throws Exception { + DefaultModelBuildingRequest request = new DefaultModelBuildingRequest(); + request.setPomFile(pomFile); + request.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL); + request.setProcessPlugins(false); + request.setSystemProperties(System.getProperties()); + request.setModelResolver(new LocalRepositoryModelResolver(localRepository())); + return new DefaultModelBuilderFactory().newInstance().build(request).getEffectiveModel(); } - private static void addManagedVersions(Map managedVersions, Model model) { - if (model.getDependencyManagement() == null) { - return; - } - for (Dependency managed : model.getDependencyManagement().getDependencies()) { - managedVersions.putIfAbsent(managed.getGroupId() + ":" + managed.getArtifactId(), managed.getVersion()); - } + private static File localRepository() { + String path = System.getProperty("maven.repo.local"); + return path != null ? new File(path) : new File(System.getProperty("user.home"), ".m2/repository"); } - private static void walkParentChain(File pomFile, Model model, java.util.function.Consumer visitor) { - visitor.accept(model); + /** Resolves parent/imported POMs already present in the local repository; no downloading. */ + private static final class LocalRepositoryModelResolver implements ModelResolver { + private final File localRepository; - Set visitedPoms = new HashSet<>(); - visitedPoms.add(canonicalPath(pomFile)); + LocalRepositoryModelResolver(File localRepository) { + this.localRepository = localRepository; + } - File currentPomFile = pomFile; - Parent parent = model.getParent(); - while (parent != null) { - File parentPomFile = resolveParentPomFile(currentPomFile, parent); - if (parentPomFile == null || !visitedPoms.add(canonicalPath(parentPomFile))) { - break; - } - Model parentModel; - try { - parentModel = readModel(parentPomFile); - } catch (Exception e) { - break; + @Override + public ModelSource resolveModel(String groupId, String artifactId, String version) + throws UnresolvableModelException { + File pom = new File( + localRepository, + groupId.replace('.', '/') + '/' + artifactId + '/' + version + '/' + artifactId + '-' + version + + ".pom"); + if (!pom.isFile()) { + throw new UnresolvableModelException("not found in " + localRepository, groupId, artifactId, version); } - visitor.accept(parentModel); - currentPomFile = parentPomFile; - parent = parentModel.getParent(); + return new FileModelSource(pom); } - } - private static String canonicalPath(File file) { - try { - return file.getCanonicalPath(); - } catch (java.io.IOException e) { - return file.getAbsolutePath(); + @Override + public ModelSource resolveModel(Parent parent) throws UnresolvableModelException { + return resolveModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion()); } - } - private static Map propertiesOf(Model model) { - Map result = new LinkedHashMap<>(); - if (model.getProperties() != null) { - for (String name : model.getProperties().stringPropertyNames()) { - result.put(name, model.getProperties().getProperty(name)); - } + @Override + public ModelSource resolveModel(Dependency dependency) throws UnresolvableModelException { + return resolveModel(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()); } - return result; - } - private static void putAllIfAbsent(Map target, Map source) { - for (Map.Entry entry : source.entrySet()) { - target.putIfAbsent(entry.getKey(), entry.getValue()); - } - } + @Override + public void addRepository(Repository repository) {} - private static File resolveParentPomFile(File childPomFile, Parent parent) { - String relativePath = parent.getRelativePath(); - if (relativePath == null || relativePath.isEmpty()) { - relativePath = "../pom.xml"; - } - File parentDir = childPomFile.getAbsoluteFile().getParentFile(); - File candidate = new File(parentDir, relativePath); - if (candidate.isDirectory()) { - candidate = new File(candidate, "pom.xml"); - } - return candidate.isFile() ? candidate : null; - } + @Override + public void addRepository(Repository repository, boolean replace) {} - static String interpolate(String value, Map properties) { - if (value == null || !value.contains("${")) { - return value; + @Override + public ModelResolver newCopy() { + return this; } - String result = value; - for (int pass = 0; pass < 5 && result.contains("${"); pass++) { - boolean changed = false; - for (Map.Entry entry : properties.entrySet()) { - if (entry.getValue() == null) { - continue; - } - String placeholder = "${" + entry.getKey() + "}"; - if (result.contains(placeholder)) { - result = result.replace(placeholder, entry.getValue()); - changed = true; - } - } - if (!changed) { - break; - } - } - return result; } static String findGoalPrefix(Model model) { @@ -433,10 +370,10 @@ private static String findGoalPrefixInPlugins(List plugins) { return null; } - static List findCompileSourceRoots(Model model, File baseDir, Map properties) { + static List findCompileSourceRoots(Model model, File baseDir) { Set roots = new LinkedHashSet<>(); String srcPath = (model.getBuild() != null && model.getBuild().getSourceDirectory() != null) - ? interpolate(model.getBuild().getSourceDirectory(), properties) + ? model.getBuild().getSourceDirectory() : "src/main/java"; File defaultSrc = new File(srcPath); roots.add((defaultSrc.isAbsolute() ? defaultSrc : new File(baseDir, srcPath)).getAbsoluteFile()); @@ -444,9 +381,9 @@ static List findCompileSourceRoots(Model model, File baseDir, Map findCompileSourceRoots(Model model, File baseDir, Map(roots); } - private static void collectCompileSourceRoots( - Object config, File baseDir, Map properties, Set roots) { + private static void collectCompileSourceRoots(Object config, File baseDir, Set roots) { if (config instanceof Xpp3Dom) { Xpp3Dom compileSourceRoots = ((Xpp3Dom) config).getChild("compileSourceRoots"); if (compileSourceRoots != null) { for (Xpp3Dom child : compileSourceRoots.getChildren("compileSourceRoot")) { String val = child.getValue(); if (val != null && !val.trim().isEmpty()) { - String s = interpolate(val.trim(), properties); + String s = val.trim(); File f = new File(s); roots.add((f.isAbsolute() ? f : new File(baseDir, s)).getAbsoluteFile()); } @@ -471,26 +407,23 @@ private static void collectCompileSourceRoots( } } - private static Set populateDependencies( - Model model, MavenProject project, Map properties, Map managedVersions) { + private static Set populateDependencies(Model model) { Set artifacts = new HashSet<>(); for (Dependency dep : model.getDependencies()) { String scope = dep.getScope() != null ? dep.getScope() : "compile"; if ("test".equals(scope) || "provided".equals(scope)) { continue; } - String depGroupId = interpolate(dep.getGroupId(), properties); - String depVersion = interpolate(dep.getVersion(), properties); - if (depVersion == null) { - depVersion = interpolate(managedVersions.get(depGroupId + ":" + dep.getArtifactId()), properties); - } - if (depVersion == null) { - depVersion = project.getVersion(); - } String depType = dep.getType() != null ? dep.getType() : "jar"; ArtifactHandler depHandler = new DefaultArtifactHandler(depType); Artifact art = new DefaultArtifact( - depGroupId, dep.getArtifactId(), depVersion, scope, depType, dep.getClassifier(), depHandler); + dep.getGroupId(), + dep.getArtifactId(), + dep.getVersion(), + scope, + depType, + dep.getClassifier(), + depHandler); artifacts.add(art); } return artifacts; diff --git a/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java index eaddbe5af..5142b5945 100644 --- a/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java +++ b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java @@ -19,12 +19,9 @@ package org.apache.maven.plugin.plugin; import java.io.File; -import java.io.InputStream; import java.nio.file.Files; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; import org.apache.maven.artifact.Artifact; @@ -32,7 +29,6 @@ import org.apache.maven.model.Model; import org.apache.maven.model.Plugin; import org.apache.maven.model.PluginManagement; -import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -90,36 +86,19 @@ void testFindGoalPrefixInPluginManagement() { } @Test - void testInterpolate() { - Map properties = new HashMap<>(); - properties.put("foo", "bar"); - properties.put("project.version", "1.2.3"); - - assertEquals("bar", StandaloneDescriptorGenerator.interpolate("${foo}", properties)); - assertEquals("v1.2.3!", StandaloneDescriptorGenerator.interpolate("v${project.version}!", properties)); - assertEquals("${unknown}", StandaloneDescriptorGenerator.interpolate("${unknown}", properties)); - assertNull(StandaloneDescriptorGenerator.interpolate(null, properties)); - assertEquals("plain", StandaloneDescriptorGenerator.interpolate("plain", properties)); - } - - @Test - void testGatherPomChainInfoWalksLocalParentChain() throws Exception { - File tempRoot = Files.createTempDirectory("parent-chain").toFile(); - tempRoot.deleteOnExit(); - - File parentDir = new File(tempRoot, "parent"); + void testFindCompileSourceRootsWalksParentChainAndInterpolatesPaths(@TempDir File tempDir) throws Exception { + File parentDir = new File(tempDir, "parent"); parentDir.mkdirs(); - File parentPom = new File(parentDir, "pom.xml"); Files.write( - parentPom.toPath(), + new File(parentDir, "pom.xml").toPath(), ("4.0.0" + "testparent1.0" + "pom" - + "barfrom-parent" + + "custom-src" + "") .getBytes()); - File childDir = new File(tempRoot, "child"); + File childDir = new File(tempDir, "child"); childDir.mkdirs(); File childPom = new File(childDir, "pom.xml"); Files.write( @@ -128,22 +107,13 @@ void testGatherPomChainInfoWalksLocalParentChain() throws Exception { + "testparent" + "1.0../parent/pom.xml" + "child" - + "quxfrom-child" + + "${srcDir}" + "") .getBytes()); - MavenXpp3Reader reader = new MavenXpp3Reader(); - Model childModel; - try (InputStream is = Files.newInputStream(childPom.toPath())) { - childModel = reader.read(is); - } - - Map properties = new HashMap<>(); - Map managedVersions = new HashMap<>(); - StandaloneDescriptorGenerator.gatherPomChainInfo(childPom, childModel, properties, managedVersions); - assertEquals("qux", properties.get("baz")); - assertEquals("bar", properties.get("foo")); - assertEquals("from-child", properties.get("shared")); + Model model = StandaloneDescriptorGenerator.buildEffectiveModel(childPom); + List roots = StandaloneDescriptorGenerator.findCompileSourceRoots(model, childDir); + assertEquals(Collections.singletonList(new File(childDir, "custom-src").getAbsoluteFile()), roots); } @Test From 96d8cae6859bd2783b523acd883d3c87991e401c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fridrich=20=C5=A0trba?= Date: Sun, 20 Sep 2026 19:39:43 +0200 Subject: [PATCH 09/12] Deduplicate v4 DI-index generation --- .../plugin/StandaloneDescriptorGenerator.java | 61 +--------- .../StandaloneDescriptorGeneratorTest.java | 4 +- .../plugin/DescriptorGeneratorMojo.java | 113 +++++++++--------- 3 files changed, 59 insertions(+), 119 deletions(-) diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java index 461ffea8c..7a04c892d 100644 --- a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -19,20 +19,14 @@ package org.apache.maven.plugin.plugin; import java.io.File; -import java.io.InputStream; import java.lang.reflect.Proxy; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DefaultArtifact; @@ -68,10 +62,6 @@ import org.codehaus.plexus.util.xml.Xpp3Dom; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.resolution.ArtifactResolutionException; -import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.Opcodes; /** * Minimal standalone runner to generate the bootstrap plugin descriptor. @@ -159,7 +149,7 @@ public static void run(File pomFile, File... extraSourceRoots) throws Exception generateV4Factory(md, classesDirectory); } } - generateV4DiIndex(classesDirectory, outputDirectory); + DescriptorGeneratorMojo.generateV4DiIndex(classesDirectory, outputDirectory); PluginDescriptorFilesGenerator generator = new PluginDescriptorFilesGenerator(); generator.execute(outputDirectory, request); @@ -188,55 +178,6 @@ private static void generateV4Factory(MojoDescriptor md, File classesDirectory) Files.write(classFile.toPath(), bin); } - static void generateV4DiIndex(File classesDirectory, File outputDirectory) throws Exception { - Set diBeans = new TreeSet<>(); - try (Stream paths = Files.walk(classesDirectory.toPath())) { - List classFiles = paths.filter(p -> p.getFileName().toString().endsWith(".class")) - .collect(Collectors.toList()); - for (Path classFile : classFiles) { - try (InputStream is = Files.newInputStream(classFile)) { - ClassReader reader = new ClassReader(is); - reader.accept( - new ClassVisitor(Opcodes.ASM9) { - private String internalName; - - @Override - public void visit( - int version, - int access, - String name, - String signature, - String superName, - String[] interfaces) { - super.visit(version, access, name, signature, superName, interfaces); - internalName = name; - } - - @Override - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if ("Lorg/apache/maven/api/di/Named;".equals(descriptor)) { - diBeans.add(internalName.replace('/', '.')); - } - return null; - } - }, - ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); - } - } - } - - File indexFile = new File(outputDirectory, "org.apache.maven.api.di.Inject"); - if (diBeans.isEmpty()) { - Files.deleteIfExists(indexFile.toPath()); - } else { - StringBuilder content = new StringBuilder(); - for (String bean : diBeans) { - content.append(bean).append(System.lineSeparator()); - } - Files.write(indexFile.toPath(), content.toString().getBytes(StandardCharsets.UTF_8)); - } - } - private static MavenProject newProject(Model model, File pomFile) { MavenProject project = new MavenProject(model); project.setFile(pomFile); diff --git a/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java index 5142b5945..55fa76135 100644 --- a/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java +++ b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java @@ -172,7 +172,7 @@ void generateV4DiIndexFindsNamedBeans(@TempDir File tempDir) throws Exception { "org.example", "SomeMojoFactory", "grp:art:1.0:goal", "java.lang.Object"); Files.write(new File(packageDir, "SomeMojoFactory.class").toPath(), bytes); - StandaloneDescriptorGenerator.generateV4DiIndex(classesDir, outputDir); + DescriptorGeneratorMojo.generateV4DiIndex(classesDir, outputDir); File indexFile = new File(outputDir, "org.apache.maven.api.di.Inject"); assertTrue(indexFile.exists(), "index file should be created when a @Named v4 bean is present"); @@ -189,7 +189,7 @@ void generateV4DiIndexDeletesStaleFileWhenNoNamedBeans(@TempDir File tempDir) th File indexFile = new File(outputDir, "org.apache.maven.api.di.Inject"); Files.write(indexFile.toPath(), "stale".getBytes()); - StandaloneDescriptorGenerator.generateV4DiIndex(classesDir, outputDir); + DescriptorGeneratorMojo.generateV4DiIndex(classesDir, outputDir); assertFalse(indexFile.exists(), "stale index file should be removed when no @Named v4 beans remain"); } diff --git a/maven-plugin-plugin/src/main/java/org/apache/maven/plugin/plugin/DescriptorGeneratorMojo.java b/maven-plugin-plugin/src/main/java/org/apache/maven/plugin/plugin/DescriptorGeneratorMojo.java index 781994b97..287131656 100644 --- a/maven-plugin-plugin/src/main/java/org/apache/maven/plugin/plugin/DescriptorGeneratorMojo.java +++ b/maven-plugin-plugin/src/main/java/org/apache/maven/plugin/plugin/DescriptorGeneratorMojo.java @@ -434,67 +434,66 @@ public void generate() throws MojoExecutionException { private void generateIndex() throws GeneratorException { try { - Set diBeans = new TreeSet<>(); - try (Stream paths = Files.walk(classesOutputDirectory.toPath())) { - List classes = paths.filter( - p -> p.getFileName().toString().endsWith(".class")) - .collect(Collectors.toList()); - for (Path classFile : classes) { - String fileString = classFile.toString(); - String className = fileString - .substring(0, fileString.length() - ".class".length()) - .replace('/', '.'); - try (InputStream is = Files.newInputStream(classFile)) { - ClassReader rdr = new ClassReader(is); - rdr.accept( - new ClassVisitor(Opcodes.ASM9) { - String className; - - @Override - public void visit( - int version, - int access, - String name, - String signature, - String superName, - String[] interfaces) { - super.visit(version, access, name, signature, superName, interfaces); - className = name; - } - - @Override - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if ("Lorg/apache/maven/api/di/Named;".equals(descriptor)) { - diBeans.add(className.replace('/', '.')); + generateV4DiIndex(classesOutputDirectory, outputDirectory); + } catch (Exception e) { + throw new GeneratorException("Unable to generate index for v4 beans", e); + } + } + + /** Scans compiled classes for {@code @Named} v4 DI beans and writes/removes the Sisu index file. */ + static void generateV4DiIndex(File classesDirectory, File outputDirectory) throws Exception { + Set diBeans = new TreeSet<>(); + try (Stream paths = Files.walk(classesDirectory.toPath())) { + for (Path classFile : paths.filter(p -> p.getFileName().toString().endsWith(".class")) + .collect(Collectors.toList())) { + try (InputStream is = Files.newInputStream(classFile)) { + new ClassReader(is) + .accept( + new ClassVisitor(Opcodes.ASM9) { + private String internalName; + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + internalName = name; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if ("Lorg/apache/maven/api/di/Named;".equals(descriptor)) { + diBeans.add(internalName.replace('/', '.')); + } + return null; } - return null; - } - }, - ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); - } - - // Class clazz = project.getClassRealm().loadClass(className); - // boolean hasQualifier = Stream.of(clazz.getAnnotations()) - // .flatMap(ann -> Stream.of(ann.getClass().getAnnotations())) - // .anyMatch(ann -> "org.apache.maven.api.di.Qualifier" - // .equals(ann.annotationType().getName())); - // if (hasQualifier) { - // diBeans.add(className); - // } + }, + ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); } + + // Class clazz = project.getClassRealm().loadClass(className); + // boolean hasQualifier = Stream.of(clazz.getAnnotations()) + // .flatMap(ann -> Stream.of(ann.getClass().getAnnotations())) + // .anyMatch(ann -> "org.apache.maven.api.di.Qualifier" + // .equals(ann.annotationType().getName())); + // if (hasQualifier) { + // diBeans.add(className); + // } } - Path path = outputDirectory.toPath().resolve("org.apache.maven.api.di.Inject"); - if (diBeans.isEmpty()) { - Files.deleteIfExists(path); - } else { - String nl = System.lineSeparator(); - try (CachingWriter w = new CachingWriter(path, StandardCharsets.UTF_8)) { - String content = diBeans.stream().collect(Collectors.joining(nl, "", nl)); - w.write(content); - } + } + Path path = outputDirectory.toPath().resolve("org.apache.maven.api.di.Inject"); + if (diBeans.isEmpty()) { + Files.deleteIfExists(path); + } else { + String content = + diBeans.stream().collect(Collectors.joining(System.lineSeparator(), "", System.lineSeparator())); + try (CachingWriter w = new CachingWriter(path, StandardCharsets.UTF_8)) { + w.write(content); } - } catch (Exception e) { - throw new GeneratorException("Unable to generate index for v4 beans", e); } } From b7bdbde728cadac504bebe83a592238f9f95e01c Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Fri, 16 Oct 2026 14:22:06 +0200 Subject: [PATCH 10/12] Scan classloader URLs, not java.class.path, for bootstrap descriptor deps --- .../plugin/StandaloneDescriptorGenerator.java | 30 ++++++++++++++----- .../StandaloneDescriptorGeneratorTest.java | 24 ++++++--------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java index 7a04c892d..2495dba47 100644 --- a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -20,6 +20,9 @@ import java.io.File; import java.lang.reflect.Proxy; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; @@ -139,7 +142,7 @@ public static void run(File pomFile, File... extraSourceRoots) throws Exception DefaultPluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); request.setEncoding("UTF-8"); request.setSkipErrorNoDescriptorsFound(true); - request.setDependencies(buildScanArtifacts()); + request.setDependencies(buildScanArtifacts(StandaloneDescriptorGenerator.class.getClassLoader())); mojoScanner.populatePluginDescriptor(request); @@ -370,15 +373,26 @@ private static Set populateDependencies(Model model) { return artifacts; } - static Set buildScanArtifacts() { + // exec:java runs in Maven's JVM: java.class.path is only the launcher, real classpath is in the URLClassLoader + static Set buildScanArtifacts(ClassLoader loader) throws URISyntaxException { + Set entries = new LinkedHashSet<>(); + for (ClassLoader cl = loader; cl != null; cl = cl.getParent()) { + if (cl instanceof URLClassLoader) { + for (URL url : ((URLClassLoader) cl).getURLs()) { + if ("file".equals(url.getProtocol())) { + entries.add(new File(url.toURI())); + } + } + } + } + for (String entry : System.getProperty("java.class.path", "").split(File.pathSeparator)) { + if (!entry.isEmpty()) { + entries.add(new File(entry).getAbsoluteFile()); + } + } Set scanArtifacts = new HashSet<>(); - String classpath = System.getProperty("java.class.path", ""); int index = 0; - for (String entry : classpath.split(File.pathSeparator)) { - if (entry.isEmpty()) { - continue; - } - File file = new File(entry); + for (File file : entries) { if (!file.exists()) { continue; } diff --git a/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java index 55fa76135..feda89e7c 100644 --- a/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java +++ b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java @@ -19,6 +19,8 @@ package org.apache.maven.plugin.plugin; import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.file.Files; import java.util.Collections; import java.util.List; @@ -117,26 +119,18 @@ void testFindCompileSourceRootsWalksParentChainAndInterpolatesPaths(@TempDir Fil } @Test - void testBuildScanArtifactsIncludesClasspathEntries() throws Exception { - String originalClasspath = System.getProperty("java.class.path"); - try { - File tempDir = Files.createTempDirectory("scan-artifacts").toFile(); - tempDir.deleteOnExit(); - File classpathJar = new File(tempDir, "somelib.jar"); - classpathJar.createNewFile(); + void testBuildScanArtifactsIncludesClassLoaderEntries(@TempDir File tempDir) throws Exception { + File classpathJar = new File(tempDir, "somelib.jar"); + classpathJar.createNewFile(); - System.setProperty("java.class.path", classpathJar.getAbsolutePath()); - - Set scanArtifacts = StandaloneDescriptorGenerator.buildScanArtifacts(); + try (URLClassLoader loader = + new URLClassLoader(new URL[] {classpathJar.toURI().toURL()}, null)) { + Set scanArtifacts = StandaloneDescriptorGenerator.buildScanArtifacts(loader); assertTrue( scanArtifacts.stream() .anyMatch(a -> classpathJar.getAbsoluteFile().equals(a.getFile())), - "the real classpath jar should be included for scanning"); - } finally { - if (originalClasspath != null) { - System.setProperty("java.class.path", originalClasspath); - } + "the class loader jar should be included for scanning"); } } From dffc159f1a6529d058a4a6bc81185d06caf54f02 Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Fri, 23 Oct 2026 18:30:17 +0200 Subject: [PATCH 11/12] Fail fast on unsupported RepositorySystem calls in bootstrap proxy --- .../plugin/StandaloneDescriptorGenerator.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java index 2495dba47..b32a960ea 100644 --- a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -411,10 +411,19 @@ private static RepositorySystem createMinimalRepositorySystem() { RepositorySystem.class.getClassLoader(), new Class[] {RepositorySystem.class}, (proxy, method, args) -> { - if ("resolveArtifact".equals(method.getName())) { - throw new ArtifactResolutionException(Collections.emptyList()); + switch (method.getName()) { + case "resolveArtifact": + throw new ArtifactResolutionException(Collections.emptyList()); + case "equals": + return proxy == args[0]; + case "hashCode": + return System.identityHashCode(proxy); + case "toString": + return "bootstrap RepositorySystem"; + default: + throw new UnsupportedOperationException( + "RepositorySystem." + method.getName() + "() not supported in bootstrap mode"); } - return null; }); } } From 54b4459a3f352f816e25673229c0d4e8196fa322 Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Wed, 28 Oct 2026 10:48:16 +0100 Subject: [PATCH 12/12] Resolve parent POMs via "ide" WorkspaceReader when present --- .../plugin/StandaloneDescriptorGenerator.java | 82 +++++++++++-------- .../StandaloneDescriptorGeneratorTest.java | 48 ++++++++++- 2 files changed, 97 insertions(+), 33 deletions(-) diff --git a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java index b32a960ea..ea909d1e7 100644 --- a/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java +++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java @@ -64,6 +64,7 @@ import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.repository.WorkspaceReader; import org.eclipse.aether.resolution.ArtifactResolutionException; /** @@ -87,7 +88,23 @@ public static void main(String[] args) { } public static void run(File pomFile, File... extraSourceRoots) throws Exception { - Model model = buildEffectiveModel(pomFile); + ContainerConfiguration containerConfiguration = new DefaultContainerConfiguration() + .setClassPathScanning(PlexusConstants.SCANNING_INDEX) + .setAutoWiring(true); + PlexusContainer container = new DefaultPlexusContainer(containerConfiguration); + try { + generate(container, pomFile, extraSourceRoots); + } finally { + container.dispose(); + } + } + + private static void generate(PlexusContainer container, File pomFile, File... extraSourceRoots) throws Exception { + // same hook Maven uses for IDE/xmvn workspaces; absent in a plain Maven build + WorkspaceReader workspace = container.hasComponent(WorkspaceReader.class, "ide") + ? container.lookup(WorkspaceReader.class, "ide") + : null; + Model model = buildEffectiveModel(pomFile, workspace); File baseDir = pomFile.getParentFile(); MavenProject project = newProject(model, pomFile); @@ -129,40 +146,32 @@ public static void run(File pomFile, File... extraSourceRoots) throws Exception project.setArtifacts(populateDependencies(model)); - ContainerConfiguration containerConfiguration = new DefaultContainerConfiguration() - .setClassPathScanning(PlexusConstants.SCANNING_INDEX) - .setAutoWiring(true); - PlexusContainer container = new DefaultPlexusContainer(containerConfiguration); - try { - container.addComponent(createMinimalRepositorySystem(), RepositorySystem.class, ""); - MojoScanner mojoScanner = container.lookup(MojoScanner.class); + container.addComponent(createMinimalRepositorySystem(), RepositorySystem.class, ""); + MojoScanner mojoScanner = container.lookup(MojoScanner.class); - PluginDescriptor pluginDescriptor = buildPluginDescriptor(project, model); + PluginDescriptor pluginDescriptor = buildPluginDescriptor(project, model); - DefaultPluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); - request.setEncoding("UTF-8"); - request.setSkipErrorNoDescriptorsFound(true); - request.setDependencies(buildScanArtifacts(StandaloneDescriptorGenerator.class.getClassLoader())); + DefaultPluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); + request.setEncoding("UTF-8"); + request.setSkipErrorNoDescriptorsFound(true); + request.setDependencies(buildScanArtifacts(StandaloneDescriptorGenerator.class.getClassLoader())); - mojoScanner.populatePluginDescriptor(request); + mojoScanner.populatePluginDescriptor(request); - outputDirectory.mkdirs(); - for (MojoDescriptor md : pluginDescriptor.getMojos()) { - if (md instanceof ExtendedMojoDescriptor && ((ExtendedMojoDescriptor) md).isV4Api()) { - generateV4Factory(md, classesDirectory); - } + outputDirectory.mkdirs(); + for (MojoDescriptor md : pluginDescriptor.getMojos()) { + if (md instanceof ExtendedMojoDescriptor && ((ExtendedMojoDescriptor) md).isV4Api()) { + generateV4Factory(md, classesDirectory); } - DescriptorGeneratorMojo.generateV4DiIndex(classesDirectory, outputDirectory); + } + DescriptorGeneratorMojo.generateV4DiIndex(classesDirectory, outputDirectory); - PluginDescriptorFilesGenerator generator = new PluginDescriptorFilesGenerator(); - generator.execute(outputDirectory, request); + PluginDescriptorFilesGenerator generator = new PluginDescriptorFilesGenerator(); + generator.execute(outputDirectory, request); - File generatedFile = new File(outputDirectory, "plugin.xml"); - if (!generatedFile.exists()) { - throw new IllegalStateException("Descriptor was not generated: " + generatedFile.getAbsolutePath()); - } - } finally { - container.dispose(); + File generatedFile = new File(outputDirectory, "plugin.xml"); + if (!generatedFile.exists()) { + throw new IllegalStateException("Descriptor was not generated: " + generatedFile.getAbsolutePath()); } } @@ -222,13 +231,13 @@ private static PluginDescriptor buildPluginDescriptor(MavenProject project, Mode } /** Builds the fully inherited, interpolated model, the same way Maven itself would. */ - static Model buildEffectiveModel(File pomFile) throws Exception { + static Model buildEffectiveModel(File pomFile, WorkspaceReader workspace) throws Exception { DefaultModelBuildingRequest request = new DefaultModelBuildingRequest(); request.setPomFile(pomFile); request.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL); request.setProcessPlugins(false); request.setSystemProperties(System.getProperties()); - request.setModelResolver(new LocalRepositoryModelResolver(localRepository())); + request.setModelResolver(new LocalRepositoryModelResolver(workspace, localRepository())); return new DefaultModelBuilderFactory().newInstance().build(request).getEffectiveModel(); } @@ -237,17 +246,26 @@ private static File localRepository() { return path != null ? new File(path) : new File(System.getProperty("user.home"), ".m2/repository"); } - /** Resolves parent/imported POMs already present in the local repository; no downloading. */ + /** Resolves parent/imported POMs via the workspace reader or the local repository; no downloading. */ private static final class LocalRepositoryModelResolver implements ModelResolver { + private final WorkspaceReader workspace; private final File localRepository; - LocalRepositoryModelResolver(File localRepository) { + LocalRepositoryModelResolver(WorkspaceReader workspace, File localRepository) { + this.workspace = workspace; this.localRepository = localRepository; } @Override public ModelSource resolveModel(String groupId, String artifactId, String version) throws UnresolvableModelException { + if (workspace != null) { + File pom = workspace.findArtifact( + new org.eclipse.aether.artifact.DefaultArtifact(groupId, artifactId, "", "pom", version)); + if (pom != null && pom.isFile()) { + return new FileModelSource(pom); + } + } File pom = new File( localRepository, groupId.replace('.', '/') + '/' + artifactId + '/' + version + '/' + artifactId + '-' + version diff --git a/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java index feda89e7c..572b34f79 100644 --- a/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java +++ b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java @@ -32,6 +32,8 @@ import org.apache.maven.model.Plugin; import org.apache.maven.model.PluginManagement; import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.eclipse.aether.repository.WorkspaceReader; +import org.eclipse.aether.repository.WorkspaceRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -113,11 +115,55 @@ void testFindCompileSourceRootsWalksParentChainAndInterpolatesPaths(@TempDir Fil + "") .getBytes()); - Model model = StandaloneDescriptorGenerator.buildEffectiveModel(childPom); + Model model = StandaloneDescriptorGenerator.buildEffectiveModel(childPom, null); List roots = StandaloneDescriptorGenerator.findCompileSourceRoots(model, childDir); assertEquals(Collections.singletonList(new File(childDir, "custom-src").getAbsoluteFile()), roots); } + @Test + void testBuildEffectiveModelResolvesParentThroughWorkspaceReader(@TempDir File tempDir) throws Exception { + File parentPom = new File(tempDir, "somewhere-else.pom"); + Files.write( + parentPom.toPath(), + ("4.0.0" + + "testws-parent1.0" + + "pom" + + "yes" + + "") + .getBytes()); + File childPom = new File(tempDir, "pom.xml"); + Files.write( + childPom.toPath(), + ("4.0.0" + + "testws-parent" + + "1.0" + + "child" + + "") + .getBytes()); + + WorkspaceReader workspace = new WorkspaceReader() { + @Override + public WorkspaceRepository getRepository() { + return new WorkspaceRepository("test"); + } + + @Override + public File findArtifact(org.eclipse.aether.artifact.Artifact artifact) { + return "ws-parent".equals(artifact.getArtifactId()) && "pom".equals(artifact.getExtension()) + ? parentPom + : null; + } + + @Override + public List findVersions(org.eclipse.aether.artifact.Artifact artifact) { + return Collections.emptyList(); + } + }; + + Model model = StandaloneDescriptorGenerator.buildEffectiveModel(childPom, workspace); + assertEquals("yes", model.getProperties().getProperty("fromParent")); + } + @Test void testBuildScanArtifactsIncludesClassLoaderEntries(@TempDir File tempDir) throws Exception { File classpathJar = new File(tempDir, "somelib.jar");