Skip to content

Commit 02de10f

Browse files
committed
Fix CI-friendly version processing with profile properties (fix #11196)
Changes to \ in profiles do not propagate to the final project version. This issue occurs because CI-friendly version processing happens before profile activation, so profile properties are not available during version resolution. This commit implements enhanced property resolution that performs lightweight profile activation during CI-friendly version processing to ensure profile properties are available for both version resolution and repository URL interpolation. Key changes: - Enhanced CI-friendly version processing with profile-aware property resolution - Unified property resolution for both CI-friendly versions and repository URLs - Added directory properties (basedir, rootDirectory) to profile activation context - Comprehensive test coverage for profile-based CI-friendly versions The solution maintains full backward compatibility while enabling profile-based version manipulation that was possible in Maven 3 but broken in Maven 4. Fixes #11196
1 parent 4b7a1df commit 02de10f

File tree

7 files changed

+513
-31
lines changed

7 files changed

+513
-31
lines changed

impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java

Lines changed: 94 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,93 @@ String replaceCiFriendlyVersion(Map<String, String> properties, String version)
626626
return version != null ? interpolator.interpolate(version, properties::get) : null;
627627
}
628628

629+
/**
630+
* Get enhanced properties that include profile-aware property resolution.
631+
* This method activates profiles to ensure that properties defined in profiles
632+
* are available for CI-friendly version processing and repository URL interpolation.
633+
* It also includes directory-related properties that may be needed during profile activation.
634+
*/
635+
private Map<String, String> getEnhancedProperties(Model model, Path rootDirectory) {
636+
Map<String, String> properties = new HashMap<>();
637+
638+
// Add directory-specific properties first, as they may be needed for profile activation
639+
if (model.getProjectDirectory() != null) {
640+
String basedir = model.getProjectDirectory().toString();
641+
String basedirUri = model.getProjectDirectory().toUri().toString();
642+
properties.put("basedir", basedir);
643+
properties.put("project.basedir", basedir);
644+
properties.put("project.basedir.uri", basedirUri);
645+
}
646+
try {
647+
String root = rootDirectory.toString();
648+
String rootUri = rootDirectory.toUri().toString();
649+
properties.put("project.rootDirectory", root);
650+
properties.put("project.rootDirectory.uri", rootUri);
651+
} catch (IllegalStateException e) {
652+
// Root directory not available, continue without it
653+
}
654+
655+
// Handle root vs non-root project properties with profile activation
656+
if (!Objects.equals(rootDirectory, model.getProjectDirectory())) {
657+
Path rootModelPath = modelProcessor.locateExistingPom(rootDirectory);
658+
if (rootModelPath != null) {
659+
Model rootModel = derive(Sources.buildSource(rootModelPath)).readFileModel();
660+
properties.putAll(getPropertiesWithProfiles(rootModel, properties));
661+
}
662+
} else {
663+
properties.putAll(getPropertiesWithProfiles(model, properties));
664+
}
665+
666+
return properties;
667+
}
668+
669+
/**
670+
* Get properties from a model including properties from activated profiles.
671+
* This performs lightweight profile activation to merge profile properties.
672+
*
673+
* @param model the model to get properties from
674+
* @param baseProperties base properties (including directory properties) to include in profile activation context
675+
*/
676+
private Map<String, String> getPropertiesWithProfiles(Model model, Map<String, String> baseProperties) {
677+
Map<String, String> properties = new HashMap<>();
678+
679+
// Start with base properties (including directory properties)
680+
properties.putAll(baseProperties);
681+
682+
// Add model properties
683+
properties.putAll(model.getProperties());
684+
685+
try {
686+
// Create a profile activation context for this model with base properties available
687+
DefaultProfileActivationContext profileContext = getProfileActivationContext(request, model);
688+
689+
// Activate profiles and merge their properties
690+
List<Profile> activeProfiles = getActiveProfiles(model.getProfiles(), profileContext);
691+
692+
for (Profile profile : activeProfiles) {
693+
properties.putAll(profile.getProperties());
694+
}
695+
} catch (Exception e) {
696+
// If profile activation fails, log a warning but continue with base properties
697+
// This ensures that CI-friendly versions still work even if profile activation has issues
698+
logger.warn("Failed to activate profiles for CI-friendly version processing: {}", e.getMessage());
699+
logger.debug("Profile activation failure details", e);
700+
}
701+
702+
// User properties override everything
703+
properties.putAll(session.getEffectiveProperties());
704+
705+
return properties;
706+
}
707+
708+
/**
709+
* Convenience method for getting properties with profiles without additional base properties.
710+
* This is a backward compatibility method that provides an empty base properties map.
711+
*/
712+
private Map<String, String> getPropertiesWithProfiles(Model model) {
713+
return getPropertiesWithProfiles(model, new HashMap<>());
714+
}
715+
629716
private void buildBuildPom() throws ModelBuilderException {
630717
// Retrieve and normalize the source path, ensuring it's non-null and in absolute form
631718
Path top = request.getSource().getPath();
@@ -1501,21 +1588,11 @@ Model doReadFileModel() throws ModelBuilderException {
15011588
}
15021589
}
15031590

1504-
// CI friendly version
1505-
// All expressions are interpolated using user properties and properties
1506-
// defined on the root project.
1507-
Map<String, String> properties = new HashMap<>();
1508-
if (!Objects.equals(rootDirectory, model.getProjectDirectory())) {
1509-
Path rootModelPath = modelProcessor.locateExistingPom(rootDirectory);
1510-
if (rootModelPath != null) {
1511-
Model rootModel =
1512-
derive(Sources.buildSource(rootModelPath)).readFileModel();
1513-
properties.putAll(rootModel.getProperties());
1514-
}
1515-
} else {
1516-
properties.putAll(model.getProperties());
1517-
}
1518-
properties.putAll(session.getEffectiveProperties());
1591+
// Enhanced property resolution with profile activation for CI-friendly versions and repository URLs
1592+
// This includes directory properties, profile properties, and user properties
1593+
Map<String, String> properties = getEnhancedProperties(model, rootDirectory);
1594+
1595+
// CI friendly version processing with profile-aware properties
15191596
model = model.with()
15201597
.version(replaceCiFriendlyVersion(properties, model.getVersion()))
15211598
.parent(
@@ -1526,22 +1603,8 @@ Model doReadFileModel() throws ModelBuilderException {
15261603
model.getParent().getVersion()))
15271604
: null)
15281605
.build();
1529-
// Interpolate repository URLs
1530-
if (model.getProjectDirectory() != null) {
1531-
String basedir = model.getProjectDirectory().toString();
1532-
String basedirUri = model.getProjectDirectory().toUri().toString();
1533-
properties.put("basedir", basedir);
1534-
properties.put("project.basedir", basedir);
1535-
properties.put("project.basedir.uri", basedirUri);
1536-
}
1537-
try {
1538-
String root = request.getSession().getRootDirectory().toString();
1539-
String rootUri =
1540-
request.getSession().getRootDirectory().toUri().toString();
1541-
properties.put("project.rootDirectory", root);
1542-
properties.put("project.rootDirectory.uri", rootUri);
1543-
} catch (IllegalStateException e) {
1544-
}
1606+
1607+
// Repository URL interpolation with the same profile-aware properties
15451608
UnaryOperator<String> callback = properties::get;
15461609
model = model.with()
15471610
.repositories(interpolateRepository(model.getRepositories(), callback))

impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,94 @@ public void testMergeRepositories() throws Exception {
122122
assertEquals("central", repositories.get(3).getId()); // default
123123
}
124124

125+
@Test
126+
public void testCiFriendlyVersionWithProfiles() {
127+
// Test case 1: Default profile should set revision to baseVersion+dev
128+
ModelBuilderRequest request = ModelBuilderRequest.builder()
129+
.session(session)
130+
.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
131+
.source(Sources.buildSource(getPom("ci-friendly-profiles")))
132+
.build();
133+
ModelBuilderResult result = builder.newSession().build(request);
134+
assertNotNull(result);
135+
assertEquals("0.2.0+dev", result.getEffectiveModel().getVersion());
136+
137+
// Test case 2: Release profile should set revision to baseVersion only
138+
request = ModelBuilderRequest.builder()
139+
.session(session)
140+
.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
141+
.source(Sources.buildSource(getPom("ci-friendly-profiles")))
142+
.activeProfileIds(List.of("releaseBuild"))
143+
.build();
144+
result = builder.newSession().build(request);
145+
assertNotNull(result);
146+
assertEquals("0.2.0", result.getEffectiveModel().getVersion());
147+
}
148+
149+
@Test
150+
public void testRepositoryUrlInterpolationWithProfiles() {
151+
// Test case 1: Default properties should be used
152+
ModelBuilderRequest request = ModelBuilderRequest.builder()
153+
.session(session)
154+
.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
155+
.source(Sources.buildSource(getPom("repository-url-profiles")))
156+
.build();
157+
ModelBuilderResult result = builder.newSession().build(request);
158+
assertNotNull(result);
159+
assertEquals(
160+
"http://default.repo.com/repository/maven-public/",
161+
result.getEffectiveModel().getRepositories().get(0).getUrl());
162+
163+
// Test case 2: Development profile should override repository URL
164+
request = ModelBuilderRequest.builder()
165+
.session(session)
166+
.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
167+
.source(Sources.buildSource(getPom("repository-url-profiles")))
168+
.activeProfileIds(List.of("development"))
169+
.build();
170+
result = builder.newSession().build(request);
171+
assertNotNull(result);
172+
assertEquals(
173+
"http://dev.repo.com/repository/maven-public/",
174+
result.getEffectiveModel().getRepositories().get(0).getUrl());
175+
176+
// Test case 3: Production profile should override repository URL
177+
request = ModelBuilderRequest.builder()
178+
.session(session)
179+
.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
180+
.source(Sources.buildSource(getPom("repository-url-profiles")))
181+
.activeProfileIds(List.of("production"))
182+
.build();
183+
result = builder.newSession().build(request);
184+
assertNotNull(result);
185+
assertEquals(
186+
"http://prod.repo.com/repository/maven-public/",
187+
result.getEffectiveModel().getRepositories().get(0).getUrl());
188+
}
189+
190+
@Test
191+
public void testDirectoryPropertiesInProfilesAndRepositories() {
192+
// Test that directory properties (like ${project.basedir}) are available
193+
// during profile activation and repository URL interpolation
194+
ModelBuilderRequest request = ModelBuilderRequest.builder()
195+
.session(session)
196+
.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
197+
.source(Sources.buildSource(getPom("directory-properties-profiles")))
198+
.activeProfileIds(List.of("local-repo"))
199+
.build();
200+
ModelBuilderResult result = builder.newSession().build(request);
201+
assertNotNull(result);
202+
203+
// Verify CI-friendly version was resolved with profile properties
204+
assertEquals("1.0.0-LOCAL", result.getEffectiveModel().getVersion());
205+
206+
// Verify repository URL was interpolated with directory properties from profile
207+
String expectedUrl =
208+
"file://" + getPom("directory-properties-profiles").getParent().toString() + "/local-repo";
209+
assertEquals(
210+
expectedUrl, result.getEffectiveModel().getRepositories().get(0).getUrl());
211+
}
212+
125213
private Path getPom(String name) {
126214
return Paths.get("src/test/resources/poms/factory/" + name + ".xml").toAbsolutePath();
127215
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!---
3+
Licensed to the Apache Software Foundation (ASF) under one or more
4+
contributor license agreements. See the NOTICE file distributed with
5+
this work for additional information regarding copyright ownership.
6+
The ASF licenses this file to You under the Apache License, Version 2.0
7+
(the "License"); you may not use this file except in compliance with
8+
the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
-->
18+
<project xmlns="http://maven.apache.org/POM/4.1.0"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd"
21+
root="true">
22+
<modelVersion>4.1.0</modelVersion>
23+
24+
<groupId>org.apache.maven.test</groupId>
25+
<artifactId>ci-friendly-profiles-test</artifactId>
26+
<version>${revision}</version>
27+
<packaging>pom</packaging>
28+
29+
<properties>
30+
<baseVersion>0.2.0</baseVersion>
31+
<revision>${baseVersion}+dev</revision>
32+
</properties>
33+
34+
<profiles>
35+
<profile>
36+
<id>releaseBuild</id>
37+
<properties>
38+
<revision>${baseVersion}</revision>
39+
</properties>
40+
</profile>
41+
</profiles>
42+
43+
</project>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!---
3+
Licensed to the Apache Software Foundation (ASF) under one or more
4+
contributor license agreements. See the NOTICE file distributed with
5+
this work for additional information regarding copyright ownership.
6+
The ASF licenses this file to You under the Apache License, Version 2.0
7+
(the "License"); you may not use this file except in compliance with
8+
the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
-->
18+
<project xmlns="http://maven.apache.org/POM/4.1.0"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd"
21+
root="true">
22+
<modelVersion>4.1.0</modelVersion>
23+
24+
<groupId>org.apache.maven.test</groupId>
25+
<artifactId>directory-properties-profiles-test</artifactId>
26+
<version>${revision}</version>
27+
<packaging>pom</packaging>
28+
29+
<properties>
30+
<baseVersion>1.0.0</baseVersion>
31+
<revision>${baseVersion}-SNAPSHOT</revision>
32+
<repo.url>http://default.repo.com</repo.url>
33+
</properties>
34+
35+
<profiles>
36+
<!-- Profile that uses directory properties in repository URL -->
37+
<profile>
38+
<id>local-repo</id>
39+
<properties>
40+
<revision>${baseVersion}-LOCAL</revision>
41+
<repo.url>file://${project.basedir}/local-repo</repo.url>
42+
</properties>
43+
</profile>
44+
</profiles>
45+
46+
<repositories>
47+
<repository>
48+
<id>test-repo</id>
49+
<url>${repo.url}</url>
50+
</repository>
51+
</repositories>
52+
53+
</project>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!---
3+
Licensed to the Apache Software Foundation (ASF) under one or more
4+
contributor license agreements. See the NOTICE file distributed with
5+
this work for additional information regarding copyright ownership.
6+
The ASF licenses this file to You under the Apache License, Version 2.0
7+
(the "License"); you may not use this file except in compliance with
8+
the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
-->
18+
<project xmlns="http://maven.apache.org/POM/4.1.0"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd"
21+
root="true">
22+
<modelVersion>4.1.0</modelVersion>
23+
24+
<groupId>org.apache.maven.test</groupId>
25+
<artifactId>repository-url-profiles-test</artifactId>
26+
<version>1.0-SNAPSHOT</version>
27+
<packaging>pom</packaging>
28+
29+
<properties>
30+
<repo.base.url>http://default.repo.com</repo.base.url>
31+
</properties>
32+
33+
<profiles>
34+
<profile>
35+
<id>development</id>
36+
<properties>
37+
<repo.base.url>http://dev.repo.com</repo.base.url>
38+
</properties>
39+
</profile>
40+
41+
<profile>
42+
<id>production</id>
43+
<properties>
44+
<repo.base.url>http://prod.repo.com</repo.base.url>
45+
</properties>
46+
</profile>
47+
</profiles>
48+
49+
<repositories>
50+
<repository>
51+
<id>company-repo</id>
52+
<url>${repo.base.url}/repository/maven-public/</url>
53+
</repository>
54+
</repositories>
55+
56+
</project>

0 commit comments

Comments
 (0)