Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
427f958
nearly working
FinlayRJW Oct 1, 2025
4a69fd3
working but I don't like it
FinlayRJW Oct 1, 2025
c4f3931
working for buildScript
FinlayRJW Oct 1, 2025
d4039d8
tidy tests
FinlayRJW Oct 2, 2025
86711c1
tidy tests
FinlayRJW Oct 2, 2025
3a4e8de
not working
FinlayRJW Oct 2, 2025
15cb035
fix so can debug
FinlayRJW Oct 3, 2025
f8b6eca
check GMM directly
FinlayRJW Oct 3, 2025
9122a6a
read GMM directly
FinlayRJW Oct 3, 2025
32a4b6c
tidy up
FinlayRJW Oct 3, 2025
e95fad7
tidy up
FinlayRJW Oct 3, 2025
e053db8
tidy up
FinlayRJW Oct 6, 2025
801d0ac
make tests cleaner
FinlayRJW Oct 7, 2025
5ae4125
tidy tests
FinlayRJW Oct 7, 2025
3a4fa16
tidy up
FinlayRJW Oct 7, 2025
a608475
add virtual platform
FinlayRJW Oct 7, 2025
cb7d2ec
tidy up
FinlayRJW Oct 7, 2025
1ca7286
tidy up
FinlayRJW Oct 7, 2025
f827d2c
spotless
FinlayRJW Oct 7, 2025
0ab6ccc
Update src/main/java/com/palantir/gradle/versions/VirtualPlatformSett…
FinlayRJW Oct 7, 2025
896f366
Update src/main/java/com/palantir/gradle/versions/AddVirtualPlatformP…
FinlayRJW Oct 7, 2025
3851352
fix throw
FinlayRJW Oct 7, 2025
baeac65
only care about POM
FinlayRJW Oct 7, 2025
232402e
properly defined input factory
FinlayRJW Oct 7, 2025
01f7c5e
tidy up
FinlayRJW Oct 7, 2025
5b9dd60
two transitives
FinlayRJW Oct 7, 2025
de77ba0
apply from settings plugn
FinlayRJW Oct 8, 2025
16210df
remove gradle 6 + CC
FinlayRJW Oct 8, 2025
4e632e1
no need for gradle property
FinlayRJW Oct 8, 2025
c1ffec2
parse pom
FinlayRJW Oct 8, 2025
dc9b80c
explode
FinlayRJW Oct 8, 2025
9990c29
explode
FinlayRJW Oct 8, 2025
543e76b
rename
FinlayRJW Oct 8, 2025
8aaed15
Update src/main/java/com/palantir/gradle/versions/ExternallyConsisten…
FinlayRJW Oct 8, 2025
e1d9d97
actually collect all the vps
FinlayRJW Oct 8, 2025
15f27e7
before project
FinlayRJW Oct 8, 2025
6500038
add CC test
FinlayRJW Oct 8, 2025
b94709e
create consistent-versions project
FinlayRJW Oct 8, 2025
507778b
move to settings project
FinlayRJW Oct 8, 2025
c3d54b1
make private
FinlayRJW Oct 8, 2025
417c3ac
make private
FinlayRJW Oct 8, 2025
cb34c21
tidy up
FinlayRJW Oct 8, 2025
dbc3948
this is fine
FinlayRJW Oct 8, 2025
0cdf53d
tidy up
FinlayRJW Oct 8, 2025
f61f3cc
rename classes
FinlayRJW Oct 8, 2025
3b16fe2
update readme
FinlayRJW Oct 8, 2025
7568021
improve readme
FinlayRJW Oct 8, 2025
0552c0b
cacheable rule!
FinlayRJW Oct 9, 2025
df73149
filter to only java
FinlayRJW Oct 9, 2025
aa7c874
OBO error
FinlayRJW Oct 9, 2025
73e729d
Merge remote-tracking branch 'origin/develop' into finlayw/VP
FinlayRJW Oct 9, 2025
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
15 changes: 15 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ dependencies {
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-guava'
implementation 'one.util:streamex'
implementation 'com.palantir.gradle.idea-configuration:gradle-idea-configuration'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8'

testImplementation platform('org.junit:junit-bom')
testImplementation 'com.netflix.nebula:nebula-test'
Expand Down Expand Up @@ -128,6 +129,20 @@ gradlePlugin {
description = displayName
tags.set(['versions'])
}
addVirtualPlatformPlugin {
id = 'com.palantir.add-virtual-platform-plugin'
implementationClass = 'com.palantir.gradle.versions.AddVirtualPlatformPlugin'
displayName = 'Plugin to apply virtual platform constraints to the Gradle Module Metadata.'
description = displayName
tags.set(['versions'])
}
virtualPlatformSettingsPlugin {
id = 'com.palantir.virtual-platform-settings'
implementationClass = 'com.palantir.gradle.versions.VirtualPlatformSettingsPlugin'
displayName = 'Settings plugin to automatically assign components to virtual platforms'
description = displayName
tags.set(['versions'])
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.gradle.versions;

import java.util.Comparator;
import java.util.Set;
import java.util.stream.Collectors;
import org.gradle.api.NamedDomainObjectProvider;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.plugins.JavaPlugin;

public class AddVirtualPlatformPlugin implements Plugin<Project> {

private static final String PUBLISH_PLATFORM_CONSTRAINTS_PROPERTY =
"com.palantir.gradle.versions.addVirtualPlatformConstraint";

@Override
public final void apply(Project rootProject) {
if (!rootProject.equals(rootProject.getRootProject())) {
throw new IllegalStateException("AddVirtualPlatformPlugin must be applied to the root project");
}

rootProject.afterEvaluate(project -> {
if (shouldPublishPlatformConstraints(project)) {
enforceVersionAlignment(project);
}
});
}

private static boolean shouldPublishPlatformConstraints(Project project) {
return project.hasProperty(PUBLISH_PLATFORM_CONSTRAINTS_PROPERTY)
&& "true".equals(project.property(PUBLISH_PLATFORM_CONSTRAINTS_PROPERTY));
}

private void enforceVersionAlignment(Project rootProject) {
rootProject.getAllprojects().stream()
.filter(VersionsLockPlugin::isJavaLibrary)
.collect(Collectors.groupingBy(p -> String.valueOf(p.getGroup()), Collectors.toSet()))
.values()
.stream()
.filter(group -> group.size() > 1)
.forEach(this::applyVirtualPlatform);
}

private void applyVirtualPlatform(Set<Project> projectGroup) {
Project firstProject = projectGroup.stream()
.min(Comparator.comparing(Project::getPath))
.orElseThrow();

String platformCoordinates = firstProject.getGroup() + ":palantir-virtual-platform";

projectGroup.stream()
.filter(p -> p.getPluginManager().hasPlugin("java"))
.forEach(project -> addVirtualPlatformToProject(project, platformCoordinates));
}

private void addVirtualPlatformToProject(Project project, String platformCoordinates) {
NamedDomainObjectProvider<Configuration> constraints = project.getConfigurations()
.register("virtualPlatformConstraints", config -> {
config.setDescription(
"Enforces version alignment via virtual platform for modules within the same group");
config.setCanBeResolved(false);
config.setCanBeConsumed(false);
config.setVisible(false);

config.getDependencyConstraints()
.add(project.getDependencies().getConstraints().create(platformCoordinates, constraint -> {
constraint.version(
v -> v.require(project.getVersion().toString()));
constraint.because("Virtual platform for version alignment across group");
}));
});

project.getConfigurations()
.named(JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME)
.configure(conf -> conf.extendsFrom(constraints.get()));

project.getConfigurations()
.named(JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME)
.configure(conf -> conf.extendsFrom(constraints.get()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public final void apply(Project project) {
project.getPluginManager().apply(VersionsPropsPlugin.class);
project.getPluginManager().apply(GetVersionPlugin.class);
project.getPluginManager().apply(VersionsPropsIdeaPlugin.class);
project.getPluginManager().apply(AddVirtualPlatformPlugin.class);

project.getPluginManager().apply(IdeaConfigurationPlugin.class);
IdeaConfigurationExtension extension = project.getExtensions().getByType(IdeaConfigurationExtension.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1107,7 +1107,7 @@ private static boolean publishLocalConstraints(Project project) {
&& "true".equals(project.property(PUBLISH_LOCAL_CONSTRAINTS_PROPERTY));
}

private static boolean isJavaLibrary(Project project) {
static boolean isJavaLibrary(Project project) {
if (project.getPluginManager().hasPlugin("nebula.maven-publish")) {
// 'nebula.maven-publish' creates publications lazily which causes inconsistencies based
// on ordering.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.gradle.versions;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import javax.inject.Inject;
import org.gradle.api.Plugin;
import org.gradle.api.artifacts.ComponentMetadataContext;
import org.gradle.api.artifacts.ComponentMetadataRule;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.repositories.RepositoryResourceAccessor;
import org.gradle.api.initialization.Settings;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.immutables.value.Value;

public class VirtualPlatformSettingsPlugin implements Plugin<Settings> {

private static final Logger log = Logging.getLogger(VirtualPlatformSettingsPlugin.class);

@Override
public final void apply(Settings settings) {
settings.getGradle().allprojects(project -> {
project.getDependencies().getComponents().all(VirtualPlatformRule.class);
project.getBuildscript().getDependencies().getComponents().all(VirtualPlatformRule.class);
});
}

public abstract static class VirtualPlatformRule implements ComponentMetadataRule {
private static final String VIRTUAL_PLATFORM_NAME = "palantir-virtual-platform";
private static final ObjectMapper JSON_MAPPER = new ObjectMapper().registerModule(new Jdk8Module());
private static final XmlMapper XML_MAPPER = new XmlMapper();

static {
XML_MAPPER.registerModule(new Jdk8Module());
}

@Inject
protected abstract RepositoryResourceAccessor getRepositoryResourceAccessor();

@Override
public final void execute(ComponentMetadataContext context) {
ModuleVersionIdentifier id = context.getDetails().getId();

// Try GMM first
String gmmPath = buildGmmPath(id);
getRepositoryResourceAccessor().withResource(gmmPath, resource -> {
parseMetadata(resource, JSON_MAPPER, GradleModuleMetadata.class)
.flatMap(GradleModuleMetadata::extractDependencies)
.filter(deps -> hasVirtualPlatform(deps, id.getGroup()))
.ifPresent(deps -> assignToPlatform(context, id));
});

// If GMM didn't work, try POM
String pomPath = buildPomPath(id);
getRepositoryResourceAccessor().withResource(pomPath, resource -> {
parseMetadata(resource, XML_MAPPER, PomMetadata.class)
.flatMap(PomMetadata::extractDependencies)
.filter(deps -> hasVirtualPlatform(deps, id.getGroup()))
.ifPresent(deps -> assignToPlatform(context, id));
});
}

private static <T> Optional<T> parseMetadata(
InputStream resource, ObjectMapper mapper, Class<T> metadataClass) {
try {
return Optional.of(mapper.readValue(resource, metadataClass));
} catch (IOException e) {
log.debug("Failed to parse {} metadata: {}", metadataClass.getSimpleName(), e.getMessage());
return Optional.empty();
}
}

private static boolean hasVirtualPlatform(Stream<Dependency> dependencies, String expectedGroup) {
return dependencies.anyMatch(dep -> isVirtualPlatformDependency(dep, expectedGroup));
}

private static boolean isVirtualPlatformDependency(Dependency dependency, String expectedGroup) {
return dependency.group().filter(expectedGroup::equals).isPresent()
&& dependency.module().filter(VIRTUAL_PLATFORM_NAME::equals).isPresent();
}

private static String buildGmmPath(ModuleVersionIdentifier id) {
String groupPath = id.getGroup().replace('.', '/');
return String.format(
"%s/%s/%s/%s-%s.module", groupPath, id.getName(), id.getVersion(), id.getName(), id.getVersion());
}

private static String buildPomPath(ModuleVersionIdentifier id) {
String groupPath = id.getGroup().replace('.', '/');
return String.format(
"%s/%s/%s/%s-%s.pom", groupPath, id.getName(), id.getVersion(), id.getName(), id.getVersion());
}

private static void assignToPlatform(ComponentMetadataContext context, ModuleVersionIdentifier id) {
String platformNotation = id.getGroup() + ":_:" + id.getVersion();
log.info("Assigning component {} to virtual platform {}", id, platformNotation);
context.getDetails().belongsTo(platformNotation, true);
}
}

interface MetadataWithDependencies {
Optional<Stream<Dependency>> extractDependencies();
}

@Value.Immutable
@JsonDeserialize(as = ImmutableDependency.class)
@JsonIgnoreProperties(ignoreUnknown = true)
interface Dependency {
@JsonProperty("group")
@JacksonXmlProperty(localName = "groupId")
Optional<String> group();

@JsonProperty("module")
@JacksonXmlProperty(localName = "artifactId")
Optional<String> module();
}

// GMM Models
@Value.Immutable
@JsonDeserialize(as = ImmutableGradleModuleMetadata.class)
@JsonIgnoreProperties(ignoreUnknown = true)
interface GradleModuleMetadata extends MetadataWithDependencies {
@Value.Default
default List<Variant> variants() {
return List.of();
}

@Override
default Optional<Stream<Dependency>> extractDependencies() {
Stream<Dependency> deps = variants().stream().flatMap(variant -> variant.dependencyConstraints().stream());
return Optional.of(deps);
}
}

@Value.Immutable
@JsonDeserialize(as = ImmutableVariant.class)
@JsonIgnoreProperties(ignoreUnknown = true)
interface Variant {
@Value.Default
default List<Dependency> dependencyConstraints() {
return List.of();
}
}

// POM Models
@Value.Immutable
@JsonDeserialize(as = ImmutablePomMetadata.class)
@JsonIgnoreProperties(ignoreUnknown = true)
interface PomMetadata extends MetadataWithDependencies {
Optional<DependencyManagement> dependencyManagement();

@Override
default Optional<Stream<Dependency>> extractDependencies() {
return dependencyManagement().map(dm -> dm.dependencies().stream());
}
}

@Value.Immutable
@JsonDeserialize(as = ImmutableDependencyManagement.class)
@JsonIgnoreProperties(ignoreUnknown = true)
interface DependencyManagement {
@JacksonXmlElementWrapper(localName = "dependencies")
@JacksonXmlProperty(localName = "dependency")
@Value.Default
default List<Dependency> dependencies() {
return List.of();
}
}
}
Loading