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
+
diff --git a/maven-plugin-plugin-bootstrap/pom.xml b/maven-plugin-plugin-bootstrap/pom.xml
new file mode 100644
index 000000000..6b7441a29
--- /dev/null
+++ b/maven-plugin-plugin-bootstrap/pom.xml
@@ -0,0 +1,205 @@
+
+
+
+ 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
+ provided
+
+
+ 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
+
+
+ maven.repo.local
+ ${settings.localRepository}
+
+
+
+ ${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..ea909d1e7
--- /dev/null
+++ b/maven-plugin-plugin-bootstrap/src/main/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGenerator.java
@@ -0,0 +1,447 @@
+/*
+ * 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.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;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+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.PluginExecution;
+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;
+import org.apache.maven.tools.plugin.DefaultPluginToolsRequest;
+import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
+import org.apache.maven.tools.plugin.generator.GeneratorUtils;
+import org.apache.maven.tools.plugin.generator.PluginDescriptorFilesGenerator;
+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.RepositorySystem;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+
+/**
+ * Minimal standalone runner to generate the bootstrap plugin descriptor.
+ */
+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();
+ 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, File... extraSourceRoots) throws Exception {
+ 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);
+
+ List sourceRoots = findCompileSourceRoots(model, baseDir);
+ if (extraSourceRoots != null) {
+ Collections.addAll(sourceRoots, extraSourceRoots);
+ }
+
+ String outPath = (model.getBuild() != null && model.getBuild().getOutputDirectory() != null)
+ ? model.getBuild().getOutputDirectory()
+ : "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();
+
+ 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);
+ for (File sourceRoot : sourceRoots) {
+ if (sourceRoot.isDirectory()) {
+ project.addCompileSourceRoot(sourceRoot.getAbsolutePath());
+ }
+ }
+
+ project.setArtifacts(populateDependencies(model));
+
+ container.addComponent(createMinimalRepositorySystem(), RepositorySystem.class, "");
+ MojoScanner mojoScanner = container.lookup(MojoScanner.class);
+
+ PluginDescriptor pluginDescriptor = buildPluginDescriptor(project, model);
+
+ DefaultPluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor);
+ request.setEncoding("UTF-8");
+ request.setSkipErrorNoDescriptorsFound(true);
+ request.setDependencies(buildScanArtifacts(StandaloneDescriptorGenerator.class.getClassLoader()));
+
+ mojoScanner.populatePluginDescriptor(request);
+
+ outputDirectory.mkdirs();
+ for (MojoDescriptor md : pluginDescriptor.getMojos()) {
+ if (md instanceof ExtendedMojoDescriptor && ((ExtendedMojoDescriptor) md).isV4Api()) {
+ generateV4Factory(md, classesDirectory);
+ }
+ }
+ DescriptorGeneratorMojo.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);
+ }
+
+ private static MavenProject newProject(Model model, File pomFile) {
+ MavenProject project = new MavenProject(model);
+ project.setFile(pomFile);
+ return project;
+ }
+
+ 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) {
+ PluginDescriptor pluginDescriptor = new PluginDescriptor();
+ pluginDescriptor.setGroupId(project.getGroupId());
+ pluginDescriptor.setArtifactId(project.getArtifactId());
+ pluginDescriptor.setVersion(project.getVersion());
+ 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 = project.getPrerequisites().getMaven();
+ if (requiredMavenVersion != null) {
+ pluginDescriptor.setRequiredMavenVersion(requiredMavenVersion);
+ }
+ }
+ String javaVersion = model.getProperties().getProperty("javaVersion");
+ if (javaVersion != null) {
+ if ("8".equals(javaVersion)) {
+ javaVersion = "1.8";
+ }
+ pluginDescriptor.setRequiredJavaVersion(javaVersion);
+ }
+ return pluginDescriptor;
+ }
+
+ /** Builds the fully inherited, interpolated model, the same way Maven itself would. */
+ 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(workspace, localRepository()));
+ return new DefaultModelBuilderFactory().newInstance().build(request).getEffectiveModel();
+ }
+
+ 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");
+ }
+
+ /** 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(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
+ + ".pom");
+ if (!pom.isFile()) {
+ throw new UnresolvableModelException("not found in " + localRepository, groupId, artifactId, version);
+ }
+ return new FileModelSource(pom);
+ }
+
+ @Override
+ public ModelSource resolveModel(Parent parent) throws UnresolvableModelException {
+ return resolveModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
+ }
+
+ @Override
+ public ModelSource resolveModel(Dependency dependency) throws UnresolvableModelException {
+ return resolveModel(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion());
+ }
+
+ @Override
+ public void addRepository(Repository repository) {}
+
+ @Override
+ public void addRepository(Repository repository, boolean replace) {}
+
+ @Override
+ public ModelResolver newCopy() {
+ return this;
+ }
+ }
+
+ 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;
+ }
+
+ static List findCompileSourceRoots(Model model, File baseDir) {
+ Set roots = new LinkedHashSet<>();
+ String srcPath = (model.getBuild() != null && model.getBuild().getSourceDirectory() != null)
+ ? model.getBuild().getSourceDirectory()
+ : "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, roots);
+ for (PluginExecution exec : plugin.getExecutions()) {
+ collectCompileSourceRoots(exec.getConfiguration(), baseDir, roots);
+ }
+ }
+ }
+ }
+ return new ArrayList<>(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 = val.trim();
+ File f = new File(s);
+ roots.add((f.isAbsolute() ? f : new File(baseDir, s)).getAbsoluteFile());
+ }
+ }
+ }
+ }
+ }
+
+ 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 depType = dep.getType() != null ? dep.getType() : "jar";
+ ArtifactHandler depHandler = new DefaultArtifactHandler(depType);
+ Artifact art = new DefaultArtifact(
+ dep.getGroupId(),
+ dep.getArtifactId(),
+ dep.getVersion(),
+ scope,
+ depType,
+ dep.getClassifier(),
+ depHandler);
+ artifacts.add(art);
+ }
+ return artifacts;
+ }
+
+ // 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<>();
+ int index = 0;
+ for (File file : entries) {
+ 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 RepositorySystem createMinimalRepositorySystem() {
+ return (RepositorySystem) Proxy.newProxyInstance(
+ RepositorySystem.class.getClassLoader(),
+ new Class>[] {RepositorySystem.class},
+ (proxy, method, args) -> {
+ 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");
+ }
+ });
+ }
+}
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..572b34f79
--- /dev/null
+++ b/maven-plugin-plugin-bootstrap/src/test/java/org/apache/maven/plugin/plugin/StandaloneDescriptorGeneratorTest.java
@@ -0,0 +1,236 @@
+/*
+ * 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.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.List;
+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.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;
+
+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 testFindCompileSourceRootsWalksParentChainAndInterpolatesPaths(@TempDir File tempDir) throws Exception {
+ File parentDir = new File(tempDir, "parent");
+ parentDir.mkdirs();
+ Files.write(
+ new File(parentDir, "pom.xml").toPath(),
+ ("4.0.0"
+ + "testparent1.0"
+ + "pom"
+ + "custom-src"
+ + "")
+ .getBytes());
+
+ File childDir = new File(tempDir, "child");
+ childDir.mkdirs();
+ File childPom = new File(childDir, "pom.xml");
+ Files.write(
+ childPom.toPath(),
+ ("4.0.0"
+ + "testparent"
+ + "1.0../parent/pom.xml"
+ + "child"
+ + "${srcDir}"
+ + "")
+ .getBytes());
+
+ 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");
+ classpathJar.createNewFile();
+
+ 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 class loader jar should be included for scanning");
+ }
+ }
+
+ @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);
+
+ 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");
+ 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());
+
+ 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/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/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-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);
}
}
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", "-->");
}
}
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}
+
+
+
+
+