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} + + + + +