Skip to content

Commit 95618cb

Browse files
committed
Add configurable eager BOM resolution for external plugin integration
1 parent 72ac83f commit 95618cb

File tree

4 files changed

+377
-41
lines changed

4 files changed

+377
-41
lines changed

src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import netflix.nebula.dependency.recommender.provider.RecommendationResolver;
2323
import netflix.nebula.dependency.recommender.publisher.MavenBomXmlGenerator;
2424
import netflix.nebula.dependency.recommender.service.BomResolverService;
25+
import netflix.nebula.dependency.recommender.util.BomResolutionUtil;
2526
import org.apache.commons.lang3.StringUtils;
2627
import org.codehaus.groovy.runtime.MethodClosure;
2728
import org.gradle.api.Action;
@@ -40,7 +41,6 @@
4041
import org.gradle.api.logging.Logging;
4142
import org.gradle.api.plugins.ExtraPropertiesExtension;
4243
import org.gradle.api.provider.Provider;
43-
import org.gradle.internal.deprecation.DeprecationLogger;
4444
import org.gradle.util.GradleVersion;
4545

4646
import java.lang.reflect.Method;
@@ -80,17 +80,17 @@ private void applyRecommendationsDirectly(final Project project, final Configura
8080
@Override
8181
public void execute(Project p) {
8282
// Eagerly resolve and cache all BOMs if using build service approach
83-
if (shouldUseBuildService(p)) {
84-
eagerlyResolveBoms(p, recommendationProviderContainer);
83+
if (shouldUseBuildService(p) && BomResolutionUtil.shouldEagerlyResolveBoms(p, recommendationProviderContainer)) {
84+
BomResolutionUtil.eagerlyResolveBoms(p, recommendationProviderContainer, NEBULA_RECOMMENDER_BOM);
8585
}
8686

8787
p.getConfigurations().all(new ExtendRecommenderConfigurationAction(bomConfiguration, p, recommendationProviderContainer));
8888
p.subprojects(new Action<Project>() {
8989
@Override
9090
public void execute(Project sub) {
9191
// Also eagerly resolve BOMs for subprojects if using build service
92-
if (shouldUseBuildService(sub)) {
93-
eagerlyResolveBoms(sub, recommendationProviderContainer);
92+
if (shouldUseBuildService(sub) && BomResolutionUtil.shouldEagerlyResolveBoms(sub, recommendationProviderContainer)) {
93+
BomResolutionUtil.eagerlyResolveBoms(sub, recommendationProviderContainer, NEBULA_RECOMMENDER_BOM);
9494
}
9595
sub.getConfigurations().all(new ExtendRecommenderConfigurationAction(bomConfiguration, sub, recommendationProviderContainer));
9696
}
@@ -104,9 +104,9 @@ private void applyRecommendations(final Project project) {
104104
project.afterEvaluate(new Action<Project>() {
105105
@Override
106106
public void execute(Project p) {
107-
if (shouldUseBuildService(p)) {
107+
if (shouldUseBuildService(p) && BomResolutionUtil.shouldEagerlyResolveBoms(p, recommendationProviderContainer)) {
108108
// Eagerly resolve and cache all BOMs after project evaluation
109-
eagerlyResolveBoms(p, recommendationProviderContainer);
109+
BomResolutionUtil.eagerlyResolveBoms(p, recommendationProviderContainer, NEBULA_RECOMMENDER_BOM);
110110
}
111111
}
112112
});
@@ -269,7 +269,7 @@ public Set<String> getReasonsRecursive(Project project) {
269269
return getReasonsRecursive(project.getParent());
270270
return Collections.emptySet();
271271
}
272-
272+
273273
/**
274274
* Determines whether to use the BomResolverService (build service) approach.
275275
*
@@ -300,40 +300,29 @@ private boolean shouldUseBuildService(Project project) {
300300
* Eagerly resolves BOM configurations during the configuration phase to prevent
301301
* configuration resolution lock conflicts in parallel builds.
302302
*
303-
* <p>This method is called during {@code afterEvaluate} when exclusive locks are
304-
* available. It instructs the {@link BomResolverService} to resolve all BOM
305-
* configurations and cache the results for later use during dependency resolution.</p>
303+
* <p>This method delegates to {@link BomResolutionUtil#eagerlyResolveBoms} and is
304+
* provided for backward compatibility and convenience for external plugins.</p>
305+
*
306+
* <p><strong>External Plugin Usage:</strong></p>
307+
* <pre>{@code
308+
* // Get the plugin instance and container
309+
* DependencyRecommendationsPlugin plugin = project.plugins.getPlugin(DependencyRecommendationsPlugin)
310+
* RecommendationProviderContainer container = project.extensions.getByType(RecommendationProviderContainer)
311+
*
312+
* // Disable automatic resolution and add BOMs
313+
* container.setEagerlyResolve(false)
314+
* container.mavenBom(module: 'com.example:custom-bom:1.0.0')
306315
*
307-
* <p>The eager resolution prevents the need to resolve configurations during the
308-
* dependency resolution phase, which would cause {@code IllegalResolutionException}
309-
* in parallel builds with Gradle 9+.</p>
316+
* // Manually trigger resolution
317+
* plugin.eagerlyResolveBoms(project, container)
318+
* }</pre>
310319
*
311320
* @param project the Gradle project whose BOM configurations should be resolved
312321
* @param container the recommendation provider container to check for additional BOM providers
322+
* @since 12.7.0
323+
* @see BomResolutionUtil#eagerlyResolveBoms(Project, RecommendationProviderContainer, String)
313324
*/
314-
private void eagerlyResolveBoms(Project project, RecommendationProviderContainer container) {
315-
try {
316-
// Get the build service
317-
Provider<BomResolverService> bomResolverService =
318-
project.getGradle().getSharedServices().registerIfAbsent(
319-
"bomResolver", BomResolverService.class, spec -> {}
320-
);
321-
322-
// Resolve BOMs from the nebulaRecommenderBom configuration
323-
bomResolverService.get().eagerlyResolveAndCacheBoms(project, NEBULA_RECOMMENDER_BOM);
324-
325-
// Also trigger resolution for maven BOM provider if it exists
326-
// This handles mavenBom providers configured in the extension
327-
netflix.nebula.dependency.recommender.provider.MavenBomRecommendationProvider mavenBomProvider = container.getMavenBomProvider();
328-
if (mavenBomProvider != null) {
329-
try {
330-
mavenBomProvider.getVersion("dummy", "dummy"); // Trigger lazy initialization
331-
} catch (Exception e) {
332-
// Expected - just needed to trigger BOM resolution
333-
}
334-
}
335-
} catch (Exception e) {
336-
logger.warn("Failed to eagerly resolve BOMs for project " + project.getPath(), e);
337-
}
325+
public void eagerlyResolveBoms(Project project, RecommendationProviderContainer container) {
326+
BomResolutionUtil.eagerlyResolveBoms(project, container, NEBULA_RECOMMENDER_BOM);
338327
}
339328
}

src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@
2222
import org.gradle.api.artifacts.Configuration;
2323
import org.gradle.api.artifacts.Dependency;
2424
import org.gradle.api.internal.ConfigureByMapAction;
25-
import org.gradle.api.internal.DefaultNamedDomainObjectList;
2625
import org.gradle.api.model.ObjectFactory;
2726
import org.gradle.util.ConfigureUtil;
28-
import org.gradle.util.GradleVersion;
2927

3028
import java.io.File;
3129
import java.lang.reflect.InvocationTargetException;
@@ -44,6 +42,7 @@ public class RecommendationProviderContainer {
4442
private Set<String> excludedConfigurations = new HashSet<>();
4543
private Set<String> excludedConfigurationPrefixes = new HashSet<>();
4644
private Set<String> reasons = new HashSet<>();
45+
private Boolean eagerlyResolve = true;
4746

4847
// Make strategies available without import
4948
public static final RecommendationStrategies OverrideTransitives = RecommendationStrategies.OverrideTransitives;
@@ -235,7 +234,65 @@ public Boolean getStrictMode() {
235234
public void setStrictMode(Boolean strict) {
236235
strictMode = strict;
237236
}
238-
237+
238+
/**
239+
* Sets whether BOM configurations should be resolved eagerly during the configuration phase.
240+
*
241+
* <p>When set to {@code true} (default), BOM configurations will be resolved automatically
242+
* during the {@code afterEvaluate} phase to prevent configuration resolution lock conflicts
243+
* in parallel builds with Gradle 9+.</p>
244+
*
245+
* <p>When set to {@code false}, external plugins can take control of BOM resolution timing
246+
* by calling {@link netflix.nebula.dependency.recommender.util.BomResolutionUtil#eagerlyResolveBoms}
247+
* manually after modifying BOM configurations.</p>
248+
*
249+
* <p><strong>Usage by External Plugins:</strong></p>
250+
* <pre>{@code
251+
* // Disable automatic eager resolution
252+
* dependencyRecommendations {
253+
* setEagerlyResolve(false)
254+
*
255+
* // Add initial BOMs
256+
* mavenBom module: 'com.example:base-bom:1.0.0'
257+
* }
258+
*
259+
* project.afterEvaluate { p ->
260+
* def container = p.extensions.getByType(RecommendationProviderContainer)
261+
*
262+
* // Add additional BOMs dynamically
263+
* container.mavenBom(module: 'com.example:dynamic-bom:2.0.0')
264+
*
265+
* // Manually trigger resolution
266+
* BomResolutionUtil.eagerlyResolveBoms(p, container, 'nebulaRecommenderBom')
267+
* }
268+
* }</pre>
269+
*
270+
* @param eagerlyResolve {@code true} to enable automatic eager resolution,
271+
* {@code false} to disable it and allow manual control
272+
* @since 12.7.0
273+
* @see netflix.nebula.dependency.recommender.util.BomResolutionUtil#eagerlyResolveBoms
274+
*/
275+
public void setEagerlyResolve(Boolean eagerlyResolve) {
276+
this.eagerlyResolve = eagerlyResolve;
277+
}
278+
279+
/**
280+
* Returns whether BOM configurations should be resolved eagerly during the configuration phase.
281+
*
282+
* <p>This setting controls whether the dependency recommender plugin automatically resolves
283+
* BOM configurations during {@code afterEvaluate}, or whether external plugins should
284+
* handle resolution timing manually.</p>
285+
*
286+
* @return {@code true} if BOMs should be resolved eagerly (default),
287+
* {@code false} if resolution should be handled manually
288+
* @since 12.7.0
289+
* @see #setEagerlyResolve(Boolean)
290+
* @see netflix.nebula.dependency.recommender.util.BomResolutionUtil#shouldEagerlyResolveBoms
291+
*/
292+
public Boolean shouldEagerlyResolve() {
293+
return eagerlyResolve;
294+
}
295+
239296
public void excludeConfigurations(String ... names) {
240297
excludedConfigurations.addAll(Arrays.asList(names));
241298
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2025 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package netflix.nebula.dependency.recommender.util;
17+
18+
import netflix.nebula.dependency.recommender.provider.RecommendationProviderContainer;
19+
import netflix.nebula.dependency.recommender.service.BomResolverService;
20+
import org.gradle.api.Project;
21+
import org.gradle.api.logging.Logger;
22+
import org.gradle.api.logging.Logging;
23+
import org.gradle.api.provider.Provider;
24+
25+
/**
26+
* Utility class for handling BOM (Bill of Materials) resolution operations.
27+
*
28+
* <p>This utility provides methods for eagerly resolving BOM configurations during the
29+
* configuration phase to prevent configuration resolution lock conflicts in parallel builds
30+
* with Gradle 9+.</p>
31+
*
32+
* <p>The eager resolution approach is particularly important for:</p>
33+
* <ul>
34+
* <li>Parallel builds where configuration resolution locks can cause deadlocks</li>
35+
* <li>Build services that need cached BOM data during dependency resolution</li>
36+
* <li>External plugins that want to control when BOM resolution occurs</li>
37+
* </ul>
38+
*
39+
* @since 12.7.0
40+
*/
41+
public final class BomResolutionUtil {
42+
private static final Logger logger = Logging.getLogger(BomResolutionUtil.class);
43+
44+
private BomResolutionUtil() {
45+
// Utility class - prevent instantiation
46+
}
47+
48+
/**
49+
* Eagerly resolves BOM configurations for the given project during the configuration phase.
50+
*
51+
* <p>This method should be called during {@code afterEvaluate} when exclusive locks are
52+
* available. It instructs the {@link BomResolverService} to resolve all BOM
53+
* configurations and cache the results for later use during dependency resolution.</p>
54+
*
55+
* <p>The eager resolution prevents the need to resolve configurations during the
56+
* dependency resolution phase, which would cause {@code IllegalResolutionException}
57+
* in parallel builds with Gradle 9+.</p>
58+
*
59+
* <p><strong>Usage by External Plugins:</strong></p>
60+
* <pre>{@code
61+
* // Disable automatic eager resolution
62+
* container.setEagerlyResolve(false);
63+
*
64+
* // Modify BOMs as needed
65+
* container.mavenBom(module: 'com.example:updated-bom:1.0.0');
66+
*
67+
* // Manually trigger eager resolution
68+
* BomResolutionUtil.eagerlyResolveBoms(project, container, "nebulaRecommenderBom");
69+
* }</pre>
70+
*
71+
* @param project the Gradle project whose BOM configurations should be resolved
72+
* @param container the recommendation provider container to check for additional BOM providers
73+
* @param bomConfigurationName the name of the BOM configuration to resolve
74+
* @throws IllegalArgumentException if any parameter is null
75+
* @since 12.7.0
76+
*/
77+
public static void eagerlyResolveBoms(Project project, RecommendationProviderContainer container, String bomConfigurationName) {
78+
if (project == null) {
79+
throw new IllegalArgumentException("Project cannot be null");
80+
}
81+
if (container == null) {
82+
throw new IllegalArgumentException("RecommendationProviderContainer cannot be null");
83+
}
84+
if (bomConfigurationName == null || bomConfigurationName.trim().isEmpty()) {
85+
throw new IllegalArgumentException("BOM configuration name cannot be null or empty");
86+
}
87+
88+
try {
89+
// Get the build service
90+
Provider<BomResolverService> bomResolverService =
91+
project.getGradle().getSharedServices().registerIfAbsent(
92+
"bomResolver", BomResolverService.class, spec -> {}
93+
);
94+
95+
// Resolve BOMs from the specified configuration
96+
bomResolverService.get().eagerlyResolveAndCacheBoms(project, bomConfigurationName);
97+
98+
// Also trigger resolution for maven BOM provider if it exists
99+
// This handles mavenBom providers configured in the extension
100+
netflix.nebula.dependency.recommender.provider.MavenBomRecommendationProvider mavenBomProvider = container.getMavenBomProvider();
101+
if (mavenBomProvider != null) {
102+
try {
103+
mavenBomProvider.getVersion("dummy", "dummy"); // Trigger lazy initialization
104+
} catch (Exception e) {
105+
// Expected - just needed to trigger BOM resolution
106+
logger.debug("Triggered BOM resolution for maven BOM provider", e);
107+
}
108+
}
109+
110+
logger.debug("Successfully resolved BOMs for project {} using configuration {}",
111+
project.getPath(), bomConfigurationName);
112+
113+
} catch (Exception e) {
114+
logger.warn("Failed to eagerly resolve BOMs for project {} using configuration {}: {}",
115+
project.getPath(), bomConfigurationName, e.getMessage());
116+
if (logger.isDebugEnabled()) {
117+
logger.debug("BOM resolution failure details", e);
118+
}
119+
}
120+
}
121+
122+
/**
123+
* Checks if the given project should use eager BOM resolution.
124+
*
125+
* <p>This method evaluates both the container's eager resolution setting and
126+
* any project-specific overrides to determine if BOMs should be resolved eagerly.</p>
127+
*
128+
* @param project the Gradle project to check
129+
* @param container the recommendation provider container with eager resolution settings
130+
* @return true if BOMs should be resolved eagerly, false otherwise
131+
* @throws IllegalArgumentException if any parameter is null
132+
* @since 12.7.0
133+
*/
134+
public static boolean shouldEagerlyResolveBoms(Project project, RecommendationProviderContainer container) {
135+
if (project == null) {
136+
throw new IllegalArgumentException("Project cannot be null");
137+
}
138+
if (container == null) {
139+
throw new IllegalArgumentException("RecommendationProviderContainer cannot be null");
140+
}
141+
142+
// Check container setting first
143+
if (!container.shouldEagerlyResolve()) {
144+
logger.debug("Eager BOM resolution disabled for project {} via container setting", project.getPath());
145+
return false;
146+
}
147+
148+
// Could add additional project-specific checks here in the future
149+
// For example, checking project properties or environment variables
150+
151+
logger.debug("Eager BOM resolution enabled for project {}", project.getPath());
152+
return true;
153+
}
154+
}

0 commit comments

Comments
 (0)