Search code examples
mavenmaven-pluginmaven-assembly-plugin

How do you resolve dependencies across all modules in a maven plugin?


I'm writing a Maven plugin that gets the resolved dependencies. It works fine for a single module project/pom, but fails on multiple module projects.

Here's a code snippet

@Mojo(
  name="scan",
  aggregator = true,
  defaultPhase = LifecyclePhase.COMPILE,
  threadSafe = true,
  requiresDependencyCollection = ResolutionScope.TEST,
  requiresDependencyResolution = ResolutionScope.TEST,
  requiresOnline = true
)
public class MyMojo extends AbstractMojo {

  @Parameter(property = "project", required = true, readonly = true)
  private MavenProject project;

  @Parameter(property = "reactorProjects", required = true, readonly = true)
  private List<MavenProject> reactorProjects;


  @Override
  public void execute() throws MojoExecutionException {
    for(MavenProject p : reactorProjects) {
      for(Artifact a : p.getArtifacts()) {
         ...consolidate artifacts
      }
    }
  }
}

The above code will consolidate all the resolved dependencies across all the modules, but it includes some additional ones.

Here's a sample project to work with. Please download this github repo

From the modules-project main folder, please run

mvn dependency:tree -Dverbose -Dincludes=commons-logging

You should see an output like this

[INFO] ------------------------------------------------------------------------
[INFO] Building core 0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ core ---
[INFO] com.github:core:jar:0.1-SNAPSHOT
[INFO] \- axis:axis:jar:1.4:compile
[INFO]    +- commons-logging:commons-logging:jar:1.0.4:runtime
[INFO]    \- commons-discovery:commons-discovery:jar:0.2:runtime
[INFO]       \- (commons-logging:commons-logging:jar:1.0.3:runtime - omitted for conflict with 1.0.4)
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building web 0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ web ---
[INFO] com.github:web:war:0.1-SNAPSHOT
[INFO] +- commons-logging:commons-logging:jar:1.1.1:compile
[INFO] \- com.github:core:jar:0.1-SNAPSHOT:compile
[INFO]    \- axis:axis:jar:1.4:compile
[INFO]       +- (commons-logging:commons-logging:jar:1.0.4:runtime - omitted for conflict with 1.1.1)
[INFO]       \- commons-discovery:commons-discovery:jar:0.2:runtime
[INFO]          \- (commons-logging:commons-logging:jar:1.0.3:runtime - omitted for conflict with 1.1.1)
[INFO] ------------------------------------------------------------------------

Notice that the module/project core depends on commons-logging 1.0.4 and commons-logging 1.0.3, but 1.0.3 is omitted due to a conflict and 1.0.4 is resolved. This means that if you were to build core on its own, you should only get commons-logging 1.0.4.

Notice that module/project web depends on conflicting versions of commons-logging as well but resolves to 1.1.1.

Now if you were to build the "entire project" (modules-project) with the "mvn package" command, you should see that modules-project/web/target/myweb/WEB-INF/lib contains all the resolved dependencies and it includes ONLY commons-logging 1.1.1.

Here's the problem with the code

In the above code, reactorProjects is instantiated with 3 MavenProject's: modules-project, core, and web.

For modules-project and web, it resolves and returns commons-logging 1.1.1. However, for the core project, it resolves and returns commons-logging 1.0.4.

I want my plugin code to know that commons-logging 1.1.1 is the dependency that the build will produce, and not commons-logging 1.0.4

Any thoughts?


Solution

  • You practically have all it takes in your question. The following plugin will print in the console output the artifacts of the WAR project in the reactor:

    @Mojo(name = "foo", aggregator = true, requiresDependencyResolution = ResolutionScope.TEST)
    public class MyMojo extends AbstractMojo {
    
        @Parameter(defaultValue = "${project}", readonly = true, required = true)
        private MavenProject project;
    
        @Parameter(defaultValue = "${session}", readonly = true, required = true)
        private MavenSession session;
    
        @Parameter(property = "reactorProjects", required = true, readonly = true)
        private List<MavenProject> reactorProjects;
    
        public void execute() throws MojoExecutionException, MojoFailureException {
            MavenProject packagedProject = getWarProject(reactorProjects);
            for (Artifact artifact : packagedProject.getArtifacts()) {
                getLog().info(artifact.toString());
            }
        }
    
        private MavenProject getWarProject(List<MavenProject> list) throws MojoExecutionException {
            for (MavenProject project : list) {
                if ("war".equals(project.getPackaging())) {
                    return project;
                }
            }
            throw new MojoExecutionException("No WAR project found in the reactor");
        }
    
    }
    

    What this does is that it acquires all the projects in the reactor with the injected parameter reactorProjects. Then, it loops to find which one of those is the "war" by comparing their packaging. When it is found, getArtifacts() will return all the resolved artifacts for that project.

    The magic that makes it work is the aggregator = true in the MOJO definition:

    Flags this Mojo to run it in a multi module way, i.e. aggregate the build with the set of projects listed as modules.

    When added to core POM

    <plugin>
      <groupId>sample.plugin</groupId>
      <artifactId>test-maven-plugin</artifactId>
      <version>1.0.0</version>
      <executions>
        <execution>
          <id>test</id>
          <phase>compile</phase>
          <goals>
            <goal>foo</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
    

    and run with your example project, this prints in the console:

    [INFO] commons-logging:commons-logging:jar:1.1.1:compile
    [INFO] com.github:core:jar:0.1-SNAPSHOT:compile
    [INFO] axis:axis:jar:1.4:compile
    [INFO] org.apache.axis:axis-jaxrpc:jar:1.4:compile
    [INFO] org.apache.axis:axis-saaj:jar:1.4:compile
    [INFO] axis:axis-wsdl4j:jar:1.5.1:runtime
    [INFO] commons-discovery:commons-discovery:jar:0.2:runtime
    

    This is good enough. With that, we can go forward and, for example, compare the resolved artifacts by the current project being build and the packaged project. If we add a method

    private void printConflictingArtifacts(Set<Artifact> packaged, Set<Artifact> current) {
        for (Artifact a1 : current) {
            for (Artifact a2 : packaged) {
                if (a1.getGroupId().equals(a2.getGroupId()) && 
                        a1.getArtifactId().equals(a2.getArtifactId()) &&
                        !a1.getVersion().equals(a2.getVersion())) {
                    getLog().warn("Conflicting dependency: " + a2 + " will be packaged and found " + a1);
                }
            }
        }
    }
    

    called with

    printConflictingArtifacts(packagedProject.getArtifacts(), project.getArtifacts());
    

    that compares the current artifacts with the artifacts of the packaged project, and only retain those with the same group/artifact id but different version, we can get in the console output with your example:

    [WARNING] Conflicting dependency: commons-logging:commons-logging:jar:1.1.1:compile will be packaged and found commons-logging:commons-logging:jar:1.0.4:runtime
    

    The above assumed that our final packaging module was a WAR module. We could make that more generic and let the user specify which one of the module is the target module (i.e. that will package the real delivery).

    For that, we can add a parameter to our MOJO

    @Parameter(property = "packagingArtifact")
    private String packagingArtifact;
    

    This parameter will be of the form groupId:artifactId and will represent the coordinates of the target module. We can then add a method getPackagingProject whose goal will be to return the MavenProject associated with those coordinates.

    The configuration of the plugin inside core would be

    <plugin>
        <groupId>sample.plugin</groupId>
        <artifactId>test-maven-plugin</artifactId>
        <version>1.0.0</version>
        <executions>
            <execution>
                <id>test</id>
                <phase>compile</phase>
                <goals>
                    <goal>foo</goal>
                </goals>
                <configuration>
                    <packagingArtifact>com.github:web</packagingArtifact>
                </configuration>
            </execution>
        </executions>
    </plugin>
    

    And the full MOJO would be:

    @Mojo(name = "foo", aggregator = true, requiresDependencyResolution = ResolutionScope.TEST, defaultPhase = LifecyclePhase.COMPILE)
    public class MyMojo extends AbstractMojo {
    
        @Parameter(defaultValue = "${project}", readonly = true, required = true)
        private MavenProject project;
    
        @Parameter(defaultValue = "${session}", readonly = true, required = true)
        private MavenSession session;
    
        @Parameter(property = "reactorProjects", required = true, readonly = true)
        private List<MavenProject> reactorProjects;
    
        @Parameter(property = "packagingArtifact")
        private String packagingArtifact;
    
        public void execute() throws MojoExecutionException, MojoFailureException {
            MavenProject packagedProject = getPackagingProject(reactorProjects, packagingArtifact);
            printConflictingArtifacts(packagedProject.getArtifacts(), project.getArtifacts());
        }
    
        private void printConflictingArtifacts(Set<Artifact> packaged, Set<Artifact> current) {
            for (Artifact a1 : current) {
                for (Artifact a2 : packaged) {
                    if (a1.getGroupId().equals(a2.getGroupId()) && a1.getArtifactId().equals(a2.getArtifactId())
                            && !a1.getVersion().equals(a2.getVersion())) {
                        getLog().warn("Conflicting dependency: " + a2 + " will be packaged and found " + a1);
                    }
                }
            }
        }
    
        private MavenProject getPackagingProject(List<MavenProject> list, String artifact) throws MojoExecutionException {
            if (artifact == null) {
                return getWarProject(list);
            }
            String[] tokens = artifact.split(":");
            for (MavenProject project : list) {
                if (project.getGroupId().equals(tokens[0]) && project.getArtifactId().equals(tokens[1])) {
                    return project;
                }
            }
            throw new MojoExecutionException("No " + artifact + " project found in the reactor");
        }
    
        private MavenProject getWarProject(List<MavenProject> list) throws MojoExecutionException {
            for (MavenProject project : list) {
                if ("war".equals(project.getPackaging())) {
                    return project;
                }
            }
            throw new MojoExecutionException("No WAR project found in the reactor");
        }
    
    }
    

    This implements the idea of above: when the user has given a target module, we use it as reference. When this parameter is not present, we default to finding a WAR in the reactor.