Skip to content

Commit 85f96cc

Browse files
authored
[Gradle] Configure transitive dependencies via ComponentMetadataRules (#134169) (#134710)
This introduces ComponentMetadataRulesPlugin that contains declarative logic for dealing with transitive dependencies on a per dependency level. Ulitmately we want more finegrained control over our dependencies without loosing information about transitive dependencies. The initial list of the applied component metadata rules will be more finegrained over time. Initially this is mostly a reflection of how we brought in dependencies before by basically making the transitive dependencies we identified as required where added as direct dependency. I started looking through the existing dependencies applyging the following pattern: if no problematic transitive dependency detected, do not apply any component meta data rule. if only non group dependencies have been problematic, use ExcludeOtherGroupsTransitiveRule which allows transitive dependencies brought with the same groupId as the parent but excludes all others. Otherwise exclude all transitive dependencies by applying ExcludeAllTransitivesRule We will add more specific rules in the future as we see the need to "fix' component metadata of thirdparty dependencies. This change replaces our plain transitive = false approach for non elasticsearch dependencies Historically we have solved dealing with transitive dependencies and component metadata in this regard by just ignoring it and bringing in dependencies explicitly. This results in weaker control what we bring in and why loose information why a dependency is needed and how its tight ot another dependency on the classpath transitive behavior differed in different context as we only have applied transitivity Furthermore the way we have configured transitive = false for each dependency resulted in other problems when using other newer Gradle APIs like test suites.
1 parent 7612a2e commit 85f96cc

File tree

15 files changed

+813
-133
lines changed

15 files changed

+813
-133
lines changed

BUILDING.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,70 @@ will have the `origin` attribute been set to `Generated by Gradle`.
9797
> If you want to add a level of verification you can manually confirm the checksum (e.g. by looking it up on the website of the library)
9898
> Please replace the content of the `origin` attribute by `official site` in that case.
9999
100+
##### Handling transitive dependencies
101+
102+
Dependency management is a critical aspect of maintaining a secure and reliable build system, requiring explicit control over what we rely on. The Elasticsearch build mainly uses component metadata rules declared in the `ComponentMetadataRulesPlugin`
103+
plugin to manage transitive dependencies and avoid version conflicts.
104+
This approach ensures we have explicit control over all dependencies used in the build.
105+
106+
###### General Guidelines
107+
108+
1. **Avoid unused transitive dependencies** - Dependencies that are not actually used by our code should be excluded to reduce the attack surface and avoid potential conflicts.
109+
110+
2. **Prefer versions declared in `build-tools-internal/version.properties`** - All dependency versions should be centrally managed in this file to ensure consistency across the entire build.
111+
112+
3. **Libraries required to compile our code should be direct dependencies** - If we directly use a library in our source code, it should be declared as a direct dependency rather than relying on it being transitively available.
113+
114+
###### Component Metadata Rules
115+
116+
We use two main types of component metadata rules at this point to manage transitive dependencies:
117+
118+
- **`ExcludeAllTransitivesRule`** - Excludes all transitive dependencies for libraries where we want complete control over dependencies or the transitive dependencies are unused.
119+
120+
- **`ExcludeOtherGroupsTransitiveRule`** - Excludes transitive dependencies that don't belong to the same group as the direct dependency, while keeping same-group dependencies.
121+
-
122+
- **`ExcludeByGroup`** - Excludes transitive dependencies that match a specific groupId while keeping all other transitive dependencies with different groupIds.
123+
124+
Examples from the `ComponentMetadataRulesPlugin`:
125+
126+
```gradle
127+
// Exclude all transitives - used when transitive deps are unused or problematic
128+
components.withModule("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor", ExcludeAllTransitivesRule.class);
129+
130+
// Exclude other groups - used when we want same-group deps but not external ones
131+
components.withModule("com.azure:azure-core", ExcludeOtherGroupsTransitiveRule.class);
132+
133+
// Exclude only specific groups - used when we want exclude specific group of transitive deps.
134+
components.withModule("org.apache.logging.log4j:log4j-api", ExcludeByGroup.class, rule -> {
135+
rule.params(List.of("biz.aQute.bnd", "org.osgi"));
136+
});
137+
```
138+
139+
###### Common Scenarios
140+
141+
**Version Conflicts**: When a transitive dependency brings in a different version than what we use:
142+
```gradle
143+
// brings in jackson-databind and jackson-annotations not used
144+
components.withModule("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor", ExcludeAllTransitivesRule.class);
145+
```
146+
147+
**Unused Dependencies**: When transitive dependencies are not actually used:
148+
```gradle
149+
// brings in azure-core-http-netty. not used
150+
components.withModule("com.azure:azure-core-http-netty", ExcludeAllTransitivesRule.class);
151+
```
152+
153+
**Mismatching Version Dependencies**: When other versions are required:
154+
```gradle
155+
// brings in org.slf4j:slf4j-api:1.7.25. We use 2.0.6
156+
components.withModule("org.apache.directory.api:api-asn1-ber", ExcludeOtherGroupsTransitiveRule.class);
157+
```
158+
159+
When adding or updating dependencies, ensure that any required transitive dependencies are either:
160+
1. Already available as direct dependencies with compatible versions
161+
2. Added as direct dependencies if they're actually used by our code
162+
3. Properly excluded if they're not needed
163+
100164
#### Custom plugin and task implementations
101165

102166
Build logic that is used across multiple subprojects should be considered to be moved into a Gradle plugin with according Gradle task implementation.

build-conventions/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ repositories {
7272
}
7373

7474
dependencies {
75+
constraints {
76+
api("org.slf4j:slf4j-api:2.0.6")
77+
}
7578
api buildLibs.maven.model
7679
api buildLibs.shadow.plugin
7780
api buildLibs.apache.rat

build-tools-internal/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ gradlePlugin {
4040
id = 'elasticsearch.build-complete'
4141
implementationClass = 'org.elasticsearch.gradle.internal.ElasticsearchBuildCompletePlugin'
4242
}
43+
componentMetadataRules {
44+
id = 'elasticsearch.component-metadata-rules'
45+
implementationClass = 'org.elasticsearch.gradle.internal.dependencies.rules.ComponentMetadataRulesPlugin'
46+
}
4347
distro {
4448
id = 'elasticsearch.distro'
4549
implementationClass = 'org.elasticsearch.gradle.internal.distribution.ElasticsearchDistributionPlugin'
@@ -281,6 +285,9 @@ dependencies {
281285
testImplementation buildLibs.asm
282286
integTestImplementation buildLibs.asm
283287
api(buildLibs.snakeyaml)
288+
api("org.slf4j:slf4j-api:2.0.6") {
289+
because("Align with what we use in production")
290+
}
284291
}
285292
// Forcefully downgrade the jackson platform as used in production
286293
api enforcedPlatform(buildLibs.jackson.platform)

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,14 @@
1616
import org.elasticsearch.gradle.internal.test.MutedTestPlugin;
1717
import org.elasticsearch.gradle.internal.test.TestUtil;
1818
import org.elasticsearch.gradle.test.SystemPropertyCommandLineArgumentProvider;
19-
import org.elasticsearch.gradle.util.GradleUtils;
2019
import org.gradle.api.JavaVersion;
2120
import org.gradle.api.Plugin;
2221
import org.gradle.api.Project;
2322
import org.gradle.api.artifacts.Configuration;
24-
import org.gradle.api.artifacts.ResolutionStrategy;
2523
import org.gradle.api.file.FileCollection;
2624
import org.gradle.api.plugins.JavaBasePlugin;
2725
import org.gradle.api.plugins.JavaPluginExtension;
2826
import org.gradle.api.provider.Provider;
29-
import org.gradle.api.tasks.SourceSet;
30-
import org.gradle.api.tasks.SourceSetContainer;
3127
import org.gradle.api.tasks.compile.AbstractCompile;
3228
import org.gradle.api.tasks.compile.CompileOptions;
3329
import org.gradle.api.tasks.compile.GroovyCompile;
@@ -67,8 +63,6 @@ public void apply(Project project) {
6763
project.getPluginManager().apply(ElasticsearchTestBasePlugin.class);
6864
project.getPluginManager().apply(PrecommitTaskPlugin.class);
6965
project.getPluginManager().apply(MutedTestPlugin.class);
70-
71-
configureConfigurations(project);
7266
configureCompile(project);
7367
configureInputNormalization(project);
7468
configureNativeLibraryPath(project);
@@ -77,54 +71,6 @@ public void apply(Project project) {
7771
project.getExtensions().getExtraProperties().set("versions", VersionProperties.getVersions());
7872
}
7973

80-
/**
81-
* Makes dependencies non-transitive.
82-
* <p>
83-
* Gradle allows setting all dependencies as non-transitive very easily.
84-
* Sadly this mechanism does not translate into maven pom generation. In order
85-
* to effectively make the pom act as if it has no transitive dependencies,
86-
* we must exclude each transitive dependency of each direct dependency.
87-
* <p>
88-
* Determining the transitive deps of a dependency which has been resolved as
89-
* non-transitive is difficult because the process of resolving removes the
90-
* transitive deps. To sidestep this issue, we create a configuration per
91-
* direct dependency version. This specially named and unique configuration
92-
* will contain all of the transitive dependencies of this particular
93-
* dependency. We can then use this configuration during pom generation
94-
* to iterate the transitive dependencies and add excludes.
95-
*/
96-
public static void configureConfigurations(Project project) {
97-
// we are not shipping these jars, we act like dumb consumers of these things
98-
if (project.getPath().startsWith(":test:fixtures") || project.getPath().equals(":build-tools")) {
99-
return;
100-
}
101-
// fail on any conflicting dependency versions
102-
project.getConfigurations().all(configuration -> {
103-
if (configuration.getName().endsWith("Fixture")) {
104-
// just a self contained test-fixture configuration, likely transitive and hellacious
105-
return;
106-
}
107-
configuration.resolutionStrategy(ResolutionStrategy::failOnVersionConflict);
108-
});
109-
110-
// disable transitive dependency management
111-
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
112-
sourceSets.all(sourceSet -> disableTransitiveDependenciesForSourceSet(project, sourceSet));
113-
}
114-
115-
private static void disableTransitiveDependenciesForSourceSet(Project project, SourceSet sourceSet) {
116-
List<String> sourceSetConfigurationNames = List.of(
117-
sourceSet.getApiConfigurationName(),
118-
sourceSet.getImplementationConfigurationName(),
119-
sourceSet.getCompileOnlyConfigurationName(),
120-
sourceSet.getRuntimeOnlyConfigurationName()
121-
);
122-
123-
project.getConfigurations()
124-
.matching(c -> sourceSetConfigurationNames.contains(c.getName()))
125-
.configureEach(GradleUtils::disableTransitiveDependencies);
126-
}
127-
12874
/**
12975
* Adds compiler settings to the project
13076
*/

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaModulePathPlugin.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.gradle.api.artifacts.result.ResolvedComponentResult;
2222
import org.gradle.api.artifacts.result.ResolvedDependencyResult;
2323
import org.gradle.api.attributes.LibraryElements;
24+
import org.gradle.api.attributes.Usage;
2425
import org.gradle.api.file.FileCollection;
2526
import org.gradle.api.logging.Logger;
2627
import org.gradle.api.plugins.JavaPlugin;
@@ -75,12 +76,13 @@ void configureCompileModulePath(Project project) {
7576
it.extendsFrom(compileClasspath);
7677
it.setCanBeResolved(true);
7778
it.setCanBeConsumed(false); // we don't want this configuration used by dependent projects
78-
it.attributes(
79-
attrs -> attrs.attribute(
79+
it.attributes(attrs -> {
80+
attrs.attribute(
8081
LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
8182
project.getObjects().named(LibraryElements.class, LibraryElements.CLASSES)
82-
)
83-
);
83+
);
84+
attrs.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_API));
85+
});
8486
}).getIncoming().artifactView(it -> {
8587
it.componentFilter(cf -> {
8688
var visited = new HashSet<ComponentIdentifier>();

0 commit comments

Comments
 (0)