Open HannesWell opened 2 years ago
The specification how a Mojo should be treated by M2E (ignored, executed, ...) could also be done via a corresponding annotation in the mojo-class. Either lifecycle-mapping-metadata generator plugin could process them and generate a corresponding xml file or they could even be retained at runtim so that m2e can inspect the class for such annotation, which would make the xml file generation obsolete, as well as a 'lifecycle-mapping-metadata generator plugin'.
For my work on https://github.com/eclipse/tycho/issues/945 I created the following very simple LifecycleMappingGenerator
application.
It is not very sophisticated (it can only apply the same action for all mojos) and contains some very hacky parts (String-manipluation to resolve constant-expressions, constant paths) but at least it worked for the task mentioned above.
package org.eclipse.m2e.core.internal.lifecyclemapping.generator;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.eclipse.equinox.app.IApplication;
import org.eclipse.equinox.app.IApplicationContext;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.eclipse.m2e.core.internal.lifecyclemapping.model.LifecycleMappingMetadataSource;
import org.eclipse.m2e.core.internal.lifecyclemapping.model.PluginExecutionFilter;
import org.eclipse.m2e.core.internal.lifecyclemapping.model.PluginExecutionMetadata;
import org.eclipse.m2e.core.internal.lifecyclemapping.model.io.xpp3.LifecycleMappingMetadataSourceXpp3Writer;
import org.eclipse.m2e.core.lifecyclemapping.model.PluginExecutionAction;
public class LifecycleMappingGenerator implements IApplication {
private static final Path LIFECYCLE_MAPPING_METADATA_XML = Path.of("src", "main", "resources", "META-INF", "m2e",
"lifecycle-mapping-metadata.xml");
@Override
public Object start(IApplicationContext context) throws Exception {
Path root = Path.of("C:\\dev\\git\\org.eclipse.tycho");
PluginExecutionAction action = PluginExecutionAction.ignore;
long start = System.currentTimeMillis();
try (var walk = filesWithMatchingName(root, "pom.xml"::equals)) {
List<Path> poms = walk.toList();
for(Path pomXML : poms) {
Path projectRoot = pomXML.getParent();
Path javaMain = projectRoot.resolve(Path.of("src", "main", "java"));
try (Stream<Path> javaFiles = filesWithMatchingName(javaMain, n -> n.endsWith(".java"))) {
List<String> mojos = javaFiles.flatMap(f -> extractMojoName(f).stream()).sorted().toList();
if(!mojos.isEmpty()) {
System.out.println("Found mojos for: " + projectRoot);
mojos.forEach(m -> System.out.println(" " + m));
LifecycleMappingMetadataSource source = new LifecycleMappingMetadataSource();
source.addPluginExecution(createPluginExecution(action, mojos));
Path metadataFile = projectRoot.resolve(LIFECYCLE_MAPPING_METADATA_XML);
Files.createDirectories(metadataFile.getParent());
try (var out = Files.newOutputStream(metadataFile)) {
LifecycleMappingMetadataSourceXpp3Writer writer = new LifecycleMappingMetadataSourceXpp3Writer();
writer.write(out, source);
}
}
}
}
}
System.out.println("Completed after " + (System.currentTimeMillis() - start) + "ms");
return EXIT_OK;
}
@Override
public void stop() {
}
private static final String MOJO_START = "@Mojo(";
private static Optional<String> extractMojoName(Path file) {
List<String> mojos;
try (var lines = Files.lines(file)) {
mojos = lines.filter(l -> l.strip().startsWith(MOJO_START)).map(String::strip).toList();
} catch(IOException e) {
throw new IllegalStateException("Faild to look up mojo annotation", e);
}
if(mojos.isEmpty()) {
return Optional.empty();
} else if(mojos.size() == 1) {
return getMojoAttributeValue(mojos.get(0), "name", file);
} else {
throw new IllegalStateException("Multiple @Mojo annotation found");
}
}
private static Optional<String> getMojoAttributeValue(String mojoAnnotation, String attributeName, Path file) {
mojoAnnotation = mojoAnnotation.strip();
String arguments = mojoAnnotation.substring(MOJO_START.length(), mojoAnnotation.length() - 1);
return Arrays.stream(arguments.split(",")).map(e -> {
String[] keyValue = e.split("=");
if(keyValue.length != 2) {
throw new IllegalArgumentException("Invalid parameter: " + e);
}
return attributeName.equals(keyValue[0].strip()) ? keyValue[1].strip() : null;
}).filter(Objects::nonNull).flatMap(v -> extractLiteralAttributeValue(v, file)).findFirst();
}
private static Stream<String> extractLiteralAttributeValue(String value, Path file) {
if(value.startsWith("\"") && value.endsWith("\"")) { // value is a literal string
return Stream.of(value.substring(1, value.length() - 1));
}
String filename = file.getFileName().toString();
assert filename.endsWith(".java");
String className = filename.substring(0, filename.length() - ".java".length());
if(value.startsWith(className + ".")) { // value refers to a static final field of the class
String fieldName = value.substring(className.length() + ".".length());
try (var lines = Files.lines(file)) {
Pattern stringWithLiteralValue = Pattern
.compile("(( *static *)|( *final *))+String +" + fieldName + " *= *\"(?<fieldName>[^\"]+)\";");
Optional<String> constExpression = lines.map(line -> {
Matcher matcher = stringWithLiteralValue.matcher(line);
return matcher.find() ? matcher.group("fieldName") : null;
}).filter(Objects::nonNull).findFirst();
if(constExpression.isPresent()) {
return constExpression.stream();
}
} catch(IOException e) {
throw new IllegalStateException("Failed to read constant expression field", e);
}
}
throw new UnsupportedOperationException("Unable to extract literal value of ");
}
private static Stream<Path> filesWithMatchingName(Path root, Predicate<String> filenameFilter) throws IOException {
if(Files.isDirectory(root)) {
return Files.walk(root).filter(p -> filenameFilter.test(p.getFileName().toString())).filter(Files::isRegularFile);
}
return Stream.empty();
}
private static PluginExecutionMetadata createPluginExecution(PluginExecutionAction action, List<String> mojos) {
PluginExecutionMetadata exe = new PluginExecutionMetadata();
PluginExecutionFilter filter = new PluginExecutionFilter();
filter.getGoals().addAll(mojos);
exe.setFilter(filter);
setAction(exe, action);
return exe;
}
private static void setAction(PluginExecutionMetadata exe, PluginExecutionAction action) {
Xpp3Dom dom = new Xpp3Dom("action");
dom.addChild(new Xpp3Dom(action.name()));
exe.setActionDom(dom);
}
}
If you like to do this a bit less "hacky" all maven plugins contain a META-INF/maven/plugin.xml
(generated during the maven build) that contains all mojo with ther corresponding data.
If you like to do this a bit less "hacky" all maven plugins contain a
META-INF/maven/plugin.xml
(generated during the maven build) that contains all mojo with ther corresponding data.
Thank for that hint. Yes if this becomes reality I already thought that the information about the available goals has to be retrieved from a more reliable source, just like that. This also makes it simpler.
In general, what do you think about the suggested alternatives to use annotations (that are maybe even retained at runtime)? If annotations are provided, they probably have to go into a dedicated Maven-artifact that becomes a dependency for the referencing Plug-in (while the generator plug-in, would be a 'build-plugin') and if the should be available at runtime they obviously also have to be available at runtime. But I think for M2E this is not a problem because this can be the same (OSGi compliant) jar. But I wonder why other frameworks that do similar things (for example OSGi DS), choose to have annotations that are not retained at runtime but from which a metadata file is generated during build time? I could imagine that this was for historical reasons (IIRC earlier one could craft the service component xml manually) and for performance reasons because only checking and reading one file is likely faster than scanning all class files for a corresponding annotation. But the latter is probably different for m2e, because due to the nature of the problem we already know exactly the class to check.
Besides that I could imagine that some Plug-in developers don't want to 'pollute' their plug-in code with IDE specific annotations and only want to specify something like the lifecycle-mapping 'externally' in the pom.
Annotations are great, but I think we still need the XML because loading the XML is easy but loading the class itself might becomes problematic. also if we choose to retain them in the runtime, we enforce plugins to keep a runtime dependency on the annotations (just keep in mind that Maven plugins are not only executed inside m2e). So I think the best would be to have class or source retention policy. Given that coding all that stuff is quite heavy, it might be a better alternative to write a maven-plugin instead that maybe hooks at the generation of the plugin.xml
. Given that m2e executes this plugin then, it would auto generate the data as well without any need to provide a special m2e plugin.
About DS: Just keep in mind that loading a resource is always possible, while loading a class potentially would activate the bundle if it has lazy
policy.
OK, yes that makes sense. Then we should stick with the XML. Just for clarification, my actual intention is to write a Maven-Plugin for that. The posted OSGi application was just a workaround/reference for my work in M2E. Most of it can probably done better. So yes the plan is to generate that during build and to obtain as much information possible from Maven directly instead of coding that myself. Of course this plug-in then should nicely interact with M2E, when executed as part of the Workspace build. :D
Isn't a simple annotation processor enough? I don't think it needs a full-blown Maven plugin for that. But in general a great proposal.
Isn't a simple annotation processor enough?
Are you reffering to something like this?
I have not yet done something like this, but ist sounds very promissing. Is it correct that we then can implement the processor in the same Maven artifact like the annotations and everything is then Just picked up automagically for a consumer?
The only blocker for this is that we are not yet publishing M2E to Maven-Central.
Is there already an annotation model existing for lifecycle mapping if we want to implement an annotation processor for those?
Not that I now. In fact IT IS part of this proposal to create one.
For Plug-in developers that want to make their Maven plugins 'M2E-ready' it would be convenient to provide a Maven Plugin to generate a
META-INF/m2e/lifecycle-mapping-metadata.xml
automatically during the build for all Mojos of the plug-inThis plugin should
META-INF/m2e/lifecycle-mapping-metadata.xml
automatically with proper contentI think the most 'difficult' part at the moment is that M2E does not yet publish to Maven-Central. We only publish that plugin, but I think it might be beneficial for m2e in general to be published to Maven-Central. With the work being done for Eclipse-Platform that task should become simpler.