Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide Maven-Plugin to generate M2E lifecycle-mapping-metadata #830

Open
HannesWell opened this issue Jul 2, 2022 · 10 comments
Open

Provide Maven-Plugin to generate M2E lifecycle-mapping-metadata #830

HannesWell opened this issue Jul 2, 2022 · 10 comments

Comments

@HannesWell
Copy link
Contributor

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-in

This plugin should

  • Scan the Maven-plugin being build for Mojos and ensure that for each mojo a mapping is specified, warn/error if not
  • Provide a convenient way to define each the action for each mojo executed in m2e
    • e.g. in the mojo's configuration section where one could define a default action for all mojos and/or a mapping from goal to action
  • Generate the META-INF/m2e/lifecycle-mapping-metadata.xml automatically with proper content
  • If possible check if the Mojos use the plexus-BuildContext ? (maybe check if at least the dependency is present.

I 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.

@HannesWell
Copy link
Contributor Author

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'.

@HannesWell
Copy link
Contributor Author

For my work on eclipse-tycho/tycho#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);
  }
}

@laeubi
Copy link
Member

laeubi commented Jul 3, 2022

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.

@HannesWell
Copy link
Contributor Author

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.

@laeubi
Copy link
Member

laeubi commented Jul 3, 2022

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.

@HannesWell
Copy link
Contributor Author

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

@kwin
Copy link
Member

kwin commented Aug 18, 2022

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.

@HannesWell
Copy link
Contributor Author

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.

@mickaelistria
Copy link
Contributor

Is there already an annotation model existing for lifecycle mapping if we want to implement an annotation processor for those?

@HannesWell
Copy link
Contributor Author

HannesWell commented Aug 22, 2022

Not that I now. In fact IT IS part of this proposal to create one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants