Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/maven-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ jobs:
build:
name: Verify
uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v4
with:
maven-matrix: '[ "3.6.3", "3.9.7", "4.0.0-beta-3" ]'
jdk-matrix: '[ "8", "17", "21" ]'
matrix-exclude: '[ { maven: "4.0.0-beta-3", jdk: "8" } ]'

1 change: 1 addition & 0 deletions maven-plugin-plugin/src/it/v4api/invoker.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
# specific language governing permissions and limitations
# under the License.

invoker.java.version = 17+
invoker.goals.1 = clean install
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@

import org.apache.maven.api.MojoExecution;
import org.apache.maven.api.Project;
import org.apache.maven.api.ResolutionScope;
import org.apache.maven.api.Session;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Component;
import org.apache.maven.api.plugin.annotations.Execute;
import org.apache.maven.api.plugin.annotations.LifecyclePhase;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;
import org.apache.maven.api.services.ArtifactInstaller;
Expand All @@ -38,15 +37,12 @@
* Test mojo for the v4 api plugin descriptor generation.
* This mojo is not actually runnable because:
* - it's using a custom lifecycle which is not defined
* - it has a @Component dependency on ArtifactInstaller (hint=test) which does not exist
* - it has a @Inject dependency on ArtifactInstaller (hint=test) which does not exist
*
* @since 1.2
*/
@Mojo(
name = "first",
requiresDependencyResolution = ResolutionScope.TEST,
defaultPhase = LifecyclePhase.INTEGRATION_TEST)
@Execute(phase = LifecyclePhase.GENERATE_SOURCES, lifecycle = "cobertura")
@Mojo(name = "first", defaultPhase = "integration-test")
@Execute(phase = "generate-sources", lifecycle = "cobertura")
public class FirstMojo implements org.apache.maven.api.plugin.Mojo {

/**
Expand All @@ -66,23 +62,24 @@ public class FirstMojo implements org.apache.maven.api.plugin.Mojo {
@Parameter(name = "namedParam", alias = "alias")
private String aliasedParam;

@Component
@Inject
private Session session;

@Component
@Inject
private Project project;

@Component
@Inject
private MojoExecution mojo;

@Component
@Inject
private Settings settings;

@Component
@Inject
private Log log;

@Component(role = ArtifactInstaller.class, hint = "test")
private Object custom;
@Inject
@Named("test")
private ArtifactInstaller custom;

public void execute() throws MojoException {}
}
53 changes: 10 additions & 43 deletions maven-plugin-plugin/src/it/v4api/verify.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -24,60 +24,21 @@ assert descriptorFile.isFile()

def pluginDescriptor = new XmlParser().parse( descriptorFile );

assert pluginDescriptor.requiredJavaVersion.text() == '1.8'
assert pluginDescriptor.requiredMavenVersion.text() == '4.0.0-alpha-4'
assert pluginDescriptor.requiredJavaVersion.text() == '17'
assert pluginDescriptor.requiredMavenVersion.text() == '4.0.0-beta-3'

def mojo = pluginDescriptor.mojos.mojo.findAll{ it.goal.text() == "first" }[0]

assert mojo.goal.text() == 'first'
assert mojo.implementation.text() == 'org.apache.maven.its.v4api.FirstMojo'
assert mojo.language.text() == 'java'
assert mojo.description.text().startsWith('Test mojo for the v4 api plugin descriptor generation.')
assert mojo.requiresDependencyResolution.text() == 'test'
assert mojo.requiresDependencyCollection.text() == ''
assert mojo.requiresProject.text() == 'true'
assert mojo.requiresOnline.text() == 'false'
assert mojo.requiresDirectInvocation.text() == 'false'
assert mojo.projectRequired.text() == 'true'
assert mojo.onlineRequired.text() == 'false'
assert mojo.aggregator.text() == 'false'
assert mojo.threadSafe.text() == 'false'
assert mojo.phase.text() == 'integration-test'
assert mojo.executePhase.text() == 'generate-sources'
assert mojo.executeLifecycle.text() == 'cobertura'
assert mojo.v4Api.text() == 'true'

assert mojo.configuration.basedir[0].text() == ''
assert mojo.configuration.basedir[0].'@implementation' == 'java.nio.file.Path'
assert mojo.configuration.basedir[0].'@default-value' == '${basedir}'

assert mojo.configuration.touchFile[0].text() == '${first.touchFile}'
assert mojo.configuration.touchFile[0].'@implementation' == 'java.nio.file.Path'
assert mojo.configuration.touchFile[0].'@default-value' == '${project.build.directory}/touch.txt'

assert mojo.requirements.requirement.size() == 6

assert mojo.requirements.requirement[0].role.text() == 'org.apache.maven.api.services.ArtifactInstaller'
assert mojo.requirements.requirement[0].'role-hint'.text() == 'test'
assert mojo.requirements.requirement[0].'field-name'.text() == 'custom'

assert mojo.requirements.requirement[1].role.text() == 'org.apache.maven.api.plugin.Log'
assert mojo.requirements.requirement[1].'role-hint'.isEmpty()
assert mojo.requirements.requirement[1].'field-name'.text() == 'log'

assert mojo.requirements.requirement[2].role.text() == 'org.apache.maven.api.MojoExecution'
assert mojo.requirements.requirement[2].'role-hint'.isEmpty()
assert mojo.requirements.requirement[2].'field-name'.text() == 'mojo'

assert mojo.requirements.requirement[3].role.text() == 'org.apache.maven.api.Project'
assert mojo.requirements.requirement[3].'role-hint'.isEmpty()
assert mojo.requirements.requirement[3].'field-name'.text() == 'project'

assert mojo.requirements.requirement[4].role.text() == 'org.apache.maven.api.Session'
assert mojo.requirements.requirement[4].'role-hint'.isEmpty()
assert mojo.requirements.requirement[4].'field-name'.text() == 'session'

assert mojo.requirements.requirement[5].role.text() == 'org.apache.maven.api.settings.Settings'
assert mojo.requirements.requirement[5].'role-hint'.isEmpty()
assert mojo.requirements.requirement[5].'field-name'.text() == 'settings'

assert mojo.parameters.parameter.size() == 3

Expand All @@ -90,6 +51,8 @@ assert parameter.deprecated.isEmpty()
assert parameter.required.text() == 'false'
assert parameter.editable.text() == 'false'
assert parameter.description.text() == 'Project directory.'
assert parameter.defaultValue.text() == '${basedir}'
assert parameter.expression.isEmpty()

parameter = mojo.parameters.parameter.findAll{ it.name.text() == "touchFile" }[0]
assert parameter.name.text() == 'touchFile'
Expand All @@ -100,6 +63,8 @@ assert parameter.deprecated.isEmpty()
assert parameter.required.text() == 'true'
assert parameter.editable.text() == 'true'
assert parameter.description.text() == ''
assert parameter.defaultValue.text() == '${project.build.directory}/touch.txt'
assert parameter.expression.text() == '${first.touchFile}'

parameter = mojo.parameters.parameter.findAll{ it.name.text() == "namedParam" }[0]
assert parameter.name.text() == 'namedParam'
Expand All @@ -110,6 +75,8 @@ assert parameter.deprecated.text() == 'As of 0.2'
assert parameter.required.text() == 'false'
assert parameter.editable.text() == 'true'
assert parameter.description.text() == ''
assert parameter.defaultValue.isEmpty()
assert parameter.expression.isEmpty()

// check help mojo source and class
assert new File( basedir, "target/classes/org/apache/maven/its/v4api/HelpMojo.class" ).isFile()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,32 @@
package org.apache.maven.plugin.plugin;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.tools.plugin.DefaultPluginToolsRequest;
import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
import org.apache.maven.tools.plugin.ExtendedPluginDescriptor;
import org.apache.maven.tools.plugin.PluginToolsRequest;
import org.apache.maven.tools.plugin.extractor.ExtractionException;
Expand All @@ -48,8 +54,14 @@
import org.apache.maven.tools.plugin.scanner.MojoScanner;
import org.codehaus.plexus.component.repository.ComponentDependency;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.io.CachingOutputStream;
import org.codehaus.plexus.util.io.CachingWriter;
import org.objectweb.asm.*;
import org.sonatype.plexus.build.incremental.BuildContext;

import static org.objectweb.asm.Opcodes.*;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;

/**
* <p>
* Generate a plugin descriptor.
Expand Down Expand Up @@ -78,6 +90,12 @@ public class DescriptorGeneratorMojo extends AbstractGeneratorMojo {
@Parameter(defaultValue = "${project.build.outputDirectory}/META-INF/maven", readonly = true)
private File outputDirectory;

/**
* The directory where the generated class files will be put.
*/
@Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true)
private File classesOutputDirectory;

/**
* The file encoding of the source files.
*
Expand Down Expand Up @@ -269,7 +287,6 @@ public class DescriptorGeneratorMojo extends AbstractGeneratorMojo {
protected BuildContext buildContext;

public void generate() throws MojoExecutionException {

if (!"maven-plugin".equalsIgnoreCase(project.getArtifactId())
&& project.getArtifactId().toLowerCase().startsWith("maven-")
&& project.getArtifactId().toLowerCase().endsWith("-plugin")
Expand Down Expand Up @@ -352,6 +369,12 @@ public void generate() throws MojoExecutionException {
PluginDescriptorFilesGenerator pluginDescriptorGenerator = new PluginDescriptorFilesGenerator();
pluginDescriptorGenerator.execute(outputDirectory, request);

// Generate the additional factories for v4 mojos
generateFactories(request.getPluginDescriptor());

// Generate index for v4 beans
generateIndex();

buildContext.refresh(outputDirectory);
} catch (GeneratorException e) {
throw new MojoExecutionException("Error writing plugin descriptor", e);
Expand All @@ -367,6 +390,128 @@ public void generate() throws MojoExecutionException {
}
}

private void generateIndex() throws GeneratorException {
try {
Set<String> diBeans = new TreeSet<>();
try (Stream<Path> paths = Files.walk(classesOutputDirectory.toPath())) {
List<Path> 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('/', '.'));
}
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);
// }
}
}
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);
}
}
} catch (Exception e) {
throw new GeneratorException("Unable to generate index for v4 beans", e);
}
}

private void generateFactories(PluginDescriptor pd) throws GeneratorException {
try {
for (MojoDescriptor md : pd.getMojos()) {
if (md instanceof ExtendedMojoDescriptor && ((ExtendedMojoDescriptor) md).isV4Api()) {
generateFactory(md);
}
}
} catch (IOException e) {
throw new GeneratorException("Unable to generate factories for v4 mojos", e);
}
}

private void generateFactory(MojoDescriptor md) throws IOException {
String mojoClassName = md.getImplementation();
String packageName = mojoClassName.substring(0, mojoClassName.lastIndexOf('.'));
String generatorClassName = mojoClassName.substring(mojoClassName.lastIndexOf('.') + 1) + "Factory";
String mojoName = md.getId();

getLog().debug("Generating v4 factory for " + mojoClassName);

byte[] bin = computeGeneratorClassBytes(packageName, generatorClassName, mojoName, mojoClassName);

try (OutputStream os = new CachingOutputStream(classesOutputDirectory
.toPath()
.resolve(packageName.replace('.', '/') + "/" + generatorClassName + ".class"))) {
os.write(bin);
}
}

static byte[] computeGeneratorClassBytes(
String packageName, String generatorClassName, String mojoName, String mojoClassName) {
String mojo = mojoClassName.replace('.', '/');
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cw.visitSource(generatorClassName + ".java", null);
AnnotationVisitor av = cw.visitAnnotation("Lorg/apache/maven/api/di/Named;", true);
av.visit("value", mojoName);
av.visitEnd();
cw.visitAnnotation("Lorg/apache/maven/api/annotations/Generated;", true).visitEnd();
cw.visit(
V1_8,
ACC_PUBLIC + ACC_SUPER + ACC_SYNTHETIC,
packageName.replace(".", "/") + "/" + generatorClassName,
null,
mojo,
null);
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC + ACC_SYNTHETIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, mojo, "<init>", "()V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
cw.visitEnd();
return cw.toByteArray();
}

private PluginDescriptor extendPluginDescriptor(PluginToolsRequest request) {
ExtendedPluginDescriptor extendedPluginDescriptor = new ExtendedPluginDescriptor(request.getPluginDescriptor());
extendedPluginDescriptor.setRequiredJavaVersion(getRequiredJavaVersion(request));
Expand Down
Loading