Home > Software engineering >  Executing individual maven plugin goals in multi-module project
Executing individual maven plugin goals in multi-module project

Time:08-08

I'm trying to overcome well-known maven issue, described in various SO questions, for example:

before now I was familiar with following workarounds:

  • mvn install - that is exactly what I would like to avoid
  • extremely comprehensive project configuration involving <skip>/<properties>/<profiles>
  • copy dependencies using maven-dependency-plugin into module folder whilst packaging and setup classpath

and all of those workarounds looks very poor from my perspective.

Today I have read about root-reactor aware subfolder builds in maven-4, however maven-4 is not yet released and I'm interested to get a solution for maven-3. I have performed some research and have found a couple useful extension points in maven-3:

DefaultArtifactResolver.java:

if ( workspace != null )
{
    File file = workspace.findArtifact( artifact );
    if ( file != null )
    {
        artifact = artifact.setFile( file );
        result.setArtifact( artifact );
        result.setRepository( workspace.getRepository() );
        artifactResolved( session, trace, artifact, result.getRepository(), null );
        continue;
    }
}

DefaultProjectDependenciesResolver.java

for ( RepositorySessionDecorator decorator : decorators )
{
    RepositorySystemSession decorated = decorator.decorate( project, session );
    if ( decorated != null )
    {
        session = decorated;
    }
}

and finally I have implemented a very simple maven extension (full source code on github):

@Component(role = RepositorySessionDecorator.class)
public class FakeRepositorySessionDecorator implements RepositorySessionDecorator {

    @Requirement
    protected ArtifactHandlerManager artifactHandlerManager;

    @Override
    public RepositorySystemSession decorate(MavenProject project, RepositorySystemSession session) {
        String enabled = session.getUserProperties().get("fakerepo");
        if (!"true".equalsIgnoreCase(enabled)) {
            return null;
        }
        MavenProject root = project;
        while (root != null && !root.isExecutionRoot()) {
            root = root.getParent();
        }
        if (root != null) {
            WorkspaceReader workspaceReader = session.getWorkspaceReader();
            workspaceReader = new FakeWorkspaceReader(workspaceReader, root, artifactHandlerManager);
            return new DefaultRepositorySystemSession(session)
                    .setWorkspaceReader(workspaceReader);
        }
        return null;
    }

}

The idea is if developer specifies -Dfakeroot when executing maven plugin goal my extension expands workspace scope from single module to the project root and when requested new expanded workspace tries to find packaged artifact among submodule folders, thus the sequence of commands like:

mvn clean package
mvn exec:exec -pl submodule -Dfakeroot

leads developer to the expected result.

The question is: what I may brake if I remove requirement to specify -Dfakerepo and enable the behaviour described above by default (i.e. apply new behaviour for all maven goals and lifecycle phases)? From my perspective it is always more reasonable to lookup packaged artifacts among submodule folders rather than in local repository. Or am I missing something?


UPD.

I have found a following hypothetical scenario when my extension may work not like "expected":

  • let there are two submodules A and B in multi-module project, and B depends on A
  • developer have modified at least A and issues something like mvn -am test -pl B

in that case if A was packaged previously my extension forces maven to use stale artifact, however default implementation would use A/target/classes as classpath entry, on the other hand A/target/classes may contain stale classes (we are not issuing clean), thus the behaviour of "default implementation" is also far from ideal in that case.


UPD2.

It seems that it is worth to explain why I that issue is bothering me. Actually, there are a couple of "typical" scenarios:

  1. developers would like to maintain their own infrastructure (in particular that is primarily a DB), i.e.: start and stop multiple instances, perform DB migrations, debug, etc - hereby we would like to avoid CI issues like "something went wrong in CI pipeline - guess what". And the goal is to make it as simple as possible, for example we have a special exec goal in dev submodule, which performs DB migrations:

<dependencies>

    <dependency>
        <groupId>tld.project</groupId>
        <artifactId>another-submodule</artifactId>
    </dependency>
    
</dependencies>

<execution>
    <id>liquibase-update-primary</id>
    <phase>install</phase>
    <goals>
        <goal>exec</goal>
    </goals>
    <configuration>
        <executable>java</executable>
        <arguments>
            <argument>-classpath</argument>
            <!-- expecting to get module dependencies there -->
            <classpath/>
            <!-- main class -->
            <argument>liquibase.integration.commandline.Main</argument>
            <!-- covered by project properties -->
            <argument>--changeLogFile=${primary.changeLogFile}</argument>
            <!-- covered by profile properties -->
            <argument>--url=${jdbc.url}</argument>
            <argument>--driver=${jdbc.driver}</argument>
            <argument>--username=${jdbc.username}</argument>
            <argument>--password=${jdbc.password}</argument>
            <argument>--logLevel=info</argument>
            <argument>update</argument>
        </arguments>
    </configuration>
</execution>

and that obviously does not work in maven-3, because it expects to find tld.project-another-submodule artifact in local repository, however it is possible to perform the following trick with maven-dependency-plugin:

<execution>
    <id>liquibase-dependencies</id>
    <phase>package</phase>
    <goals>
        <goal>copy</goal>
    </goals>
    <configuration>
        <artifactItems>
            <artifactItem>
                <!-- 
                    now we may tell liquibase to load extra jars
                    from  ${project.build.directory}/liquibase
                -->
                <groupId>tld.project</groupId>
                <artifactId>another-submodule</artifactId>
                <type>jar</type>
                <destFileName>another-submodule.jar</destFileName>
                <outputDirectory>${project.build.directory}/liquibase</outputDirectory>
            </artifactItem>
        </artifactItems>
    </configuration>
</execution>
  1. We would like to run integration tests individually without recompiling/packaging the entire project i.e. issuing something like mvn verify -pl it-submodule, that is both useful from developer and CI perspective:

    • Developers and DevOps may perform infrastructure-related steps somewhere between package and verify phases
    • CI may run verify multiple times (yep, someone may think about how is it possible to reiterate failed tests in CI pipeline, however our goal is to run verify phase multiple times in a row to make sure there are no flapping tests)
  2. In case of large projects every extra lifecycle step takes a lot of time

CodePudding user response:

Well,

I have taken a look on how that implemented in maven-4 and got a conclusion that it won't work as expected:

  1. maven team have expanded the scope of reactor workspace, now the scope of reactor workspace is an entire project, regardless whether -f or -pl was specified - exactly the same what I'm doing.
  2. maven team have added some heuristics to determine whether packaged artifact is stale or not (simply comparing modification dates of packaged artifact and classes in target/classes, nothing related to sources though) - that is actually the answer to my Q.
  3. there is an issue which make "root-reactor aware subfolder builds" feature completely useless and dangerous: classifiers are not supported - maven-4 tries to pick up main artifact regardless what classifier was specified, in case of failure it falls back to ~/.m2/repository
  4. if maven fails to discover main artifact it tries either target/classes or target/test-classes - in the most cases that leads to the exception "Artifact has not been packaged yet. When used on reactor artifact, copy should be executed after packaging: see MDEP-187"
  • Related