Issue
I'm trying to overcome well-known maven issue, described in various SO questions, for example:
- Maven exec:java goal on a multi-module project
- href="https://stackoverflow.com/questions/57187588/spring-boot-multi-module-maven-project-repackage-failed">Spring Boot Multi-Module maven project repackage failed
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 setupclasspath
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
:
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
andB
in multi-module project, andB
depends onA
- developer have modified at least
A
and issues something likemvn -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:
- 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 indev
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>
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
andverify
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 runverify
phase multiple times in a row to make sure there are no flapping tests)
- Developers and DevOps may perform infrastructure-related steps somewhere between
In case of large projects every extra lifecycle step takes a lot of time
Solution
Well,
I have taken a look on how that implemented in maven-4
and got a conclusion that it won't work as expected:
- 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. - 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. - 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
- filed MNG-7527 - if maven fails to discover main artifact it tries either
target/classes
ortarget/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"
Answered By - Andrey B. Panfilov
Answer Checked By - Candace Johnson (JavaFixing Volunteer)