Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ abstract class NxProjectReportTask @Inject constructor(private val projectLayout
logger.info("${Date()} Target Name Overrides ${targetNameOverrides.get()}")
logger.info("${Date()} Target Name Prefix: ${targetNamePrefix.get()}")
logger.info("${Date()} Atomized: ${atomized.get()}")

val project = projectRef.get() // Get project reference at execution time
val report =
createNodeForProject(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ fun processTargetsForProject(
dependencies,
targetNameOverrides,
gitIgnoreClassifier,
targetNamePrefix)
targetNamePrefix,
project)

targets[targetName] = target

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import dev.nx.gradle.data.Dependency
import dev.nx.gradle.data.ExternalDepData
import dev.nx.gradle.data.ExternalNode
import java.io.File
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.internal.TaskInternal
import org.gradle.api.internal.provider.ProviderInternal
import org.gradle.api.internal.tasks.DefaultTaskDependency
import org.gradle.api.tasks.TaskProvider

/**
* Process a task and convert it into target Going to populate:
Expand All @@ -24,7 +30,8 @@ fun processTask(
dependencies: MutableSet<Dependency>,
targetNameOverrides: Map<String, String>,
gitIgnoreClassifier: GitIgnoreClassifier,
targetNamePrefix: String = ""
targetNamePrefix: String = "",
project: Project,
): MutableMap<String, Any?> {
val logger = task.logger
logger.info("NxProjectReportTask: process $task for $projectRoot")
Expand Down Expand Up @@ -77,15 +84,16 @@ fun processTask(
task.description ?: "Run ${projectBuildPath}.${task.name}", projectBuildPath, task.name)
target["metadata"] = metadata

target["options"] =
if (continuous) {
mapOf(
"taskName" to "${projectBuildPath}:${task.name}",
"continuous" to true,
"excludeDependsOn" to shouldExcludeDependsOn(task))
} else {
mapOf("taskName" to "${projectBuildPath}:${task.name}")
}
target["options"] = buildMap {
put("taskName", "${projectBuildPath}:${task.name}")
val providerDependencies = findProviderBasedDependencies(task)
if (providerDependencies.isNotEmpty()) {
put("includeDependsOnTasks", providerDependencies.toList())
}
if (continuous) {
put("continuous", true)
}
}

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

private val tasksWithDependsOn = setOf("bootRun", "bootJar")
/**
* Finds provider-based task dependencies by inspecting lifecycle dependencies without triggering
* resolution. Uses Gradle internal APIs to access raw dependency values and check for providers
* with known producer tasks.
*/
fun findProviderBasedDependencies(task: Task): Set<String> {
val logger = task.logger
val producerTasks = mutableSetOf<String>()

try {
val taskInternal = task as? TaskInternal ?: return emptySet()
val lifecycleDeps = taskInternal.lifecycleDependencies

if (lifecycleDeps is DefaultTaskDependency) {
val rawDeps: Set<Any> = lifecycleDeps.mutableValues

rawDeps.forEach { dep ->
when (dep) {
is ProviderInternal<*> -> {
try {
val producer = dep.producer
if (producer.isKnown) {
producer.visitProducerTasks(
Action { producerTask -> producerTasks.add(producerTask.path) })
}
} catch (e: Exception) {
logger.debug("Could not get producer from provider: ${e.message}")
}
}
is TaskProvider<*> -> {
try {
producerTasks.add(dep.name)
} catch (e: Exception) {
logger.debug("Could not get name from TaskProvider: ${e.message}")
}
}
}
}
}

if (producerTasks.isNotEmpty()) {
logger.info("Task ${task.path} has provider-based dependencies: $producerTasks")
}
} catch (e: Exception) {
logger.debug("Could not analyze provider dependencies for ${task.path}: ${e.message}")
}

fun shouldExcludeDependsOn(task: Task): Boolean {
return !tasksWithDependsOn.contains(task.name)
return producerTasks
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ class ProcessTaskUtilsTest {
externalNodes = mutableMapOf(),
dependencies = mutableSetOf(),
targetNameOverrides = emptyMap(),
gitIgnoreClassifier = gitIgnoreClassifier)
gitIgnoreClassifier = gitIgnoreClassifier,
project = project)

assertEquals(true, result["cache"])
assertEquals(result["executor"], "@nx/gradle:gradle")
Expand Down Expand Up @@ -497,7 +498,8 @@ class ProcessTaskUtilsTest {
externalNodes = mutableMapOf(),
dependencies = mutableSetOf(),
targetNameOverrides = emptyMap(),
gitIgnoreClassifier = gitIgnoreClassifier)
gitIgnoreClassifier = gitIgnoreClassifier,
project = project)

assertNotNull(result)

Expand All @@ -522,4 +524,75 @@ class ProcessTaskUtilsTest {
it is Map<*, *> && (it["dependentTasksOutputFiles"] as String) == "build/classes/**/*"
})
}

@Nested
inner class ProviderBasedDependenciesTests {

@Test
fun `returns empty set when task has no dependencies`() {
val task = project.tasks.register("standalone").get()
assertTrue(findProviderBasedDependencies(task).isEmpty())
}

@Test
fun `identifies TaskProvider dependencies`() {
val producerProvider = project.tasks.register("producer")
val consumerProvider = project.tasks.register("consumer")
consumerProvider.configure { it.dependsOn(producerProvider) }

val result = findProviderBasedDependencies(consumerProvider.get())

assertTrue(result.any { it.contains("producer") }, "Found: $result")
}

@Test
fun `identifies ProviderInternal from task output files`() {
val producerProvider =
project.tasks.register("producer") { task ->
task.outputs.file(
java.io.File(project.layout.buildDirectory.asFile.get(), "output.jar"))
}
val consumerProvider = project.tasks.register("consumer")
consumerProvider.configure { it.dependsOn(producerProvider.map { p -> p.outputs.files }) }

val result = findProviderBasedDependencies(consumerProvider.get())

assertTrue(result.any { it.contains("producer") }, "Found: $result")
}

@Test
fun `identifies ProviderInternal from task output directory`() {
val compileProvider =
project.tasks.register("compile") { task ->
task.outputs.dir(java.io.File(project.layout.buildDirectory.asFile.get(), "classes"))
}
val jarProvider = project.tasks.register("jar")
jarProvider.configure { it.dependsOn(compileProvider.map { p -> p.outputs.files }) }

val result = findProviderBasedDependencies(jarProvider.get())

assertTrue(result.any { it.contains("compile") }, "Found: $result")
}

@Test
fun `identifies multiple TaskProvider dependencies`() {
val provider1 = project.tasks.register("task1")
val provider2 = project.tasks.register("task2")
val provider3 = project.tasks.register("task3")

val consumerProvider = project.tasks.register("consumer")
consumerProvider.configure { task ->
task.dependsOn(provider1)
task.dependsOn(provider2)
task.dependsOn(provider3)
}

val result = findProviderBasedDependencies(consumerProvider.get())

assertEquals(3, result.size)
assertTrue(result.any { it.contains("task1") }, "Found: $result")
assertTrue(result.any { it.contains("task2") }, "Found: $result")
assertTrue(result.any { it.contains("task3") }, "Found: $result")
}
}
}
27 changes: 27 additions & 0 deletions packages/gradle/src/executors/gradle/get-exclude-task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,33 @@ describe('getExcludeTasks', () => {
const excludes = getExcludeTasks(targets, nodes, runningTaskIds);
expect(excludes).toEqual(new Set(['testApp1']));
});

it('should not exclude tasks that are in includeDependsOnTasks', () => {
const targets = new Set<string>(['app1:test']);
const runningTaskIds = new Set<string>(['app1:test']);
const includeDependsOnTasks = new Set<string>(['lintApp1']);
const excludes = getExcludeTasks(
targets,
nodes,
runningTaskIds,
includeDependsOnTasks
);
// lintApp1 should not be excluded because it's in includeDependsOnTasks
expect(excludes).toEqual(new Set(['buildApp2']));
});

it('should not exclude any tasks if all are in includeDependsOnTasks', () => {
const targets = new Set<string>(['app1:test']);
const runningTaskIds = new Set<string>(['app1:test']);
const includeDependsOnTasks = new Set<string>(['lintApp1', 'buildApp2']);
const excludes = getExcludeTasks(
targets,
nodes,
runningTaskIds,
includeDependsOnTasks
);
expect(excludes).toEqual(new Set());
});
});

describe('getAllDependsOn', () => {
Expand Down
11 changes: 9 additions & 2 deletions packages/gradle/src/executors/gradle/get-exclude-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ import {
*
* For example, if a project defines `dependsOn: ['lint']` for the `test` target,
* and only `test` is running, this will return: ['lint']
*
* @param taskIds - Set of Nx task IDs to process
* @param nodes - Project graph nodes
* @param runningTaskIds - Set of task IDs that are currently running (won't be excluded)
* @param includeDependsOnTasks - Set of Gradle task names that should be included (not excluded)
* (typically provider-based dependencies that Gradle must resolve)
*/
export function getExcludeTasks(
taskIds: Set<string>,
nodes: Record<string, ProjectGraphProjectNode>,
runningTaskIds: Set<string> = new Set()
runningTaskIds: Set<string> = new Set(),
includeDependsOnTasks: Set<string> = new Set()
): Set<string> {
const excludes = new Set<string>();

Expand All @@ -25,7 +32,7 @@ export function getExcludeTasks(
const taskId = typeof dep === 'string' ? dep : dep?.target;
if (taskId && !runningTaskIds.has(taskId)) {
const gradleTaskName = getGradleTaskNameWithNxTaskId(taskId, nodes);
if (gradleTaskName) {
if (gradleTaskName && !includeDependsOnTasks.has(gradleTaskName)) {
excludes.add(gradleTaskName);
}
}
Expand Down
15 changes: 14 additions & 1 deletion packages/gradle/src/executors/gradle/gradle-batch.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,21 @@ export function getGradlewTasksToRun(
const testTaskIdsWithExclude: Set<string> = new Set([]);
const taskIdsWithoutExclude: Set<string> = new Set([]);
const gradlewTasksToRun: Record<string, GradleExecutorSchema> = {};
const includeDependsOnTasks: Set<string> = new Set();

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

gradlewTasksToRun[taskId] = input;

// Collect tasks that should be included (not excluded) - typically provider-based dependencies
if (input.includeDependsOnTasks) {
for (const task of input.includeDependsOnTasks) {
includeDependsOnTasks.add(task);
}
}

if (input.excludeDependsOn) {
if (input.testClassName) {
testTaskIdsWithExclude.add(taskId);
Expand All @@ -144,7 +152,12 @@ export function getGradlewTasksToRun(
dependencies.forEach((dep) => allDependsOn.add(dep));
}

const excludeTasks = getExcludeTasks(taskIdsWithExclude, nodes, allDependsOn);
const excludeTasks = getExcludeTasks(
taskIdsWithExclude,
nodes,
allDependsOn,
includeDependsOnTasks
);

const allTestsDependsOn = new Set<string>();
for (const taskId of testTaskIdsWithExclude) {
Expand Down
6 changes: 5 additions & 1 deletion packages/gradle/src/executors/gradle/gradle.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default async function gradleExecutor(
'testClassName',
'args',
'excludeDependsOn',
'includeDependsOnTasks',
'__unparsed__',
]);
Object.entries(options).forEach(([key, value]) => {
Expand All @@ -55,9 +56,12 @@ export default async function gradleExecutor(
});

if (options.excludeDependsOn) {
const includeDependsOnTasks = new Set(options.includeDependsOnTasks ?? []);
getExcludeTasks(
new Set([`${context.projectName}:${context.targetName}`]),
context.projectGraph.nodes
context.projectGraph.nodes,
new Set(),
includeDependsOnTasks
).forEach((task) => {
if (task) {
args.push('--exclude-task', task);
Expand Down
1 change: 1 addition & 0 deletions packages/gradle/src/executors/gradle/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export interface GradleExecutorSchema {
testClassName?: string;
args?: string[] | string;
excludeDependsOn: boolean;
includeDependsOnTasks?: string[];
__unparsed__?: string[];
}
8 changes: 8 additions & 0 deletions packages/gradle/src/executors/gradle/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
"default": true,
"x-priority": "internal"
},
"includeDependsOnTasks": {
"type": "array",
"items": {
"type": "string"
},
"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.",
"x-priority": "internal"
},
"__unparsed__": {
"type": "array",
"items": {
Expand Down
Loading