Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -100,6 +101,7 @@ public void execute(
List<MojoExecution> mojoExecutions, MavenSession session, MojoExecutionRunner mojoExecutionRunner)
throws LifecycleExecutionException {

Map<String, MojoExecutionEvent> validationTimeEvents = null;
try {
final MavenProject project = session.getCurrentProject();
final Source source = getSource(mojoExecutions);
Expand Down Expand Up @@ -134,6 +136,16 @@ public void execute(
}
if (cacheState == INITIALIZED) {
result = cacheController.findCachedBuild(session, project, mojoExecutions, skipCache);

// Capture validation-time properties for all mojos to ensure consistent property reading
// at the same lifecycle point for all builds (eliminates Maven 4 injection timing issues)
// Always capture when cacheState is INITIALIZED since we may need to save
validationTimeEvents = captureValidationTimeProperties(session, project, mojoExecutions);
result = CacheResult.rebuilded(result, validationTimeEvents);
LOGGER.debug(
"Captured validation-time properties for {} mojos in project {}",
validationTimeEvents.size(),
projectName);
}
} else {
LOGGER.info("Cache is disabled on project level for {}", projectName);
Expand Down Expand Up @@ -183,9 +195,17 @@ public void execute(
.isEmpty()) {
LOGGER.debug("Cache storing is skipped since there was no \"clean\" phase.");
} else {
final Map<String, MojoExecutionEvent> executionEvents =
mojoListener.getProjectExecutions(project);
cacheController.save(result, mojoExecutions, executionEvents);
// Only save cache if there are validation-time events to store
// When running only clean phase, there are no cacheable mojos
validateValidationTimeEvents(projectName, mojoExecutions, result.getValidationTimeEvents());
if (result.getValidationTimeEvents() == null
|| result.getValidationTimeEvents().isEmpty()) {
LOGGER.debug("Skipping cache storage for {} - no cacheable mojos executed", projectName);
} else {
LOGGER.debug(
"Using validation-time properties for cache storage (consistent lifecycle point)");
cacheController.save(result, mojoExecutions, result.getValidationTimeEvents());
}
}
}
} finally {
Expand All @@ -204,6 +224,8 @@ public void execute(
}
} catch (MojoExecutionException e) {
throw new LifecycleExecutionException(e.getMessage(), e);
} finally {
releaseValidationTimeMojos(validationTimeEvents);
}
}

Expand Down Expand Up @@ -444,6 +466,89 @@ boolean isParamsMatched(
return true;
}

/**
* Captures plugin properties at validation time for all mojo executions.
* This ensures properties are read at the same lifecycle point for all builds,
* eliminating timing mismatches caused by Maven 4's auto-injection of properties
* like --module-version during execution.
*
* @param session Maven session
* @param project Current project
* @param mojoExecutions List of mojo executions to capture properties for
* @return Map of execution key to MojoExecutionEvent captured at validation time
*/
private Map<String, MojoExecutionEvent> captureValidationTimeProperties(
MavenSession session, MavenProject project, List<MojoExecution> mojoExecutions)
throws LifecycleExecutionException {
Map<String, MojoExecutionEvent> validationTimeEvents = new HashMap<>();

try {
for (MojoExecution mojoExecution : mojoExecutions) {
// Skip mojos that don't execute or are in clean phase
if (mojoExecution.getLifecyclePhase() == null
|| !lifecyclePhasesHelper.isLaterPhaseThanClean(mojoExecution.getLifecyclePhase())) {
continue;
}

mojoExecutionScope.enter();
try {
mojoExecutionScope.seed(MavenProject.class, project);
mojoExecutionScope.seed(MojoExecution.class, mojoExecution);

Mojo mojo = mavenPluginManager.getConfiguredMojo(Mojo.class, session, mojoExecution);
MojoExecutionEvent event = new MojoExecutionEvent(session, project, mojoExecution, mojo);
validationTimeEvents.put(mojoExecutionKey(mojoExecution), event);

LOGGER.debug(
"Captured validation-time properties for {}",
mojoExecution.getMojoDescriptor().getFullGoalName());
} catch (PluginConfigurationException | PluginContainerException e) {
throw new LifecycleExecutionException(
"Cannot capture validation-time properties for "
+ mojoExecution.getMojoDescriptor().getFullGoalName(),
e);
} finally {
try {
mojoExecutionScope.exit();
} catch (MojoExecutionException e) {
LOGGER.debug("Error exiting mojo execution scope: {}", e.getMessage());
}
}
}
} catch (LifecycleExecutionException e) {
releaseValidationTimeMojos(validationTimeEvents);
throw e;
}

LOGGER.debug("Captured validation-time properties for {} mojos", validationTimeEvents.size());
return validationTimeEvents;
}

private void releaseValidationTimeMojos(Map<String, MojoExecutionEvent> validationTimeEvents) {
if (validationTimeEvents != null) {
for (MojoExecutionEvent event : validationTimeEvents.values()) {
mavenPluginManager.releaseMojo(event.getMojo(), event.getExecution());
}
}
}

private void validateValidationTimeEvents(
String projectName,
List<MojoExecution> mojoExecutions,
Map<String, MojoExecutionEvent> validationTimeEvents) {
for (MojoExecution mojoExecution : mojoExecutions) {
if (mojoExecution.getLifecyclePhase() != null
&& lifecyclePhasesHelper.isLaterPhaseThanClean(mojoExecution.getLifecyclePhase())
&& (validationTimeEvents == null
|| !validationTimeEvents.containsKey(mojoExecutionKey(mojoExecution)))) {
throw new AssertionError("Validation-time properties not captured for "
+ mojoExecution.getMojoDescriptor().getFullGoalName()
+ " in project "
+ projectName);
}
}
}

private enum CacheRestorationStatus {
SUCCESS,
FAILURE,
Expand Down
65 changes: 57 additions & 8 deletions src/main/java/org/apache/maven/buildcache/CacheResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
*/
package org.apache.maven.buildcache;

import java.util.Map;

import org.apache.maven.buildcache.xml.Build;
import org.apache.maven.buildcache.xml.CacheSource;
import org.apache.maven.execution.MojoExecutionEvent;

import static java.util.Objects.requireNonNull;

Expand All @@ -31,49 +34,86 @@ public class CacheResult {
private final RestoreStatus status;
private final Build build;
private final CacheContext context;
private final Map<String, MojoExecutionEvent> validationTimeEvents;

private CacheResult(RestoreStatus status, Build build, CacheContext context) {
private CacheResult(
RestoreStatus status,
Build build,
CacheContext context,
Map<String, MojoExecutionEvent> validationTimeEvents) {
Comment thread
cowwoc marked this conversation as resolved.
this.status = requireNonNull(status);
this.build = build;
this.context = context;
this.validationTimeEvents = validationTimeEvents;
}

public static CacheResult empty(CacheContext context) {
requireNonNull(context);
return new CacheResult(RestoreStatus.EMPTY, null, context);
return new CacheResult(RestoreStatus.EMPTY, null, context, null);
}

public static CacheResult empty(CacheContext context, Map<String, MojoExecutionEvent> validationTimeEvents) {
requireNonNull(context);
return new CacheResult(RestoreStatus.EMPTY, null, context, validationTimeEvents);
}

public static CacheResult empty() {
return new CacheResult(RestoreStatus.EMPTY, null, null);
return new CacheResult(RestoreStatus.EMPTY, null, null, null);
}

public static CacheResult failure(Build build, CacheContext context) {
requireNonNull(build);
requireNonNull(context);
return new CacheResult(RestoreStatus.FAILURE, build, context);
return new CacheResult(RestoreStatus.FAILURE, build, context, null);
}

public static CacheResult failure(
Build build, CacheContext context, Map<String, MojoExecutionEvent> validationTimeEvents) {
requireNonNull(build);
requireNonNull(context);
return new CacheResult(RestoreStatus.FAILURE, build, context, validationTimeEvents);
}

public static CacheResult success(Build build, CacheContext context) {
requireNonNull(build);
requireNonNull(context);
return new CacheResult(RestoreStatus.SUCCESS, build, context);
return new CacheResult(RestoreStatus.SUCCESS, build, context, null);
}

public static CacheResult success(
Build build, CacheContext context, Map<String, MojoExecutionEvent> validationTimeEvents) {
requireNonNull(build);
requireNonNull(context);
return new CacheResult(RestoreStatus.SUCCESS, build, context, validationTimeEvents);
}

public static CacheResult partialSuccess(Build build, CacheContext context) {
requireNonNull(build);
requireNonNull(context);
return new CacheResult(RestoreStatus.PARTIAL, build, context);
return new CacheResult(RestoreStatus.PARTIAL, build, context, null);
}

public static CacheResult partialSuccess(
Build build, CacheContext context, Map<String, MojoExecutionEvent> validationTimeEvents) {
requireNonNull(build);
requireNonNull(context);
return new CacheResult(RestoreStatus.PARTIAL, build, context, validationTimeEvents);
}

public static CacheResult failure(CacheContext context) {
requireNonNull(context);
return new CacheResult(RestoreStatus.FAILURE, null, context);
return new CacheResult(RestoreStatus.FAILURE, null, context, null);
}

public static CacheResult failure(CacheContext context, Map<String, MojoExecutionEvent> validationTimeEvents) {
requireNonNull(context);
return new CacheResult(RestoreStatus.FAILURE, null, context, validationTimeEvents);
}

public static CacheResult rebuilt(CacheResult original, Build build) {
requireNonNull(original);
requireNonNull(build);
return new CacheResult(original.status, build, original.context);
return new CacheResult(original.status, build, original.context, original.validationTimeEvents);
}

/**
Expand All @@ -84,6 +124,11 @@ public static CacheResult rebuilded(CacheResult original, Build build) {
return rebuilt(original, build);
}

public static CacheResult rebuilded(CacheResult original, Map<String, MojoExecutionEvent> validationTimeEvents) {
requireNonNull(original);
return new CacheResult(original.status, original.build, original.context, validationTimeEvents);
}

public boolean isSuccess() {
return status == RestoreStatus.SUCCESS;
}
Expand Down Expand Up @@ -111,4 +156,8 @@ public RestoreStatus getStatus() {
public boolean isFinal() {
return build != null && build.getDto().is_final();
}

public Map<String, MojoExecutionEvent> getValidationTimeEvents() {
return validationTimeEvents;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.buildcache.its;

import org.apache.maven.buildcache.its.junit.IntegrationTest;
import org.apache.maven.it.VerificationException;
import org.apache.maven.it.Verifier;
import org.junit.jupiter.api.Test;

/**
* Integration test for JPMS module compilation with explicit moduleVersion configuration.
*
* <p>This test verifies that the validation-time property capture approach works correctly
* when the moduleVersion is explicitly configured in the POM. Unlike Maven 4's auto-injection
* scenario, this configuration is present at validation time, so there's no timing mismatch.
* However, validation-time capture should still work correctly.
*
* <p>This test verifies:
* <ol>
* <li>First build creates cache entry with validation-time properties</li>
* <li>Second build restores from cache successfully</li>
* <li>Explicit configuration is captured correctly at validation time</li>
* </ol>
*/
@IntegrationTest("src/test/projects/explicit-module-version")
class ExplicitModuleVersionTest {

/**
* Verifies that JPMS module compilation with explicit moduleVersion works with cache restoration.
* This tests that validation-time capture works correctly when moduleVersion is explicitly
* configured in the POM (no Maven 4 auto-injection needed).
*
* @param verifier Maven verifier for running builds
* @throws VerificationException if verification fails
*/
@Test
void testExplicitModuleVersionCacheRestoration(Verifier verifier) throws VerificationException {
verifier.setAutoclean(false);

// First build - should create cache entry with validation-time properties
verifier.setLogFileName("../log-build-1.txt");
verifier.executeGoal("clean");
verifier.executeGoal("package");
verifier.verifyErrorFreeLog();

// Verify compilation succeeded
verifier.verifyFilePresent("target/classes/module-info.class");
verifier.verifyFilePresent("target/classes/org/apache/maven/caching/test/explicit/ExplicitVersionModule.class");
verifier.verifyFilePresent("target/explicit-module-version-1.0.0-SNAPSHOT.jar");

// Second build - should restore from cache
verifier.setLogFileName("../log-build-2.txt");
verifier.executeGoal("clean");
verifier.executeGoal("package");
verifier.verifyErrorFreeLog();

// Verify cache was used (not rebuilt)
verifier.verifyTextInLog(
"Found cached build, restoring org.apache.maven.caching.test.explicit:explicit-module-version from cache");

// Verify compilation was skipped (restored from cache)
verifier.verifyTextInLog("Skipping plugin execution (cached): compiler:compile");

// Verify JAR was restored from cache
verifier.verifyFilePresent("target/explicit-module-version-1.0.0-SNAPSHOT.jar");
}
}
Loading
Loading