Skip to content

Commit 4af6080

Browse files
authored
feat(gradle): excludeDependsOn based on provider relationships (#33923)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> We have a hard coded list of task targets to not exclude depends on. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> We resolve a gradle task such that we can identify if there are provider dependency relationships involved. If there are, then do not exclude depends on since Gradle needs the dependsOn tasks to fulfill providers. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
1 parent d73fd46 commit 4af6080

File tree

10 files changed

+207
-26
lines changed

10 files changed

+207
-26
lines changed

packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/NxProjectReportTask.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ abstract class NxProjectReportTask @Inject constructor(private val projectLayout
5252
logger.info("${Date()} Target Name Overrides ${targetNameOverrides.get()}")
5353
logger.info("${Date()} Target Name Prefix: ${targetNamePrefix.get()}")
5454
logger.info("${Date()} Atomized: ${atomized.get()}")
55+
5556
val project = projectRef.get() // Get project reference at execution time
5657
val report =
5758
createNodeForProject(

packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/ProjectUtils.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ fun processTargetsForProject(
151151
dependencies,
152152
targetNameOverrides,
153153
gitIgnoreClassifier,
154-
targetNamePrefix)
154+
targetNamePrefix,
155+
project)
155156

156157
targets[targetName] = target
157158

packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/TaskUtils.kt

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import dev.nx.gradle.data.Dependency
55
import dev.nx.gradle.data.ExternalDepData
66
import dev.nx.gradle.data.ExternalNode
77
import java.io.File
8+
import org.gradle.api.Action
9+
import org.gradle.api.Project
810
import org.gradle.api.Task
11+
import org.gradle.api.internal.TaskInternal
12+
import org.gradle.api.internal.provider.ProviderInternal
13+
import org.gradle.api.internal.tasks.DefaultTaskDependency
14+
import org.gradle.api.tasks.TaskProvider
915

1016
/**
1117
* Process a task and convert it into target Going to populate:
@@ -24,7 +30,8 @@ fun processTask(
2430
dependencies: MutableSet<Dependency>,
2531
targetNameOverrides: Map<String, String>,
2632
gitIgnoreClassifier: GitIgnoreClassifier,
27-
targetNamePrefix: String = ""
33+
targetNamePrefix: String = "",
34+
project: Project,
2835
): MutableMap<String, Any?> {
2936
val logger = task.logger
3037
logger.info("NxProjectReportTask: process $task for $projectRoot")
@@ -77,15 +84,16 @@ fun processTask(
7784
task.description ?: "Run ${projectBuildPath}.${task.name}", projectBuildPath, task.name)
7885
target["metadata"] = metadata
7986

80-
target["options"] =
81-
if (continuous) {
82-
mapOf(
83-
"taskName" to "${projectBuildPath}:${task.name}",
84-
"continuous" to true,
85-
"excludeDependsOn" to shouldExcludeDependsOn(task))
86-
} else {
87-
mapOf("taskName" to "${projectBuildPath}:${task.name}")
88-
}
87+
target["options"] = buildMap {
88+
put("taskName", "${projectBuildPath}:${task.name}")
89+
val providerDependencies = findProviderBasedDependencies(task)
90+
if (providerDependencies.isNotEmpty()) {
91+
put("includeDependsOnTasks", providerDependencies.toList())
92+
}
93+
if (continuous) {
94+
put("continuous", true)
95+
}
96+
}
8997

9098
return target
9199
}
@@ -394,12 +402,6 @@ fun getMetadata(
394402
* Into an external dependency with key: "gradle:commons-lang3-3.13.0" with value: { "type":
395403
* "gradle", "name": "commons-lang3", "data": { "version": "3.13.0", "packageName":
396404
* "org.apache.commons.commons-lang3", "hash": "b7263237aa89c1f99b327197c41d0669707a462e",} }
397-
*
398-
* @param inputFile Path to the dependency jar.
399-
* @param externalNodes Map to populate with the resulting ExternalNode.
400-
* @param logger Gradle logger for warnings and debug info
401-
* @return The external dependency key (e.g., gradle:commons-lang3-3.13.0), or null if parsing
402-
* fails.
403405
*/
404406
fun getExternalDepFromInputFile(
405407
inputFile: String,
@@ -470,8 +472,52 @@ fun isCacheable(task: Task): Boolean {
470472
return !nonCacheableTasks.contains(task.name)
471473
}
472474

473-
private val tasksWithDependsOn = setOf("bootRun", "bootJar")
475+
/**
476+
* Finds provider-based task dependencies by inspecting lifecycle dependencies without triggering
477+
* resolution. Uses Gradle internal APIs to access raw dependency values and check for providers
478+
* with known producer tasks.
479+
*/
480+
fun findProviderBasedDependencies(task: Task): Set<String> {
481+
val logger = task.logger
482+
val producerTasks = mutableSetOf<String>()
483+
484+
try {
485+
val taskInternal = task as? TaskInternal ?: return emptySet()
486+
val lifecycleDeps = taskInternal.lifecycleDependencies
487+
488+
if (lifecycleDeps is DefaultTaskDependency) {
489+
val rawDeps: Set<Any> = lifecycleDeps.mutableValues
490+
491+
rawDeps.forEach { dep ->
492+
when (dep) {
493+
is ProviderInternal<*> -> {
494+
try {
495+
val producer = dep.producer
496+
if (producer.isKnown) {
497+
producer.visitProducerTasks(
498+
Action { producerTask -> producerTasks.add(producerTask.path) })
499+
}
500+
} catch (e: Exception) {
501+
logger.debug("Could not get producer from provider: ${e.message}")
502+
}
503+
}
504+
is TaskProvider<*> -> {
505+
try {
506+
producerTasks.add(dep.name)
507+
} catch (e: Exception) {
508+
logger.debug("Could not get name from TaskProvider: ${e.message}")
509+
}
510+
}
511+
}
512+
}
513+
}
514+
515+
if (producerTasks.isNotEmpty()) {
516+
logger.info("Task ${task.path} has provider-based dependencies: $producerTasks")
517+
}
518+
} catch (e: Exception) {
519+
logger.debug("Could not analyze provider dependencies for ${task.path}: ${e.message}")
520+
}
474521

475-
fun shouldExcludeDependsOn(task: Task): Boolean {
476-
return !tasksWithDependsOn.contains(task.name)
522+
return producerTasks
477523
}

packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/ProcessTaskUtilsTest.kt

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ class ProcessTaskUtilsTest {
102102
externalNodes = mutableMapOf(),
103103
dependencies = mutableSetOf(),
104104
targetNameOverrides = emptyMap(),
105-
gitIgnoreClassifier = gitIgnoreClassifier)
105+
gitIgnoreClassifier = gitIgnoreClassifier,
106+
project = project)
106107

107108
assertEquals(true, result["cache"])
108109
assertEquals(result["executor"], "@nx/gradle:gradle")
@@ -497,7 +498,8 @@ class ProcessTaskUtilsTest {
497498
externalNodes = mutableMapOf(),
498499
dependencies = mutableSetOf(),
499500
targetNameOverrides = emptyMap(),
500-
gitIgnoreClassifier = gitIgnoreClassifier)
501+
gitIgnoreClassifier = gitIgnoreClassifier,
502+
project = project)
501503

502504
assertNotNull(result)
503505

@@ -522,4 +524,75 @@ class ProcessTaskUtilsTest {
522524
it is Map<*, *> && (it["dependentTasksOutputFiles"] as String) == "build/classes/**/*"
523525
})
524526
}
527+
528+
@Nested
529+
inner class ProviderBasedDependenciesTests {
530+
531+
@Test
532+
fun `returns empty set when task has no dependencies`() {
533+
val task = project.tasks.register("standalone").get()
534+
assertTrue(findProviderBasedDependencies(task).isEmpty())
535+
}
536+
537+
@Test
538+
fun `identifies TaskProvider dependencies`() {
539+
val producerProvider = project.tasks.register("producer")
540+
val consumerProvider = project.tasks.register("consumer")
541+
consumerProvider.configure { it.dependsOn(producerProvider) }
542+
543+
val result = findProviderBasedDependencies(consumerProvider.get())
544+
545+
assertTrue(result.any { it.contains("producer") }, "Found: $result")
546+
}
547+
548+
@Test
549+
fun `identifies ProviderInternal from task output files`() {
550+
val producerProvider =
551+
project.tasks.register("producer") { task ->
552+
task.outputs.file(
553+
java.io.File(project.layout.buildDirectory.asFile.get(), "output.jar"))
554+
}
555+
val consumerProvider = project.tasks.register("consumer")
556+
consumerProvider.configure { it.dependsOn(producerProvider.map { p -> p.outputs.files }) }
557+
558+
val result = findProviderBasedDependencies(consumerProvider.get())
559+
560+
assertTrue(result.any { it.contains("producer") }, "Found: $result")
561+
}
562+
563+
@Test
564+
fun `identifies ProviderInternal from task output directory`() {
565+
val compileProvider =
566+
project.tasks.register("compile") { task ->
567+
task.outputs.dir(java.io.File(project.layout.buildDirectory.asFile.get(), "classes"))
568+
}
569+
val jarProvider = project.tasks.register("jar")
570+
jarProvider.configure { it.dependsOn(compileProvider.map { p -> p.outputs.files }) }
571+
572+
val result = findProviderBasedDependencies(jarProvider.get())
573+
574+
assertTrue(result.any { it.contains("compile") }, "Found: $result")
575+
}
576+
577+
@Test
578+
fun `identifies multiple TaskProvider dependencies`() {
579+
val provider1 = project.tasks.register("task1")
580+
val provider2 = project.tasks.register("task2")
581+
val provider3 = project.tasks.register("task3")
582+
583+
val consumerProvider = project.tasks.register("consumer")
584+
consumerProvider.configure { task ->
585+
task.dependsOn(provider1)
586+
task.dependsOn(provider2)
587+
task.dependsOn(provider3)
588+
}
589+
590+
val result = findProviderBasedDependencies(consumerProvider.get())
591+
592+
assertEquals(3, result.size)
593+
assertTrue(result.any { it.contains("task1") }, "Found: $result")
594+
assertTrue(result.any { it.contains("task2") }, "Found: $result")
595+
assertTrue(result.any { it.contains("task3") }, "Found: $result")
596+
}
597+
}
525598
}

packages/gradle/src/executors/gradle/get-exclude-task.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,33 @@ describe('getExcludeTasks', () => {
8787
const excludes = getExcludeTasks(targets, nodes, runningTaskIds);
8888
expect(excludes).toEqual(new Set(['testApp1']));
8989
});
90+
91+
it('should not exclude tasks that are in includeDependsOnTasks', () => {
92+
const targets = new Set<string>(['app1:test']);
93+
const runningTaskIds = new Set<string>(['app1:test']);
94+
const includeDependsOnTasks = new Set<string>(['lintApp1']);
95+
const excludes = getExcludeTasks(
96+
targets,
97+
nodes,
98+
runningTaskIds,
99+
includeDependsOnTasks
100+
);
101+
// lintApp1 should not be excluded because it's in includeDependsOnTasks
102+
expect(excludes).toEqual(new Set(['buildApp2']));
103+
});
104+
105+
it('should not exclude any tasks if all are in includeDependsOnTasks', () => {
106+
const targets = new Set<string>(['app1:test']);
107+
const runningTaskIds = new Set<string>(['app1:test']);
108+
const includeDependsOnTasks = new Set<string>(['lintApp1', 'buildApp2']);
109+
const excludes = getExcludeTasks(
110+
targets,
111+
nodes,
112+
runningTaskIds,
113+
includeDependsOnTasks
114+
);
115+
expect(excludes).toEqual(new Set());
116+
});
90117
});
91118

92119
describe('getAllDependsOn', () => {

packages/gradle/src/executors/gradle/get-exclude-task.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@ import {
99
*
1010
* For example, if a project defines `dependsOn: ['lint']` for the `test` target,
1111
* and only `test` is running, this will return: ['lint']
12+
*
13+
* @param taskIds - Set of Nx task IDs to process
14+
* @param nodes - Project graph nodes
15+
* @param runningTaskIds - Set of task IDs that are currently running (won't be excluded)
16+
* @param includeDependsOnTasks - Set of Gradle task names that should be included (not excluded)
17+
* (typically provider-based dependencies that Gradle must resolve)
1218
*/
1319
export function getExcludeTasks(
1420
taskIds: Set<string>,
1521
nodes: Record<string, ProjectGraphProjectNode>,
16-
runningTaskIds: Set<string> = new Set()
22+
runningTaskIds: Set<string> = new Set(),
23+
includeDependsOnTasks: Set<string> = new Set()
1724
): Set<string> {
1825
const excludes = new Set<string>();
1926

@@ -25,7 +32,7 @@ export function getExcludeTasks(
2532
const taskId = typeof dep === 'string' ? dep : dep?.target;
2633
if (taskId && !runningTaskIds.has(taskId)) {
2734
const gradleTaskName = getGradleTaskNameWithNxTaskId(taskId, nodes);
28-
if (gradleTaskName) {
35+
if (gradleTaskName && !includeDependsOnTasks.has(gradleTaskName)) {
2936
excludes.add(gradleTaskName);
3037
}
3138
}

packages/gradle/src/executors/gradle/gradle-batch.impl.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,21 @@ export function getGradlewTasksToRun(
119119
const testTaskIdsWithExclude: Set<string> = new Set([]);
120120
const taskIdsWithoutExclude: Set<string> = new Set([]);
121121
const gradlewTasksToRun: Record<string, GradleExecutorSchema> = {};
122+
const includeDependsOnTasks: Set<string> = new Set();
122123

123124
for (const taskId of taskIds) {
124125
const task = taskGraph.tasks[taskId];
125126
const input = inputs[task.id];
126127

127128
gradlewTasksToRun[taskId] = input;
128129

130+
// Collect tasks that should be included (not excluded) - typically provider-based dependencies
131+
if (input.includeDependsOnTasks) {
132+
for (const task of input.includeDependsOnTasks) {
133+
includeDependsOnTasks.add(task);
134+
}
135+
}
136+
129137
if (input.excludeDependsOn) {
130138
if (input.testClassName) {
131139
testTaskIdsWithExclude.add(taskId);
@@ -144,7 +152,12 @@ export function getGradlewTasksToRun(
144152
dependencies.forEach((dep) => allDependsOn.add(dep));
145153
}
146154

147-
const excludeTasks = getExcludeTasks(taskIdsWithExclude, nodes, allDependsOn);
155+
const excludeTasks = getExcludeTasks(
156+
taskIdsWithExclude,
157+
nodes,
158+
allDependsOn,
159+
includeDependsOnTasks
160+
);
148161

149162
const allTestsDependsOn = new Set<string>();
150163
for (const taskId of testTaskIdsWithExclude) {

packages/gradle/src/executors/gradle/gradle.impl.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default async function gradleExecutor(
4040
'testClassName',
4141
'args',
4242
'excludeDependsOn',
43+
'includeDependsOnTasks',
4344
'__unparsed__',
4445
]);
4546
Object.entries(options).forEach(([key, value]) => {
@@ -55,9 +56,12 @@ export default async function gradleExecutor(
5556
});
5657

5758
if (options.excludeDependsOn) {
59+
const includeDependsOnTasks = new Set(options.includeDependsOnTasks ?? []);
5860
getExcludeTasks(
5961
new Set([`${context.projectName}:${context.targetName}`]),
60-
context.projectGraph.nodes
62+
context.projectGraph.nodes,
63+
new Set(),
64+
includeDependsOnTasks
6165
).forEach((task) => {
6266
if (task) {
6367
args.push('--exclude-task', task);

packages/gradle/src/executors/gradle/schema.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export interface GradleExecutorSchema {
33
testClassName?: string;
44
args?: string[] | string;
55
excludeDependsOn: boolean;
6+
includeDependsOnTasks?: string[];
67
__unparsed__?: string[];
78
}

packages/gradle/src/executors/gradle/schema.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@
3434
"default": true,
3535
"x-priority": "internal"
3636
},
37+
"includeDependsOnTasks": {
38+
"type": "array",
39+
"items": {
40+
"type": "string"
41+
},
42+
"description": "List of Gradle task paths that should be included (not excluded) even when excludeDependsOn is true. These are typically provider-based dependencies that Gradle must resolve.",
43+
"x-priority": "internal"
44+
},
3745
"__unparsed__": {
3846
"type": "array",
3947
"items": {

0 commit comments

Comments
 (0)