Skip to content

Commit 0feb6f8

Browse files
authored
Fix CI-friendly version processing with profile properties (fix apache#11196) (apache#11225)
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 apache#11196 (cherry picked from commit 02de10f)
1 parent 897a8b6 commit 0feb6f8

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
@@ -598,6 +598,93 @@ String replaceCiFriendlyVersion(Map<String, String> properties, String version)
598598
return version != null ? interpolator.interpolate(version, properties::get) : null;
599599
}
600600

601+
/**
602+
* Get enhanced properties that include profile-aware property resolution.
603+
* This method activates profiles to ensure that properties defined in profiles
604+
* are available for CI-friendly version processing and repository URL interpolation.
605+
* It also includes directory-related properties that may be needed during profile activation.
606+
*/
607+
private Map<String, String> getEnhancedProperties(Model model, Path rootDirectory) {
608+
Map<String, String> properties = new HashMap<>();
609+
610+
// Add directory-specific properties first, as they may be needed for profile activation
611+
if (model.getProjectDirectory() != null) {
612+
String basedir = model.getProjectDirectory().toString();
613+
String basedirUri = model.getProjectDirectory().toUri().toString();
614+
properties.put("basedir", basedir);
615+
properties.put("project.basedir", basedir);
616+
properties.put("project.basedir.uri", basedirUri);
617+
}
618+
try {
619+
String root = rootDirectory.toString();
620+
String rootUri = rootDirectory.toUri().toString();
621+
properties.put("project.rootDirectory", root);
622+
properties.put("project.rootDirectory.uri", rootUri);
623+
} catch (IllegalStateException e) {
624+
// Root directory not available, continue without it
625+
}
626+
627+
// Handle root vs non-root project properties with profile activation
628+
if (!Objects.equals(rootDirectory, model.getProjectDirectory())) {
629+
Path rootModelPath = modelProcessor.locateExistingPom(rootDirectory);
630+
if (rootModelPath != null) {
631+
Model rootModel = derive(Sources.buildSource(rootModelPath)).readFileModel();
632+
properties.putAll(getPropertiesWithProfiles(rootModel, properties));
633+
}
634+
} else {
635+
properties.putAll(getPropertiesWithProfiles(model, properties));
636+
}
637+
638+
return properties;
639+
}
640+
641+
/**
642+
* Get properties from a model including properties from activated profiles.
643+
* This performs lightweight profile activation to merge profile properties.
644+
*
645+
* @param model the model to get properties from
646+
* @param baseProperties base properties (including directory properties) to include in profile activation context
647+
*/
648+
private Map<String, String> getPropertiesWithProfiles(Model model, Map<String, String> baseProperties) {
649+
Map<String, String> properties = new HashMap<>();
650+
651+
// Start with base properties (including directory properties)
652+
properties.putAll(baseProperties);
653+
654+
// Add model properties
655+
properties.putAll(model.getProperties());
656+
657+
try {
658+
// Create a profile activation context for this model with base properties available
659+
DefaultProfileActivationContext profileContext = getProfileActivationContext(request, model);
660+
661+
// Activate profiles and merge their properties
662+
List<Profile> activeProfiles = getActiveProfiles(model.getProfiles(), profileContext);
663+
664+
for (Profile profile : activeProfiles) {
665+
properties.putAll(profile.getProperties());
666+
}
667+
} catch (Exception e) {
668+
// If profile activation fails, log a warning but continue with base properties
669+
// This ensures that CI-friendly versions still work even if profile activation has issues
670+
logger.warn("Failed to activate profiles for CI-friendly version processing: {}", e.getMessage());
671+
logger.debug("Profile activation failure details", e);
672+
}
673+
674+
// User properties override everything
675+
properties.putAll(session.getEffectiveProperties());
676+
677+
return properties;
678+
}
679+
680+
/**
681+
* Convenience method for getting properties with profiles without additional base properties.
682+
* This is a backward compatibility method that provides an empty base properties map.
683+
*/
684+
private Map<String, String> getPropertiesWithProfiles(Model model) {
685+
return getPropertiesWithProfiles(model, new HashMap<>());
686+
}
687+
601688
private void buildBuildPom() throws ModelBuilderException {
602689
// Retrieve and normalize the source path, ensuring it's non-null and in absolute form
603690
Path top = request.getSource().getPath();
@@ -1394,21 +1481,11 @@ Model doReadFileModel() throws ModelBuilderException {
13941481
}
13951482
}
13961483

1397-
// CI friendly version
1398-
// All expressions are interpolated using user properties and properties
1399-
// defined on the root project.
1400-
Map<String, String> properties = new HashMap<>();
1401-
if (!Objects.equals(rootDirectory, model.getProjectDirectory())) {
1402-
Path rootModelPath = modelProcessor.locateExistingPom(rootDirectory);
1403-
if (rootModelPath != null) {
1404-
Model rootModel =
1405-
derive(Sources.buildSource(rootModelPath)).readFileModel();
1406-
properties.putAll(rootModel.getProperties());
1407-
}
1408-
} else {
1409-
properties.putAll(model.getProperties());
1410-
}
1411-
properties.putAll(session.getEffectiveProperties());
1484+
// Enhanced property resolution with profile activation for CI-friendly versions and repository URLs
1485+
// This includes directory properties, profile properties, and user properties
1486+
Map<String, String> properties = getEnhancedProperties(model, rootDirectory);
1487+
1488+
// CI friendly version processing with profile-aware properties
14121489
model = model.with()
14131490
.version(replaceCiFriendlyVersion(properties, model.getVersion()))
14141491
.parent(
@@ -1419,22 +1496,8 @@ Model doReadFileModel() throws ModelBuilderException {
14191496
model.getParent().getVersion()))
14201497
: null)
14211498
.build();
1422-
// Interpolate repository URLs
1423-
if (model.getProjectDirectory() != null) {
1424-
String basedir = model.getProjectDirectory().toString();
1425-
String basedirUri = model.getProjectDirectory().toUri().toString();
1426-
properties.put("basedir", basedir);
1427-
properties.put("project.basedir", basedir);
1428-
properties.put("project.basedir.uri", basedirUri);
1429-
}
1430-
try {
1431-
String root = request.getSession().getRootDirectory().toString();
1432-
String rootUri =
1433-
request.getSession().getRootDirectory().toUri().toString();
1434-
properties.put("project.rootDirectory", root);
1435-
properties.put("project.rootDirectory.uri", rootUri);
1436-
} catch (IllegalStateException e) {
1437-
}
1499+
1500+
// Repository URL interpolation with the same profile-aware properties
14381501
UnaryOperator<String> callback = properties::get;
14391502
model = model.with()
14401503
.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)