2121import netflix .nebula .dependency .recommender .provider .RecommendationProviderContainer ;
2222import netflix .nebula .dependency .recommender .provider .RecommendationResolver ;
2323import netflix .nebula .dependency .recommender .publisher .MavenBomXmlGenerator ;
24+ import netflix .nebula .dependency .recommender .service .BomResolverService ;
2425import org .apache .commons .lang3 .StringUtils ;
2526import org .codehaus .groovy .runtime .MethodClosure ;
2627import org .gradle .api .Action ;
3839import org .gradle .api .logging .Logger ;
3940import org .gradle .api .logging .Logging ;
4041import org .gradle .api .plugins .ExtraPropertiesExtension ;
42+ import org .gradle .api .provider .Provider ;
4143import org .gradle .internal .deprecation .DeprecationLogger ;
44+ import org .gradle .util .GradleVersion ;
4245
4346import java .lang .reflect .Method ;
4447import java .util .*;
4548
4649public class DependencyRecommendationsPlugin implements Plugin <Project > {
4750 public static final String NEBULA_RECOMMENDER_BOM = "nebulaRecommenderBom" ;
4851 public static final boolean CORE_BOM_SUPPORT_ENABLED = Boolean .getBoolean ("nebula.features.coreBomSupport" );
52+ private static final GradleVersion GRADLE_9_0 = GradleVersion .version ("9.0" );
4953 private Logger logger = Logging .getLogger (DependencyRecommendationsPlugin .class );
5054 private RecommendationProviderContainer recommendationProviderContainer ;
5155 //TODO: remove this exclusion once https://github.com/gradle/gradle/issues/6750 is resolved
@@ -75,10 +79,19 @@ private void applyRecommendationsDirectly(final Project project, final Configura
7579 project .afterEvaluate (new Action <Project >() {
7680 @ Override
7781 public void execute (Project p ) {
82+ // Eagerly resolve and cache all BOMs if using build service approach
83+ if (shouldUseBuildService (p )) {
84+ eagerlyResolveBoms (p , recommendationProviderContainer );
85+ }
86+
7887 p .getConfigurations ().all (new ExtendRecommenderConfigurationAction (bomConfiguration , p , recommendationProviderContainer ));
7988 p .subprojects (new Action <Project >() {
8089 @ Override
8190 public void execute (Project sub ) {
91+ // Also eagerly resolve BOMs for subprojects if using build service
92+ if (shouldUseBuildService (sub )) {
93+ eagerlyResolveBoms (sub , recommendationProviderContainer );
94+ }
8295 sub .getConfigurations ().all (new ExtendRecommenderConfigurationAction (bomConfiguration , sub , recommendationProviderContainer ));
8396 }
8497 });
@@ -87,6 +100,17 @@ public void execute(Project sub) {
87100 }
88101
89102 private void applyRecommendations (final Project project ) {
103+ // Add eager BOM resolution for regular (non-core) BOM support if using build service
104+ project .afterEvaluate (new Action <Project >() {
105+ @ Override
106+ public void execute (Project p ) {
107+ if (shouldUseBuildService (p )) {
108+ // Eagerly resolve and cache all BOMs after project evaluation
109+ eagerlyResolveBoms (p , recommendationProviderContainer );
110+ }
111+ }
112+ });
113+
90114 project .getConfigurations ().all (new Action <Configuration >() {
91115 @ Override
92116 public void execute (final Configuration conf ) {
@@ -101,7 +125,7 @@ public Unit invoke(ResolvableDependencies resolvableDependencies) {
101125 }
102126
103127 for (Dependency dependency : resolvableDependencies .getDependencies ()) {
104- applyRecommendationToDependency (rsFactory , dependency , new ArrayList <ProjectDependency >());
128+ applyRecommendationToDependency (rsFactory , dependency , new ArrayList <ProjectDependency >(), project );
105129
106130 // if project dependency, pull all first orders and apply recommendations if missing dependency versions
107131 // dependency.getProjectConfiguration().allDependencies iterate and inspect them as well
@@ -160,7 +184,7 @@ private boolean isExcludedConfiguration(String confName) {
160184 return false ;
161185 }
162186
163- private void applyRecommendationToDependency (final RecommendationStrategyFactory factory , Dependency dependency , List <ProjectDependency > visited ) {
187+ private void applyRecommendationToDependency (final RecommendationStrategyFactory factory , Dependency dependency , List <ProjectDependency > visited , Project rootProject ) {
164188 if (dependency instanceof ExternalModuleDependency ) {
165189 factory .getRecommendationStrategy ().inspectDependency (dependency );
166190 } else if (dependency instanceof ProjectDependency ) {
@@ -171,10 +195,10 @@ private void applyRecommendationToDependency(final RecommendationStrategyFactory
171195 try {
172196 ProjectDependency .class .getMethod ("getTargetConfiguration" );
173197 String targetConfiguration = projectDependency .getTargetConfiguration () == null ? Dependency .DEFAULT_CONFIGURATION : projectDependency .getTargetConfiguration ();
174-
175- DeprecationLogger . whileDisabled (() -> {
176- configuration [0 ] = projectDependency . getDependencyProject () .getConfigurations ().getByName (targetConfiguration );
177- });
198+ Project dependencyProject = rootProject . findProject ( projectDependency . getPath ());
199+ if ( dependencyProject != null ) {
200+ configuration [0 ] = dependencyProject .getConfigurations ().getByName (targetConfiguration );
201+ }
178202 } catch (NoSuchMethodException ignore ) {
179203 try {
180204 Method method = ProjectDependency .class .getMethod ("getProjectConfiguration" );
@@ -183,9 +207,11 @@ private void applyRecommendationToDependency(final RecommendationStrategyFactory
183207 throw new RuntimeException ("Unable to retrieve configuration for project dependency" , e );
184208 }
185209 }
186- DependencySet dependencies = configuration [0 ].getAllDependencies ();
187- for (Dependency dep : dependencies ) {
188- applyRecommendationToDependency (factory , dep , visited );
210+ if (configuration [0 ] != null ) {
211+ DependencySet dependencies = configuration [0 ].getAllDependencies ();
212+ for (Dependency dep : dependencies ) {
213+ applyRecommendationToDependency (factory , dep , visited , rootProject );
214+ }
189215 }
190216 }
191217 }
@@ -243,4 +269,71 @@ public Set<String> getReasonsRecursive(Project project) {
243269 return getReasonsRecursive (project .getParent ());
244270 return Collections .emptySet ();
245271 }
272+
273+ /**
274+ * Determines whether to use the BomResolverService (build service) approach.
275+ *
276+ * <p>The build service is used when:</p>
277+ * <ul>
278+ * <li>Gradle version is 9.0 or higher, OR</li>
279+ * <li>The gradle property 'nebula.dependency-recommender.useBuildService' is set to true</li>
280+ * </ul>
281+ *
282+ * @param project the Gradle project to check
283+ * @return true if build service should be used, false otherwise
284+ */
285+ private boolean shouldUseBuildService (Project project ) {
286+ // Check if explicitly enabled via gradle property
287+ if (project .hasProperty ("nebula.dependency-recommender.useBuildService" )) {
288+ Object property = project .property ("nebula.dependency-recommender.useBuildService" );
289+ if (Boolean .parseBoolean (property .toString ())) {
290+ return true ;
291+ }
292+ }
293+
294+ // Default behavior: use build service for Gradle 9+
295+ GradleVersion currentVersion = GradleVersion .current ();
296+ return currentVersion .compareTo (GRADLE_9_0 ) >= 0 ;
297+ }
298+
299+ /**
300+ * Eagerly resolves BOM configurations during the configuration phase to prevent
301+ * configuration resolution lock conflicts in parallel builds.
302+ *
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>
306+ *
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>
310+ *
311+ * @param project the Gradle project whose BOM configurations should be resolved
312+ * @param container the recommendation provider container to check for additional BOM providers
313+ */
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+ }
338+ }
246339}
0 commit comments