-
Notifications
You must be signed in to change notification settings - Fork 17
Add virtual platform constraint plugins for automatic version alignment across group modules #1436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
FinlayRJW
wants to merge
51
commits into
develop
Choose a base branch
from
finlayw/VP
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 15 commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
427f958
nearly working
FinlayRJW 4a69fd3
working but I don't like it
FinlayRJW c4f3931
working for buildScript
FinlayRJW d4039d8
tidy tests
FinlayRJW 86711c1
tidy tests
FinlayRJW 3a4e8de
not working
FinlayRJW 15cb035
fix so can debug
FinlayRJW f8b6eca
check GMM directly
FinlayRJW 9122a6a
read GMM directly
FinlayRJW 32a4b6c
tidy up
FinlayRJW e95fad7
tidy up
FinlayRJW e053db8
tidy up
FinlayRJW 801d0ac
make tests cleaner
FinlayRJW 5ae4125
tidy tests
FinlayRJW 3a4fa16
tidy up
FinlayRJW a608475
add virtual platform
FinlayRJW cb7d2ec
tidy up
FinlayRJW 1ca7286
tidy up
FinlayRJW f827d2c
spotless
FinlayRJW 0ab6ccc
Update src/main/java/com/palantir/gradle/versions/VirtualPlatformSett…
FinlayRJW 896f366
Update src/main/java/com/palantir/gradle/versions/AddVirtualPlatformP…
FinlayRJW 3851352
fix throw
FinlayRJW baeac65
only care about POM
FinlayRJW 232402e
properly defined input factory
FinlayRJW 01f7c5e
tidy up
FinlayRJW 5b9dd60
two transitives
FinlayRJW de77ba0
apply from settings plugn
FinlayRJW 16210df
remove gradle 6 + CC
FinlayRJW 4e632e1
no need for gradle property
FinlayRJW c1ffec2
parse pom
FinlayRJW dc9b80c
explode
FinlayRJW 9990c29
explode
FinlayRJW 543e76b
rename
FinlayRJW 8aaed15
Update src/main/java/com/palantir/gradle/versions/ExternallyConsisten…
FinlayRJW e1d9d97
actually collect all the vps
FinlayRJW 15f27e7
before project
FinlayRJW 6500038
add CC test
FinlayRJW b94709e
create consistent-versions project
FinlayRJW 507778b
move to settings project
FinlayRJW c3d54b1
make private
FinlayRJW 417c3ac
make private
FinlayRJW cb34c21
tidy up
FinlayRJW dbc3948
this is fine
FinlayRJW 0cdf53d
tidy up
FinlayRJW f61f3cc
rename classes
FinlayRJW 3b16fe2
update readme
FinlayRJW 7568021
improve readme
FinlayRJW 0552c0b
cacheable rule!
FinlayRJW df73149
filter to only java
FinlayRJW aa7c874
OBO error
FinlayRJW 73e729d
Merge remote-tracking branch 'origin/develop' into finlayw/VP
FinlayRJW File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
src/main/java/com/palantir/gradle/versions/AddVirtualPlatformPlugin.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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())); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
196 changes: 196 additions & 0 deletions
196
src/main/java/com/palantir/gradle/versions/VirtualPlatformSettingsPlugin.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> { | ||
FinlayRJW marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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); | ||
FinlayRJW marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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); | ||
FinlayRJW marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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(); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.