Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,22 @@ public enum DependencyScope {
COMPILE_ONLY("compile-only", false),

/**
* Compile, runtime and test.
* Compile, runtime and test (non-transitive).
* <p>
* <b>Note:</b> If a project uses this scope and doesn't explicitly opt-in to
* Maven 4 modelVersion (e.g., 4.1.0) through other features, the generated
* consumer POM may be downgraded to 4.0.0. In this case, downstream resolvers
* will remap this scope to {@code api} for backward compatibility, making it
* behave transitively again. To enforce non-transitive behavior, the project
* must ensure its modelVersion is preserved (e.g., via {@code preserveModelVersion=true}).
* </p>
*/
COMPILE("compile", true),
COMPILE("compile", false),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical regression: Changing COMPILE from transitive=true to transitive=false has cascading effects:

  1. DefaultConsumerPomBuilder.hasDependencyScope() uses !scope.isTransitive() to decide which dependencies to strip from consumer POMs. With this change, all compile-scoped dependencies (the most common case, since compile is the default scope) will be silently removed from consumer POMs.

  2. Maven4ScopeManagerConfiguration passes DependencyScope.COMPILE.isTransitive() to createDependencyScope(). COMPILE now falls into nonTransitiveDependencyScopes in buildResolutionScopes(), causing the resolver to eliminate transitive dependencies of compile-scoped libraries.

At minimum, DefaultConsumerPomBuilder.hasDependencyScope() needs to be updated to not rely on isTransitive() for consumer POM inclusion, and the compile-to-non-transitive behavior should be gated on model version so 4.0.0 projects retain current behavior.


/**
* Compile, runtime and test, transitively.
*/
API("api", true),

/**
* Runtime and test.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,17 @@ public interface PathScope extends ExtensibleEnum {
ProjectScope.MAIN,
DependencyScope.COMPILE_ONLY,
DependencyScope.COMPILE,
DependencyScope.API,
DependencyScope.PROVIDED);

PathScope MAIN_RUNTIME =
pathScope("main-runtime", ProjectScope.MAIN, DependencyScope.COMPILE, DependencyScope.RUNTIME);
PathScope MAIN_RUNTIME = pathScope(
"main-runtime", ProjectScope.MAIN, DependencyScope.COMPILE, DependencyScope.API, DependencyScope.RUNTIME);

PathScope TEST_COMPILE = pathScope(
"test-compile",
ProjectScope.TEST,
DependencyScope.COMPILE,
DependencyScope.API,
DependencyScope.PROVIDED,
DependencyScope.TEST_ONLY,
DependencyScope.TEST);
Expand All @@ -75,6 +77,7 @@ public interface PathScope extends ExtensibleEnum {
"test-runtime",
ProjectScope.TEST,
DependencyScope.COMPILE,
DependencyScope.API,
DependencyScope.RUNTIME,
DependencyScope.PROVIDED,
DependencyScope.TEST,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@

import java.io.InputStream;
import java.util.Collections;
import java.util.List;

import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.PluginExecution;
Expand Down Expand Up @@ -72,4 +75,57 @@ void testV4ModelPriority() {
PluginExecution.newInstance().withPriority(5))))));
assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m));
}

@Test
void testApiScopeDependencyRequires410() {
// A model with an api-scoped dependency should require modelVersion 4.1.0
Model m = model.withDependencies(List.of(Dependency.newBuilder()
.groupId("org.example")
.artifactId("api-lib")
.version("1.0")
.scope("api")
.build()));
assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m));
}

@Test
void testApiScopeInDependencyManagementRequires410() {
// A model with an api-scoped dependency in dependencyManagement should require 4.1.0
Model m = model.withDependencyManagement(DependencyManagement.newBuilder()
.dependencies(List.of(Dependency.newBuilder()
.groupId("org.example")
.artifactId("api-lib")
.version("1.0")
.scope("api")
.build()))
.build());
assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m));
}

@Test
void testCompileScopeDependencyRemains400() {
// A model with only compile-scoped dependencies should stay at 4.0.0
Model m = model.withDependencies(List.of(Dependency.newBuilder()
.groupId("org.example")
.artifactId("compile-lib")
.version("1.0")
.scope("compile")
.build()));
assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m));
}

@Test
void testApiScopeInProfileRequires410() {
// A model with an api-scoped dependency in a profile should require 4.1.0
Model m = model.withProfiles(List.of(org.apache.maven.api.model.Profile.newBuilder()
.id("my-profile")
.dependencies(List.of(Dependency.newBuilder()
.groupId("org.example")
.artifactId("api-lib")
.version("1.0")
.scope("api")
.build()))
.build()));
assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ public Collection<org.eclipse.aether.scope.DependencyScope> buildDependencyScope
ArrayList<org.eclipse.aether.scope.DependencyScope> result = new ArrayList<>();
result.add(internalScopeManager.createDependencyScope(
DependencyScope.COMPILE.id(), DependencyScope.COMPILE.isTransitive(), all()));
result.add(internalScopeManager.createDependencyScope(
DependencyScope.API.id(), DependencyScope.API.isTransitive(), all()));
result.add(internalScopeManager.createDependencyScope(
DependencyScope.RUNTIME.id(),
DependencyScope.RUNTIME.isTransitive(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ private Model buildEffectiveModel(RepositorySystemSession session, MavenProject
}
return dependency;
});
// Only keep transitive scopes (null/empty => COMPILE)
// Only keep consumer-visible scopes (compile, api, runtime)
directDependencies.values().removeIf(DefaultConsumerPomBuilder::hasDependencyScope);
managedDependencies.keySet().removeAll(directDependencies.keySet());

Expand All @@ -275,7 +275,7 @@ private Model buildEffectiveModel(RepositorySystemSession session, MavenProject
Function.identity(),
this::merge,
LinkedHashMap::new));
// Only keep transitive scopes
// Only keep consumer-visible scopes (compile, api, runtime)
directDependencies.values().removeIf(DefaultConsumerPomBuilder::hasDependencyScope);
model = model.withDependencies(directDependencies.isEmpty() ? null : directDependencies.values());
}
Expand All @@ -291,7 +291,7 @@ private static boolean hasDependencyScope(Dependency dependency) {
} else {
scope = DependencyScope.forId(scopeId);
}
return scope == null || !scope.isTransitive();
return scope != DependencyScope.COMPILE && scope != DependencyScope.RUNTIME && scope != DependencyScope.API;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking — stale comments: The hasDependencyScope() method now explicitly checks for COMPILE, RUNTIME, and API instead of using isTransitive(). The comments at lines 266 and 284 still say "Only keep transitive scopes" but COMPILE is no longer transitive. Consider updating them to "Only keep consumer-visible scopes (compile, api, runtime)" to match the new logic.

}

private Dependency merge(Dependency dep1, Dependency dep2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,108 @@ void testConsumerPomPassesProjectRepositoriesToModelBuilder() throws Exception {
consumerRequest.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId()));
assertTrue(hasCustomRepo, "Consumer POM model builder request should include the project's custom repository");
}

@Test
void testConsumerPomRetainsCompileApiRuntimeDeps() throws Exception {
// Consumer POMs must retain compile, api, and runtime dependencies
org.apache.maven.api.model.Dependency compileDep = org.apache.maven.api.model.Dependency.newBuilder()
.groupId("g")
.artifactId("compile-dep")
.version("1")
.scope("compile")
.build();
org.apache.maven.api.model.Dependency apiDep = org.apache.maven.api.model.Dependency.newBuilder()
.groupId("g")
.artifactId("api-dep")
.version("1")
.scope("api")
.build();
org.apache.maven.api.model.Dependency runtimeDep = org.apache.maven.api.model.Dependency.newBuilder()
.groupId("g")
.artifactId("runtime-dep")
.version("1")
.scope("runtime")
.build();
org.apache.maven.api.model.Dependency unscopedDep = org.apache.maven.api.model.Dependency.newBuilder()
.groupId("g")
.artifactId("unscoped-dep")
.version("1")
.build();

Model model = Model.newBuilder()
.groupId("test")
.artifactId("test")
.version("1.0")
.dependencies(List.of(compileDep, apiDep, runtimeDep, unscopedDep))
.build();

Model transformed = DefaultConsumerPomBuilder.transformNonPom(model, null);
assertNotNull(transformed.getDependencies());
// All four should be retained
assertTrue(
transformed.getDependencies().stream().anyMatch(d -> "compile-dep".equals(d.getArtifactId())),
"compile-scoped dep should be retained");
assertTrue(
transformed.getDependencies().stream().anyMatch(d -> "api-dep".equals(d.getArtifactId())),
"api-scoped dep should be retained");
assertTrue(
transformed.getDependencies().stream().anyMatch(d -> "runtime-dep".equals(d.getArtifactId())),
"runtime-scoped dep should be retained");
assertTrue(
transformed.getDependencies().stream().anyMatch(d -> "unscoped-dep".equals(d.getArtifactId())),
"unscoped (default compile) dep should be retained");
}

@Test
void testConsumerPomStripsProvidedTestSystemDeps() throws Exception {
// Consumer POMs must strip provided, test, and system dependencies
org.apache.maven.api.model.Dependency compileDep = org.apache.maven.api.model.Dependency.newBuilder()
.groupId("g")
.artifactId("compile-dep")
.version("1")
.scope("compile")
.build();
org.apache.maven.api.model.Dependency providedDep = org.apache.maven.api.model.Dependency.newBuilder()
.groupId("g")
.artifactId("provided-dep")
.version("1")
.scope("provided")
.build();
org.apache.maven.api.model.Dependency testDep = org.apache.maven.api.model.Dependency.newBuilder()
.groupId("g")
.artifactId("test-dep")
.version("1")
.scope("test")
.build();
org.apache.maven.api.model.Dependency systemDep = org.apache.maven.api.model.Dependency.newBuilder()
.groupId("g")
.artifactId("system-dep")
.version("1")
.scope("system")
.build();

Model model = Model.newBuilder()
.groupId("test")
.artifactId("test")
.version("1.0")
.dependencies(List.of(compileDep, providedDep, testDep, systemDep))
.build();

Model transformed = DefaultConsumerPomBuilder.transformNonPom(model, null);
assertNotNull(transformed.getDependencies());
// compile should be retained
assertTrue(
transformed.getDependencies().stream().anyMatch(d -> "compile-dep".equals(d.getArtifactId())),
"compile-scoped dep should be retained");
// provided, test, system should be stripped
assertFalse(
transformed.getDependencies().stream().anyMatch(d -> "provided-dep".equals(d.getArtifactId())),
"provided-scoped dep should be stripped");
assertFalse(
transformed.getDependencies().stream().anyMatch(d -> "test-dep".equals(d.getArtifactId())),
"test-scoped dep should be stripped");
assertFalse(
transformed.getDependencies().stream().anyMatch(d -> "system-dep".equals(d.getArtifactId())),
"system-scoped dep should be stripped");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1239,7 +1239,8 @@ private void validate20RawDependencies(
String scope = dependency.getScope();
if (DependencyScope.COMPILE_ONLY.id().equals(scope)
|| DependencyScope.TEST_ONLY.id().equals(scope)
|| DependencyScope.TEST_RUNTIME.id().equals(scope)) {
|| DependencyScope.TEST_RUNTIME.id().equals(scope)
|| DependencyScope.API.id().equals(scope)) {
addViolation(
problems,
Severity.ERROR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ private int getPolicy(RepositorySystemSession session, Artifact a, ArtifactDescr
private void populateResult(InternalSession session, ArtifactDescriptorResult result, Model model) {
ArtifactTypeRegistry stereotypes = session.getSession().getArtifactTypeRegistry();

// Compute once whether compile-scoped dependencies should be remapped to api (transitive)
// for backward compatibility. Use the declared modelVersion rather than feature detection
// to respect the developer's explicit intent.
String declaredModelVersion = model.getModelVersion();
boolean remapCompileToApi = declaredModelVersion == null || declaredModelVersion.startsWith("4.0.");

for (Repository repository : model.getRepositories()) {
result.addRepository(session.toRepository(
session.getService(RepositoryFactory.class).createRemote(repository)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking — test coverage gap: The remapCompileToApi logic computed here and applied in convert() is the cornerstone of backward compatibility, but it has no direct unit test. Consider adding a test in DefaultArtifactDescriptorReaderTest that verifies:

  • 4.0.0 modelVersion → compile scope is remapped to api
  • 4.1.0 modelVersion → compile scope is left as-is

Expand All @@ -355,7 +361,7 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re
logger.debug("Filtered dependency with uninterpolated expression: {}", dependency);
continue;
}
result.addDependency(convert(dependency, stereotypes));
result.addDependency(convert(dependency, stereotypes, remapCompileToApi));
}

DependencyManagement dependencyManagement = model.getDependencyManagement();
Expand All @@ -365,7 +371,7 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re
logger.debug("Filtered managed dependency with uninterpolated expression: {}", dependency);
continue;
}
result.addManagedDependency(convert(dependency, stereotypes));
result.addManagedDependency(convert(dependency, stereotypes, remapCompileToApi));
}
}

Expand All @@ -391,7 +397,10 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re
setArtifactProperties(result, model);
}

private Dependency convert(org.apache.maven.api.model.Dependency dependency, ArtifactTypeRegistry stereotypes) {
private Dependency convert(
org.apache.maven.api.model.Dependency dependency,
ArtifactTypeRegistry stereotypes,
boolean remapCompileToApi) {
ArtifactType stereotype = stereotypes.get(dependency.getType());
if (stereotype == null) {
stereotype = new DefaultType(dependency.getType(), Language.NONE, dependency.getType(), null, false)
Expand Down Expand Up @@ -420,11 +429,13 @@ private Dependency convert(org.apache.maven.api.model.Dependency dependency, Art
exclusions.add(convert(exclusion));
}

String scope = dependency.getScope() != null ? dependency.getScope() : "";
if (remapCompileToApi && ("compile".equals(scope) || scope.isEmpty())) {
scope = "api";
}

return new Dependency(
artifact,
dependency.getScope(),
dependency.getOptional() != null ? dependency.isOptional() : null,
exclusions);
artifact, scope, dependency.getOptional() != null ? dependency.isOptional() : null, exclusions);
}

private Exclusion convert(org.apache.maven.api.model.Exclusion exclusion) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public Collection<org.eclipse.aether.scope.DependencyScope> buildDependencyScope
ArrayList<org.eclipse.aether.scope.DependencyScope> result = new ArrayList<>();
result.add(internalScopeManager.createDependencyScope(
DependencyScope.COMPILE.id(), DependencyScope.COMPILE.isTransitive(), all()));
result.add(internalScopeManager.createDependencyScope(
DependencyScope.API.id(), DependencyScope.API.isTransitive(), all()));
result.add(internalScopeManager.createDependencyScope(
DependencyScope.RUNTIME.id(),
DependencyScope.RUNTIME.isTransitive(),
Expand Down
Loading