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 @@ -96,7 +96,7 @@ public void injectTransformedArtifacts(RepositorySystemSession session, MavenPro
}
}

TransformedArtifact createConsumerPomArtifact(
private TransformedArtifact createConsumerPomArtifact(
MavenProject project, Path consumer, RepositorySystemSession session) {
Path actual = project.getFile().toPath();
Path parent = project.getBaseDirectory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.apache.maven.api.model.DistributionManagement;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.ModelBase;
import org.apache.maven.api.model.Parent;
import org.apache.maven.api.model.Profile;
import org.apache.maven.api.model.Repository;
import org.apache.maven.api.model.Scm;
Expand All @@ -50,6 +51,7 @@
import org.apache.maven.impl.InternalSession;
import org.apache.maven.model.v4.MavenModelVersion;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.SourceQueries;
import org.eclipse.aether.RepositorySystemSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -342,7 +344,7 @@ static Model transformNonPom(Model model, MavenProject project) {
return model;
}

static Model transformBom(Model model, MavenProject project) {
private static Model transformBom(Model model, MavenProject project) {
boolean preserveModelVersion = model.isPreserveModelVersion();

Model.Builder builder = prune(
Expand All @@ -369,19 +371,33 @@ static Model transformPom(Model model, MavenProject project) {

// raw to consumer transform
model = model.withRoot(false).withModules(null).withSubprojects(null);
if (model.getParent() != null) {
model = model.withParent(model.getParent().withRelativePath(null));
Parent parent = model.getParent();
if (parent != null) {
model = model.withParent(parent.withRelativePath(null));
}
var projectSources = project.getBuild().getDelegate().getSources();
if (SourceQueries.usesModuleSourceHierarchy(projectSources)) {
// Dependencies are dispatched by maven-jar-plugin in the POM generated for each module.
model = model.withDependencies(null).withPackaging(POM_PACKAGING);
}

if (!preserveModelVersion) {
/*
* If tne <build> contains <source> elements, it is not compatible with the Maven 4.0.0 model.
* Remove the full <build> element instead of removing only the <sources> element, because the
* build without sources does not mean much. Reminder: this removal can be disabled by setting
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* build without sources does not mean much. Reminder: this removal can be disabled by setting
* If the <build> contains <source> elements, it is not compatible with the Maven 4.0.0 model.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The suggested change seems strange. It replaces the middle of a sentence ("build without sources …") by a copy of the first sentence. I suspect that the intend was to replace the first sentence for fixing the "n" in "the".

* the `preserveModelVersion` XML attribute or `preserve.model.version` property to true.
*/
if (SourceQueries.hasEnabledSources(projectSources)) {
model = model.withBuild(null);
}
model = model.withPreserveModelVersion(false);
String modelVersion = new MavenModelVersion().getModelVersion(model);
model = model.withModelVersion(modelVersion);
}
return model;
}

static void warnNotDowngraded(MavenProject project) {
private static void warnNotDowngraded(MavenProject project) {
LOGGER.warn("The consumer POM for " + project.getId() + " cannot be downgraded to 4.0.0. "
+ "If you intent your build to be consumed with Maven 3 projects, you need to remove "
+ "the features that request a newer model version. If you're fine with having the "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,6 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
// only set those on 2nd phase, ignore on 1st pass
if (project.getFile() != null) {
Build build = project.getBuild().getDelegate();
List<org.apache.maven.api.model.Source> sources = build.getSources();
Path baseDir = project.getBaseDirectory();
Function<ProjectScope, String> outputDirectory = (scope) -> {
if (scope == ProjectScope.MAIN) {
Expand All @@ -666,23 +665,11 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
return build.getDirectory();
}
};
// Extract modules from sources to detect modular projects
Set<String> modules = extractModules(sources);
boolean isModularProject = !modules.isEmpty();

logger.trace(
"Module detection for project {}: found {} module(s) {} - modular project: {}.",
project.getId(),
modules.size(),
modules,
isModularProject);

// Create source handling context for unified tracking of all lang/scope combinations
SourceHandlingContext sourceContext =
new SourceHandlingContext(project, baseDir, modules, isModularProject, result);
final SourceHandlingContext sourceContext = new SourceHandlingContext(project, result);

// Process all sources, tracking enabled ones and detecting duplicates
for (var source : sources) {
for (org.apache.maven.api.model.Source source : sourceContext.sources) {
var sourceRoot = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory, source);
// Track enabled sources for duplicate detection and hasSources() queries
// Only add source if it's not a duplicate enabled source (first enabled wins)
Expand Down Expand Up @@ -711,7 +698,7 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
implicit fallback (only if they match the default, e.g., inherited)
- This allows incremental adoption (e.g., custom resources + default Java)
*/
if (sources.isEmpty()) {
if (sourceContext.sources.isEmpty()) {
// Classic fallback: no <sources> configured, use legacy directories
project.addScriptSourceRoot(build.getScriptSourceDirectory());
project.addCompileSourceRoot(build.getSourceDirectory());
Expand All @@ -724,8 +711,7 @@ implicit fallback (only if they match the default, e.g., inherited)
if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) {
project.addScriptSourceRoot(build.getScriptSourceDirectory());
}

if (isModularProject) {
if (sourceContext.usesModuleSourceHierarchy()) {
// Modular: reject ALL legacy directory configurations
failIfLegacyDirectoryPresent(
build.getSourceDirectory(),
Expand Down Expand Up @@ -1243,22 +1229,6 @@ public Set<Entry<K, V>> entrySet() {
}
}

/**
* Extracts unique module names from the given list of source elements.
* A project is considered modular if it has at least one module name.
*
* @param sources list of source elements from the build
* @return set of non-blank module names
*/
private static Set<String> extractModules(List<org.apache.maven.api.model.Source> sources) {
return sources.stream()
.map(org.apache.maven.api.model.Source::getModule)
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isBlank())
.collect(Collectors.toSet());
}

private Model injectLifecycleBindings(
Model model,
ModelBuilderRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.maven.api.ProjectScope;
import org.apache.maven.api.SourceRoot;
import org.apache.maven.api.model.Resource;
import org.apache.maven.api.model.Source;
import org.apache.maven.api.services.BuilderProblem.Severity;
import org.apache.maven.api.services.ModelBuilderResult;
import org.apache.maven.api.services.ModelProblem.Version;
Expand All @@ -37,9 +38,7 @@

/**
* Handles source configuration for Maven projects with unified tracking for all language/scope combinations.
* <p>
* This class replaces the previous approach of hardcoded boolean flags (hasMain, hasTest, etc.)
* with a flexible set-based tracking mechanism that works for any language and scope combination.
* This class uses a flexible set-based tracking mechanism that works for any language and scope combination.
* <p>
* Key features:
* <ul>
Expand All @@ -51,7 +50,7 @@
*
* @since 4.0.0
*/
class SourceHandlingContext {
final class SourceHandlingContext {

private static final Logger LOGGER = LoggerFactory.getLogger(SourceHandlingContext.class);

Expand All @@ -60,26 +59,38 @@ class SourceHandlingContext {
*/
record SourceKey(Language language, ProjectScope scope, String module, Path directory) {}

/**
* The {@code <source>} elements declared in the {@code <build>} elements.
*/
final List<Source> sources;

private final MavenProject project;
private final Path baseDir;
private final Set<String> modules;
private final boolean modularProject;
private final ModelBuilderResult result;
private final Set<SourceKey> declaredSources;

SourceHandlingContext(
MavenProject project,
Path baseDir,
Set<String> modules,
boolean modularProject,
ModelBuilderResult result) {
SourceHandlingContext(MavenProject project, ModelBuilderResult result) {
this.project = project;
this.baseDir = baseDir;
this.modules = modules;
this.modularProject = modularProject;
this.sources = project.getBuild().getDelegate().getSources();
this.modules = SourceQueries.getModuleNames(sources);
this.result = result;
// Each module typically has main, test, main resources, test resources = 4 sources
this.declaredSources = new HashSet<>(4 * modules.size());
if (usesModuleSourceHierarchy()) {
LOGGER.trace("Found {} module(s) in the \"{}\" project: {}.", project.getId(), modules.size(), modules);
} else {
LOGGER.trace("Project \"{}\" is non-modular.", project.getId());
}
}

/**
* Whether the project uses module source hierarchy.
* Note that this is not synonymous of whether the project is modular,
* because it is possible to create a single Java module in a classic Maven project
* (i.e., using package hierarchy).
*/
boolean usesModuleSourceHierarchy() {
return !modules.isEmpty();
}

/**
Expand Down Expand Up @@ -112,7 +123,7 @@ boolean shouldAddSource(SourceRoot sourceRoot) {
SourceKey key = new SourceKey(
sourceRoot.language(), sourceRoot.scope(), sourceRoot.module().orElse(null), normalizedDir);

if (declaredSources.contains(key)) {
if (!declaredSources.add(key)) {
String message = String.format(
"Duplicate enabled source detected: lang=%s, scope=%s, module=%s, directory=%s. "
+ "First enabled source wins, this duplicate is ignored.",
Expand All @@ -130,7 +141,6 @@ boolean shouldAddSource(SourceRoot sourceRoot) {
return false; // Don't add duplicate enabled source
}

declaredSources.add(key);
LOGGER.debug(
"Adding and tracking enabled source: lang={}, scope={}, module={}, dir={}",
key.language(),
Expand All @@ -151,6 +161,13 @@ boolean hasSources(Language language, ProjectScope scope) {
return declaredSources.stream().anyMatch(key -> language.equals(key.language()) && scope.equals(key.scope()));
}

/**
* {@return the source directory as defined by Maven conventions}
*/
private Path getStandardSourceDirectory() {
return project.getBaseDirectory().resolve("src");
}

/**
* Fails the build if modular and classic (non-modular) sources are mixed within {@code <sources>}.
* <p>
Expand All @@ -164,30 +181,32 @@ boolean hasSources(Language language, ProjectScope scope) {
void failIfMixedModularAndClassicSources() {
for (ProjectScope scope : List.of(ProjectScope.MAIN, ProjectScope.TEST)) {
for (Language language : List.of(Language.JAVA_FAMILY, Language.RESOURCES)) {
boolean hasModular = declaredSources.stream()
.anyMatch(key ->
language.equals(key.language()) && scope.equals(key.scope()) && key.module() != null);
boolean hasClassic = declaredSources.stream()
.anyMatch(key ->
language.equals(key.language()) && scope.equals(key.scope()) && key.module() == null);

if (hasModular && hasClassic) {
String message = String.format(
"Mixed modular and classic sources detected for lang=%s, scope=%s. "
+ "A project must be either fully modular (all sources have a module) "
+ "or fully classic (no sources have a module). "
+ "The compiler plugin cannot handle mixed configurations.",
language.id(), scope.id());
LOGGER.error(message);
result.getProblemCollector()
.reportProblem(new DefaultModelProblem(
message,
Severity.ERROR,
Version.V41,
project.getModel().getDelegate(),
-1,
-1,
null));
boolean hasModular = false;
boolean hasClassic = false;
for (SourceKey key : declaredSources) {
if (language.equals(key.language()) && scope.equals(key.scope())) {
String module = key.module();
hasModular |= (module != null);
hasClassic |= (module == null);
if (hasModular && hasClassic) {
String message = String.format(
"Mixed modular and classic sources detected for lang=%s, scope=%s. "
+ "A project must be either fully modular (all sources have a module) "
+ "or fully classic (no sources have a module).",
language.id(), scope.id());
LOGGER.error(message);
result.getProblemCollector()
.reportProblem(new DefaultModelProblem(
message,
Severity.ERROR,
Version.V41,
project.getModel().getDelegate(),
-1,
-1,
null));
break;
}
}
}
}
}
Expand Down Expand Up @@ -219,7 +238,7 @@ void handleResourceConfiguration(ProjectScope scope) {
? "<source><lang>resources</lang></source>"
: "<source><lang>resources</lang><scope>test</scope></source>";

if (modularProject) {
if (usesModuleSourceHierarchy()) {
if (hasResourcesInSources) {
// Modular project with resources configured via <sources> - already added above
if (hasExplicitLegacyResources(resources, scopeId)) {
Expand Down Expand Up @@ -298,6 +317,7 @@ void handleResourceConfiguration(ProjectScope scope) {
// Use legacy resources element
LOGGER.debug(
"Using explicit or default {} resources ({} resources configured).", scopeId, resources.size());
Path baseDir = project.getBaseDirectory();
for (Resource resource : resources) {
project.addSourceRoot(new DefaultSourceRoot(baseDir, scope, resource));
}
Expand All @@ -315,7 +335,7 @@ void handleResourceConfiguration(ProjectScope scope) {
*/
private DefaultSourceRoot createModularResourceRoot(String module, ProjectScope scope) {
Path resourceDir =
baseDir.resolve("src").resolve(module).resolve(scope.id()).resolve("resources");
getStandardSourceDirectory().resolve(module).resolve(scope.id()).resolve("resources");

return new DefaultSourceRoot(
scope,
Expand Down Expand Up @@ -345,12 +365,10 @@ private boolean hasExplicitLegacyResources(List<Resource> resources, String scop
}

// Super POM default paths
String defaultPath =
baseDir.resolve("src").resolve(scope).resolve("resources").toString();
String defaultFilteredPath = baseDir.resolve("src")
.resolve(scope)
.resolve("resources-filtered")
.toString();
Path srcDir = getStandardSourceDirectory();
String defaultPath = srcDir.resolve(scope).resolve("resources").toString();
String defaultFilteredPath =
srcDir.resolve(scope).resolve("resources-filtered").toString();

// Check if any resource differs from Super POM defaults
for (Resource resource : resources) {
Expand Down
Loading