Skip to content
Merged
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 @@ -144,9 +144,238 @@ protected Model buildPom(RepositorySystemSession session, MavenProject project,
throws ModelBuilderException {
ModelBuilderResult result = buildModel(session, project, src);
Model model = result.getRawModel();
Model effectiveModel = result.getEffectiveModel();
model = interpolatePomVersions(model, effectiveModel);
return transformPom(model, project);
}

/**
* Interpolates version references in the raw model's dependencies and dependency
* management that cannot be resolved by downstream consumers through the POM's
* own {@code <properties>} section or parent chain.
* <p>
* The {@code buildPom()} path uses the raw model to preserve the parent reference
* in the consumer POM. Properties defined in the POM or inherited from the parent
* chain are available to consumers and should be left as {@code ${...}} references.
* However, properties contributed by Maven extensions (via {@code PropertyContributor}
* SPI) exist only as user properties during the build session — they are not part of
* any POM's {@code <properties>} section or parent chain. Without this interpolation
* step, such references (e.g. {@code ${nisse.jgit.dynamicVersion}}) would remain
* unresolved in the installed/deployed consumer POM, making it unusable.
* <p>
* The effective model's {@code getProperties()} returns exactly the properties from the
* POM and parent chain (the model builder's {@code merge()} method only overrides
* existing model properties — it never adds new user-property keys). A version
* reference is interpolated only when it contains at least one property name that
* is absent from that set, meaning a downstream consumer would be unable to resolve
* it.
*
* @param rawModel the raw model (no inheritance, no interpolation)
* @param effectiveModel the effective model (inheritance + full interpolation)
* @return the raw model with non-resolvable version references resolved
* @see <a href="https://github.com/apache/maven/issues/12981">GH-12981</a>
*/
static Model interpolatePomVersions(Model rawModel, Model effectiveModel) {
// Build the set of properties available to downstream consumers.
// effectiveModel.getProperties() contains properties from the POM and parent chain.
// However, BUILD_CONSUMER does not re-activate profiles, and the model builder's
// merge() only overrides existing keys — it never adds new user-property keys.
// So profile-defined properties (e.g. from <activeByDefault> profiles) may be
// absent from the effective model's properties map even though transformPom()
// preserves those profiles in the consumer POM and consumers CAN resolve them.
// We must include properties from ALL profiles in the raw model to avoid
// incorrectly interpolating resolvable references like ${junit.version}.
Map<String, String> consumerProperties = new LinkedHashMap<>(effectiveModel.getProperties());
for (Profile profile : rawModel.getProfiles()) {
consumerProperties.putAll(profile.getProperties());
}
Map<String, String> modelProperties = consumerProperties;

// Build lookups from the effective model's resolved dependency entries
Map<String, Dependency> effectiveManagedDeps = new LinkedHashMap<>();
if (effectiveModel.getDependencyManagement() != null) {
for (Dependency dep : effectiveModel.getDependencyManagement().getDependencies()) {
effectiveManagedDeps.put(getDependencyKey(dep), dep);
}
}
Map<String, Dependency> effectiveDeps = new LinkedHashMap<>();
for (Dependency dep : effectiveModel.getDependencies()) {
effectiveDeps.put(getDependencyKey(dep), dep);
}

// Interpolate dependency management versions
if (rawModel.getDependencyManagement() != null
&& !rawModel.getDependencyManagement().getDependencies().isEmpty()) {
List<Dependency> interpolatedDeps = new ArrayList<>();
boolean dmChanged = false;
for (Dependency dep : rawModel.getDependencyManagement().getDependencies()) {
if (dep.getVersion() != null
&& dep.getVersion().contains("${")
&& hasNonModelProperties(dep.getVersion(), modelProperties)) {
String key = getDependencyKey(dep);
Dependency effectiveDep = effectiveManagedDeps.get(key);
if (effectiveDep != null && !effectiveDep.getVersion().contains("${")) {
dep = dep.withVersion(effectiveDep.getVersion());
dmChanged = true;
}
}
interpolatedDeps.add(dep);
}
if (dmChanged) {
rawModel = rawModel.withDependencyManagement(
rawModel.getDependencyManagement().withDependencies(interpolatedDeps));
}
}

// Interpolate direct dependency versions
if (!rawModel.getDependencies().isEmpty()) {
List<Dependency> interpolatedDeps = new ArrayList<>();
boolean depsChanged = false;
for (Dependency dep : rawModel.getDependencies()) {
if (dep.getVersion() != null
&& dep.getVersion().contains("${")
&& hasNonModelProperties(dep.getVersion(), modelProperties)) {
String key = getDependencyKey(dep);
Dependency effectiveDep = effectiveDeps.get(key);
if (effectiveDep != null && !effectiveDep.getVersion().contains("${")) {
dep = dep.withVersion(effectiveDep.getVersion());
depsChanged = true;
}
}
interpolatedDeps.add(dep);
}
if (depsChanged) {
rawModel = rawModel.withDependencies(interpolatedDeps);
}
}

// Interpolate profile-level dependency and dependency management versions.
// Profiles are preserved in the consumer POM (transformPom keeps them), but
// extension-contributed property references in profile dependencies would remain
// unresolvable for downstream consumers. We use the same effectiveDeps /
// effectiveManagedDeps lookups because the effective model merges active profile
// deps into the main sections — giving us the resolved values we need.
if (!rawModel.getProfiles().isEmpty()) {
List<Profile> updatedProfiles = new ArrayList<>();
boolean profilesChanged = false;
for (Profile profile : rawModel.getProfiles()) {
Profile updatedProfile = profile;

// Interpolate profile dependency management
if (profile.getDependencyManagement() != null
&& !profile.getDependencyManagement().getDependencies().isEmpty()) {
List<Dependency> interpolatedDeps = new ArrayList<>();
boolean dmChanged = false;
for (Dependency dep : profile.getDependencyManagement().getDependencies()) {
if (dep.getVersion() != null
&& dep.getVersion().contains("${")
&& hasNonModelProperties(dep.getVersion(), modelProperties)) {
String key = getDependencyKey(dep);
Dependency effectiveDep = effectiveManagedDeps.get(key);
if (effectiveDep != null
&& !effectiveDep.getVersion().contains("${")) {
dep = dep.withVersion(effectiveDep.getVersion());
dmChanged = true;
}
}
interpolatedDeps.add(dep);
}
if (dmChanged) {
updatedProfile = updatedProfile.withDependencyManagement(
updatedProfile.getDependencyManagement().withDependencies(interpolatedDeps));
profilesChanged = true;
}
}

// Interpolate profile direct dependencies
if (!profile.getDependencies().isEmpty()) {
List<Dependency> interpolatedDeps = new ArrayList<>();
boolean depsChanged = false;
for (Dependency dep : profile.getDependencies()) {
if (dep.getVersion() != null
&& dep.getVersion().contains("${")
&& hasNonModelProperties(dep.getVersion(), modelProperties)) {
String key = getDependencyKey(dep);
Dependency effectiveDep = effectiveDeps.get(key);
if (effectiveDep != null
&& !effectiveDep.getVersion().contains("${")) {
dep = dep.withVersion(effectiveDep.getVersion());
depsChanged = true;
}
}
interpolatedDeps.add(dep);
}
if (depsChanged) {
updatedProfile = updatedProfile.withDependencies(interpolatedDeps);
profilesChanged = true;
}
}

updatedProfiles.add(updatedProfile);
}
if (profilesChanged) {
rawModel = rawModel.withProfiles(updatedProfiles);
}
}

return rawModel;
}

/**
* Returns {@code true} when the given version string contains at least one
* {@code ${name}} reference whose {@code name} is <em>not</em> a key in the
* supplied model properties and is not a well-known built-in property
* (e.g. {@code project.version}, {@code project.groupId}).
* <p>
* Such references originate from extension-contributed user properties
* (e.g. {@code PropertyContributor}) and would be unresolvable by downstream
* consumers that do not have the extension installed.
*/
static boolean hasNonModelProperties(String version, Map<String, String> modelProperties) {
int start = 0;
while ((start = version.indexOf("${", start)) >= 0) {
int end = version.indexOf('}', start + 2);
if (end < 0) {
break;
}
String name = version.substring(start + 2, end);
if (!modelProperties.containsKey(name) && !isBuiltInProperty(name)) {
return true;
}
start = end + 1;
}
return false;
}

/**
* Returns {@code true} when the given property name is a Maven built-in that
* downstream consumers can always resolve without any additional POM properties.
* <p>
* The intentionally scoped set of recognized prefixes is:
* <ul>
* <li>{@code project.*} — standard project coordinate properties
* (e.g. {@code project.version}, {@code project.groupId})</li>
* <li>{@code pom.*} — legacy alias for {@code project.*}</li>
* <li>{@code version} — bare alias for {@code project.version}</li>
* <li>{@code env.*} — environment variables (e.g. {@code env.PATH});
* always available from the consumer's runtime environment</li>
* <li>{@code settings.*} — Maven settings properties
* (e.g. {@code settings.localRepository}); always available from
* the consumer's Maven installation</li>
* </ul>
* Properties contributed by Maven extensions via {@code PropertyContributor}
* are NOT built-ins — they must be interpolated before the consumer POM is
* published, because consumers that do not have the extension installed cannot
* resolve them.
*/
private static boolean isBuiltInProperty(String name) {
return name.startsWith("project.")
|| name.startsWith("pom.")
|| name.equals("version")
|| name.startsWith("env.")
|| name.startsWith("settings.");
}

protected Model buildBom(RepositorySystemSession session, MavenProject project, ModelSource src)
throws ModelBuilderException {
ModelBuilderResult result = buildModel(session, project, src);
Expand Down
Loading
Loading