Skip to content

[3.10.x] Fix #13084: sandbox profile activation context for external model builds - #13114

Merged
cstamas merged 4 commits into
apache:maven-3.10.xfrom
gnodet:fix/13084-sandbox-external-profile-activation-3.10.x
Sep 16, 2026
Merged

cstamas merged 4 commits into
apache:maven-3.10.xfrom
gnodet:fix/13084-sandbox-external-profile-activation-3.10.x

Conversation

@gnodet

@gnodet gnodet commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Backport of #13112 to maven-3.10.x.

Cherry-picked and manually adapted from the master commits:

Conflicts resolved by adapting to the flat module layout of maven-3.10.x:

  • compat/maven-model-builder/ → maven-model-builder/ (flat layout)
  • impl/maven-impl/ changes have no equivalent on 3.10.x (not present)
  • its/core-it-suite/ changes dropped (ITs are in a separate repo on this branch)

See #13112 for the full rationale and design.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Backport of #13112 to maven-3.10.x. Cherry-pick is clean and the approach is consistent across both compat and impl layers.

Design note: The compat PropertyProfileActivator adds context.getProjectProperties() as a third lookup for all builds (not just external), which differs slightly from #13112 on master (where model properties were merged into system properties only in the sandbox context). This is actually more consistent: the impl layer's PropertyProfileActivator also adds model property lookup unconditionally, and the test coverage confirms the intent. POM-declared property activation now works uniformly in both project and external builds on the compat path.

No issues found. Solid backport.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet

gnodet commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ Merge conflict — non-trivial

This branch was created from master instead of maven-3.10.x, resulting in 5837 commits ahead / 707 behind the target branch. The merge produces hundreds of conflicts across restructured modules (maven-model-builder, maven-embedder, maven-resolver-provider, etc.).

The actual fix is only in 2 commits (4c8b837 and a10fb4e), but they cannot be merged as-is due to the divergent branch base.

Recommended fix: Close this PR, create a new branch from maven-3.10.x, and cherry-pick/adapt the fix commits.

This comment was posted by an automated babysit agent.

@gnodet
gnodet requested a review from cstamas September 15, 2026 12:47
gnodet added a commit to gnodet/maven that referenced this pull request Sep 15, 2026
…l model profiles

After the sandbox activation context introduced in apache#13114, profiles in
dependency POMs that fire legitimately (JDK, OS, activeByDefault,
POM-declared property) are now correctly scoped. The blanket
withoutRepositories() stripping that applied to ALL active profiles in
external builds was over-broad: it broke the established pattern where

  project → dep1 → dep2

and dep1 knows dep2 is not on Central, declaring its repository inside a
JDK- or activeByDefault-activated profile.

Remove the withoutRepositories() call and the dead helper method from
DefaultModelBuilder. The sandbox already ensures that profiles activated
by consumer -D flags or file conditions do not fire in external builds,
so any profile whose repos survive to injection has activated on
legitimate grounds.
gnodet added a commit to gnodet/maven that referenced this pull request Sep 15, 2026
…l model profiles

After the sandbox activation context introduced in apache#13114, profiles in
dependency POMs that fire legitimately (JDK, OS, activeByDefault,
POM-declared property) are now correctly scoped. The blanket
withoutRepositories() stripping that applied to ALL active profiles in
external builds was over-broad: it broke the established pattern where

  project → dep1 → dep2

and dep1 knows dep2 is not on Central, declaring its repository inside a
JDK- or activeByDefault-activated profile.

Remove the withoutRepositories() call and the dead helper method from
DefaultModelBuilder. The sandbox already ensures that profiles activated
by consumer -D flags or file conditions do not fire in external builds,
so any profile whose repos survive to injection has activated on
legitimate grounds.
…el builds

Backport of apache#13112 to `maven-3.10.x`.

Cherry-picked and manually adapted from the master commits:
- e58a96a Fix apache#13084: sandbox profile activation context for external model builds
- 8ff7ca6 Address review: fix Javadoc method ref and file-activation asymmetry
- f2bf110 fix: move model property lookup from activator to sandbox context

The second commit (c639b4f) was empty on this branch (it only touched
impl/maven-impl/ which doesn't exist on maven-3.10.x; the equivalent
adjustment was already included in the first commit's ResolvedDependencyProfileActivationTest).

Conflicts resolved by adapting to the flat module layout of maven-3.10.x:
- Changes to compat/maven-model-builder/ cherry-picked cleanly as maven-model-builder/
- Changes to impl/maven-impl/ have no direct equivalent (not removed — confirmed n/a)
- Changes to its/core-it-suite/ removed (ITs are in a separate repo on this branch)
@gnodet
gnodet force-pushed the fix/13084-sandbox-external-profile-activation-3.10.x branch from a10fb4e to f97b621 Compare September 15, 2026 18:18

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-review of rebased/squashed commit (previously approved a10fb4e, now f97b62193a).

The squash absorbed the test-fix commit (a10fb4e) into the main commit cleanly. The design is solid: sandboxing via an anonymous ProfileActivationContext rather than modifying PropertyProfileActivator is the right call — it keeps the activator stateless and avoids unintended side effects on project builds.

One performance issue on the getSystemProperties() override.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Comment on lines +559 to +566
public Map<String, String> getSystemProperties() {
Map<String, String> projectProps = delegate.getProjectProperties();
if (projectProps == null || projectProps.isEmpty()) {
return delegate.getSystemProperties();
}
Map<String, String> merged = new HashMap<>(projectProps);
merged.putAll(delegate.getSystemProperties()); // system wins
return merged;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Performance: getSystemProperties() allocates a new HashMap on every call.

OperatingSystemProfileActivator calls this method 3× per profile (name, arch, version). PropertyProfileActivator and JdkVersionProfileActivator call it once each. In a project with deep transitive deps and profiles using OS activation, this is O(deps × profiles × |systemProperties|) heap allocation per build.

Since the merged map is derived from immutable inputs (system properties don't change mid-build, project properties don't change mid-build), it can be computed once and cached:

Suggested change
public Map<String, String> getSystemProperties() {
Map<String, String> projectProps = delegate.getProjectProperties();
if (projectProps == null || projectProps.isEmpty()) {
return delegate.getSystemProperties();
}
Map<String, String> merged = new HashMap<>(projectProps);
merged.putAll(delegate.getSystemProperties()); // system wins
return merged;
public Map<String, String> getSystemProperties() {
Map<String, String> projectProps = delegate.getProjectProperties();
if (projectProps == null || projectProps.isEmpty()) {
return delegate.getSystemProperties();
}
Map<String, String> merged = new HashMap<>(projectProps);
merged.putAll(delegate.getSystemProperties()); // system wins
return Collections.unmodifiableMap(merged);

Alternatively, compute the map lazily once and store it in a final field of the anonymous class:

private static ProfileActivationContext externalActivationContext(ProfileActivationContext delegate) {
    final Map<String, String> mergedSystemProps;
    Map<String, String> projectProps = delegate.getProjectProperties();
    if (projectProps == null || projectProps.isEmpty()) {
        mergedSystemProps = delegate.getSystemProperties();
    } else {
        Map<String, String> merged = new HashMap<>(projectProps);
        merged.putAll(delegate.getSystemProperties());
        mergedSystemProps = Collections.unmodifiableMap(merged);
    }
    return new ProfileActivationContext() {
        // ...
        @Override
        public Map<String, String> getSystemProperties() {
            return mergedSystemProps;
        }
        // ...
    };
}

The latter approach is preferred: one allocation per external model instead of one per getSystemProperties() call. The unmodifiableMap wrapper also makes the contract explicit — the returned map must not be mutated (currently nothing mutates it, but there's no enforcement).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f80e41d. Pre-computed the merged map before the anonymous class — one allocation per external model instead of one per getSystemProperties() call. Also wrapped the result in Collections.unmodifiableMap() to make the no-mutation contract explicit.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-review of f80e41d33a (performance fix commit).

Prior finding: getSystemProperties() allocated a new HashMap on every call — addressed. The pre-computed mergedSystemProps is captured before the anonymous class construction, wrapped in Collections.unmodifiableMap(), and returned by value on every call. Zero allocation per getSystemProperties() invocation — correct fix.

Test coverage gap (non-blocking): testExternalModelNegatedPropertyDefaultOnIsPreserved tests the "property absent → profile fires" path, but there is no test for the suppression direction — a consumer setting skip.defaults as a system property (not -D) would suppress the dependency's default-on profile, since system properties pass through the sandbox. This is intentional behavior (system props are platform facts), but it's worth a test case to lock it in.

All other findings from prior reviews are addressed. Design is sound.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

…efault-on profile

Port test coverage gap flagged in gnodet-bot re-review:
- Add testExternalModelSystemPropertySuppressesDefaultOnProfile: verifies
  that a consumer setting skip.defaults as a *system* property suppresses
  the dependency's default-on (!skip.defaults) profile in external builds.
  System properties pass through the sandbox unchanged; this is intentional
  behavior but was unprotected by a test.
gnodet added a commit to gnodet/maven that referenced this pull request Sep 16, 2026
…efault-on profile

Port test coverage gap from the apache#13114 review:
- Add negated-property-default-on profile to the XML fixture
- Assert it fires in project builds (skip.defaults absent from system props)
- Add testExternalModelSystemPropertySuppressesDefaultOnProfile: verifies
  that a consumer setting skip.defaults as a *system* property suppresses
  the dependency's default-on profile (system props pass through the sandbox)
gnodet added a commit to gnodet/maven that referenced this pull request Sep 16, 2026
…efault-on profile

Port test coverage gap from the apache#13114 review:
- Add negated-property-default-on profile to the XML fixture
- Assert it fires in project builds (skip.defaults absent from system props)
- Add testExternalModelSystemPropertySuppressesDefaultOnProfile: verifies
  that a consumer setting skip.defaults as a *system* property suppresses
  the dependency's default-on profile (system props pass through the sandbox)
@cstamas
cstamas merged commit 6bfb447 into apache:maven-3.10.x Sep 16, 2026
12 checks passed
@github-actions github-actions Bot added this to the 3.10.0 milestone Sep 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

@cstamas Please assign appropriate label to PR according to the type of change.

@cstamas cstamas added the bug Something isn't working label Sep 16, 2026
gnodet added a commit to gnodet/maven that referenced this pull request Sep 16, 2026
…el builds

Backport of apache#13112 (master) and apache#13114 (maven-3.10.x) to maven-4.0.x.

Instead of blanket-filtering file and property profiles in external model
builds (dependency POMs, parent POMs, imported BOMs), evaluate them against
a sandboxed ProfileActivationContext that:

 - Merges model properties into system property lookups (so POM-declared
   <properties> drive property-activated profiles in dependency builds).
 - Suppresses user properties (consumer -D flags must not activate
   dependency profiles — they were not set for that artifact).
 - Pre-filters file-activated profiles (returning false from exists() would
   incorrectly activate <missing> profiles).
 - Strips repository contributions from all active external profiles.

Both compat and impl stacks are updated identically. The key insight is that
model properties are merged into system properties inside the sandbox rather
than changing PropertyProfileActivator's lookup chain, to avoid unintended
activation when a project POM declares a property that coincidentally matches
a profile's activation condition.

Tests: ExternalModelProfileActivationTest (compat) and DefaultModelBuilderTest
(impl) cover file suppression, user-property suppression, system-property
preservation, POM-declared-property activation, negated-property default-on
preservation, and system-property opt-out of the default-on pattern.
gnodet added a commit that referenced this pull request Sep 17, 2026
…lds (#13158)

* Fix #13084: sandbox profile activation context for external model builds

Backport of #13112 (master) and #13114 (maven-3.10.x) to maven-4.0.x.

Instead of blanket-filtering file and property profiles in external model
builds (dependency POMs, parent POMs, imported BOMs), evaluate them against
a sandboxed ProfileActivationContext that:

 - Merges model properties into system property lookups (so POM-declared
   <properties> drive property-activated profiles in dependency builds).
 - Suppresses user properties (consumer -D flags must not activate
   dependency profiles — they were not set for that artifact).
 - Pre-filters file-activated profiles (returning false from exists() would
   incorrectly activate <missing> profiles).
 - Strips repository contributions from all active external profiles.

Both compat and impl stacks are updated identically. The key insight is that
model properties are merged into system properties inside the sandbox rather
than changing PropertyProfileActivator's lookup chain, to avoid unintended
activation when a project POM declares a property that coincidentally matches
a profile's activation condition.

Tests: ExternalModelProfileActivationTest (compat) and DefaultModelBuilderTest
(impl) cover file suppression, user-property suppression, system-property
preservation, POM-declared-property activation, negated-property default-on
preservation, and system-property opt-out of the default-on pattern.

* Address review: fix mutable map leak in compat sandbox context, pre-filter missing()-condition profiles in CONSUMER_DEPENDENCY builds
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants