Skip to content

Commit dedca8c

Browse files
Hiteshsai007gnodetclaude
authored
[MNG-8287] Dependencies should always use consumer pom (#12744)
* [MNG-8287] Dependencies should always use consumer pom When generating a consumer POM, profiles activated by packaging that match the current project's packaging should have their content inlined into the consumer POM and the activation should be removed. This ensures consistent dependency resolution across different tools. * Fix consumer POM dependency resolution for packaging-activated profiles - Inline packaging-activated profiles during transformPom for non-flattened builds - Drop profiles activated by non-matching packaging entirely - Update ConsumerPomBuilderTest to enable flattening and assert on dropped profiles Signed-off-by: Hitesh <hiteshsaibv.24cs@saividya.ac.in> * Fix consumer POM packaging profile handling * Address review feedback: add dependency deduplication and test coverage - Add key-based deduplication when merging inlined dependencies, managed dependencies, and repositories from packaging-activated profiles to prevent duplicates in the consumer POM - Add unit tests for dependency management and repository inlining paths in inlinePackagingActivatedProfiles - Add unit test for duplicate dependency guard (model deps take precedence over profile duplicates) - Widen transformBom visibility to package-private to match transformNonPom and transformPom Signed-off-by: Hitesh <hiteshsaibv.24cs@saividya.ac.in> * Filter import-scoped managed deps from inlined packaging profiles When inlining packaging-activated profiles, import-scoped entries in dependencyManagement (BOM imports) were not filtered out. These entries are flattened during resolution and must not reappear in the consumer POM. Also fix NPE in testBomPackagingActivatedProfilesArePreserved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Signed-off-by: Hitesh <hiteshsaibv.24cs@saividya.ac.in> Co-authored-by: Guillaume Nodet <gnodet@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 24d371b commit dedca8c

5 files changed

Lines changed: 524 additions & 1 deletion

File tree

impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.LinkedHashMap;
2626
import java.util.List;
2727
import java.util.Map;
28+
import java.util.Objects;
2829
import java.util.Properties;
2930
import java.util.function.Function;
3031
import java.util.stream.Collectors;
@@ -37,6 +38,7 @@
3738
import org.apache.maven.api.feature.Features;
3839
import org.apache.maven.api.model.Activation;
3940
import org.apache.maven.api.model.Dependency;
41+
import org.apache.maven.api.model.DependencyManagement;
4042
import org.apache.maven.api.model.DistributionManagement;
4143
import org.apache.maven.api.model.Model;
4244
import org.apache.maven.api.model.ModelBase;
@@ -459,6 +461,10 @@ private ModelBuilderResult buildModel(RepositorySystemSession session, MavenProj
459461

460462
static Model transformNonPom(Model model, MavenProject project) {
461463
boolean preserveModelVersion = model.isPreserveModelVersion();
464+
String packaging = model.getPackaging();
465+
466+
// Inline packaging-activated profiles into the model
467+
model = inlinePackagingActivatedProfiles(model, packaging);
462468

463469
Model.Builder builder = prune(
464470
Model.newBuilder(model, true)
@@ -490,7 +496,7 @@ static Model transformNonPom(Model model, MavenProject project) {
490496
return model;
491497
}
492498

493-
private static Model transformBom(Model model, MavenProject project) {
499+
static Model transformBom(Model model, MavenProject project) {
494500
boolean preserveModelVersion = model.isPreserveModelVersion();
495501

496502
Model.Builder builder = prune(
@@ -554,6 +560,125 @@ private static void warnNotDowngraded(MavenProject project) {
554560
+ "attribute on the <project> element of your POM.");
555561
}
556562

563+
/**
564+
* Inlines packaging-activated profiles into the model.
565+
* <p>
566+
* When a profile is activated by packaging and the packaging matches the project's packaging,
567+
* the profile's content (dependencies, dependency management, repositories) is merged into
568+
* the main model and the profile is removed. This ensures consistent behavior across all
569+
* tools consuming the POM, since packaging activation is a 4.1.0+ feature not available
570+
* in Maven 3 or other tools like Gradle.
571+
* <p>
572+
* If the profile has other activation conditions besides packaging, only the packaging
573+
* part is stripped from the activation; the profile's content is <b>not</b> inlined to
574+
* preserve AND semantics (the content remains gated by the remaining conditions).
575+
* <p>
576+
* Profiles with a non-matching packaging activation are dropped entirely, since they
577+
* can never activate for this artifact's fixed packaging and their presence would block
578+
* model version downgrade to 4.0.0.
579+
* <p>
580+
* Non-transitive scope dependencies (test, provided, system) from inlined profiles are
581+
* filtered out to prevent leakage into the consumer POM.
582+
*
583+
* @param model the model to process
584+
* @param packaging the project's packaging type
585+
* @return the model with packaging-activated profiles inlined
586+
*/
587+
static Model inlinePackagingActivatedProfiles(Model model, String packaging) {
588+
List<Profile> remainingProfiles = new ArrayList<>();
589+
List<Dependency> additionalDeps = new ArrayList<>();
590+
List<Dependency> additionalManagedDeps = new ArrayList<>();
591+
List<Repository> additionalRepos = new ArrayList<>();
592+
593+
for (Profile profile : model.getProfiles()) {
594+
Activation activation = profile.getActivation();
595+
if (activation != null && activation.getPackaging() != null) {
596+
if (Objects.equals(activation.getPackaging(), packaging)) {
597+
Activation strippedActivation = stripPackagingActivation(activation);
598+
if (strippedActivation != null) {
599+
// Keep the profile but remove the packaging activation part
600+
// Do not inline its contents since it has other activation conditions
601+
remainingProfiles.add(profile.withActivation(strippedActivation));
602+
} else {
603+
// Packaging is the ONLY condition.
604+
// Inline profile content into the model
605+
additionalDeps.addAll(profile.getDependencies());
606+
if (profile.getDependencyManagement() != null) {
607+
additionalManagedDeps.addAll(
608+
profile.getDependencyManagement().getDependencies());
609+
}
610+
additionalRepos.addAll(profile.getRepositories());
611+
}
612+
} else {
613+
// Packaging does not match — drop the profile entirely
614+
}
615+
} else {
616+
// No packaging activation — keep the profile as-is
617+
remainingProfiles.add(profile);
618+
}
619+
}
620+
621+
// Merge additional dependencies into the model, deduplicating by key
622+
if (!additionalDeps.isEmpty()) {
623+
additionalDeps.removeIf(DefaultConsumerPomBuilder::hasDependencyScope);
624+
Map<String, Dependency> mergedDeps = new LinkedHashMap<>();
625+
for (Dependency dep : model.getDependencies()) {
626+
mergedDeps.put(getDependencyKey(dep), dep);
627+
}
628+
for (Dependency dep : additionalDeps) {
629+
mergedDeps.putIfAbsent(getDependencyKey(dep), dep);
630+
}
631+
model = model.withDependencies(mergedDeps.values());
632+
}
633+
634+
// Merge additional managed dependencies into the model, deduplicating by key
635+
if (!additionalManagedDeps.isEmpty()) {
636+
// Filter out import-scoped entries — they are BOM references that get
637+
// flattened during resolution and must not reappear in the consumer POM
638+
additionalManagedDeps.removeIf(dep -> "import".equals(dep.getScope()));
639+
DependencyManagement dm = model.getDependencyManagement();
640+
Map<String, Dependency> mergedManagedDeps = new LinkedHashMap<>();
641+
if (dm != null) {
642+
for (Dependency dep : dm.getDependencies()) {
643+
mergedManagedDeps.put(getDependencyKey(dep), dep);
644+
}
645+
}
646+
for (Dependency dep : additionalManagedDeps) {
647+
mergedManagedDeps.putIfAbsent(getDependencyKey(dep), dep);
648+
}
649+
model = model.withDependencyManagement((dm != null ? dm : DependencyManagement.newInstance())
650+
.withDependencies(mergedManagedDeps.values()));
651+
}
652+
653+
// Merge additional repositories into the model, deduplicating by id
654+
if (!additionalRepos.isEmpty()) {
655+
Map<String, Repository> mergedRepos = new LinkedHashMap<>();
656+
for (Repository repo : model.getRepositories()) {
657+
mergedRepos.put(repo.getId(), repo);
658+
}
659+
for (Repository repo : additionalRepos) {
660+
mergedRepos.putIfAbsent(repo.getId(), repo);
661+
}
662+
model = model.withRepositories(mergedRepos.values());
663+
}
664+
665+
return model.withProfiles(remainingProfiles);
666+
}
667+
668+
/**
669+
* Strips the packaging activation from an activation, returning the remaining activation
670+
* or {@code null} if packaging was the only activation condition.
671+
*/
672+
private static Activation stripPackagingActivation(Activation activation) {
673+
Activation stripped =
674+
Activation.newBuilder(activation, true).packaging(null).build();
675+
// Check if the remaining activation has any other conditions
676+
if (isActivationEmpty(stripped)) {
677+
return null;
678+
}
679+
return stripped;
680+
}
681+
557682
/**
558683
* Strips {@code executable()} conditions from profile activations.
559684
* <p>

0 commit comments

Comments
 (0)