diff --git a/docs/generated/packages/gradle/executors/gradle.json b/docs/generated/packages/gradle/executors/gradle.json index 05df55053e..9a8f63ba1d 100644 --- a/docs/generated/packages/gradle/executors/gradle.json +++ b/docs/generated/packages/gradle/executors/gradle.json @@ -24,6 +24,12 @@ ], "description": "The arguments to pass to the Gradle task.", "examples": [["--warning-mode", "all"], "--stracktrace"] + }, + "excludeDependsOn": { + "type": "boolean", + "description": "If true, the tasks will not execute its dependsOn tasks (e.g. pass --exclude-task args to gradle command). If false, the task will execute its dependsOn tasks.", + "default": true, + "x-priority": "internal" } }, "required": ["taskName"], diff --git a/e2e/gradle/src/gradle-import.test.ts b/e2e/gradle/src/gradle-import.test.ts index 5a2e14ea97..54688623b1 100644 --- a/e2e/gradle/src/gradle-import.test.ts +++ b/e2e/gradle/src/gradle-import.test.ts @@ -9,6 +9,7 @@ import { e2eCwd, readJson, runCommand, + createFile, } from '@nx/e2e/utils'; import { mkdirSync, rmdirSync, writeFileSync } from 'fs'; import { execSync } from 'node:child_process'; @@ -37,6 +38,8 @@ describe('Nx Import Gradle', () => { }); } + createFile('.gitignore', '.kotlin/'); + try { rmdirSync(tempImportE2ERoot); } catch {} diff --git a/packages/gradle/batch-runner/build.gradle.kts b/packages/gradle/batch-runner/build.gradle.kts index 7d311e98fc..962fe91935 100644 --- a/packages/gradle/batch-runner/build.gradle.kts +++ b/packages/gradle/batch-runner/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { val toolingApiVersion = "8.13" // Match the Gradle version you're working with implementation("org.gradle:gradle-tooling-api:$toolingApiVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") runtimeOnly("org.slf4j:slf4j-simple:1.7.10") implementation("com.google.code.gson:gson:2.10.1") } diff --git a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/NxBatchRunner.kt b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/NxBatchRunner.kt index 7b55ece743..c96464f55d 100644 --- a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/NxBatchRunner.kt +++ b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/NxBatchRunner.kt @@ -7,17 +7,20 @@ import dev.nx.gradle.runner.runTasksInParallel import dev.nx.gradle.util.logger import java.io.File import kotlin.system.exitProcess +import kotlinx.coroutines.runBlocking import org.gradle.tooling.GradleConnector import org.gradle.tooling.ProjectConnection fun main(args: Array) { val options = parseArgs(args) configureLogger(options.quiet) + logger.info("NxBatchOptions: $options") if (options.workspaceRoot.isBlank()) { logger.severe("❌ Missing required arguments --workspaceRoot") exitProcess(1) } + if (options.tasks.isEmpty()) { logger.severe("❌ Missing required arguments --tasks") exitProcess(1) @@ -29,7 +32,9 @@ fun main(args: Array) { connection = GradleConnector.newConnector().forProjectDirectory(File(options.workspaceRoot)).connect() - val results = runTasksInParallel(connection, options.tasks, options.args) + val results = runBlocking { + runTasksInParallel(connection, options.tasks, options.args, options.excludeTasks) + } val reportJson = Gson().toJson(results) println(reportJson) diff --git a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/cli/ArgParser.kt b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/cli/ArgParser.kt index c11363076a..3bc30eaf21 100644 --- a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/cli/ArgParser.kt +++ b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/cli/ArgParser.kt @@ -29,11 +29,16 @@ fun parseArgs(args: Array): NxBatchOptions { gson.fromJson(tasksJson, taskType) } else emptyMap() + val excludeTasks = + argMap["--excludeTasks"]?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } + ?: emptyList() + return NxBatchOptions( workspaceRoot = argMap["--workspaceRoot"] ?: "", tasks = tasksMap, args = argMap["--args"] ?: "", - quiet = argMap["--quiet"]?.toBoolean() ?: false) + quiet = argMap["--quiet"]?.toBoolean() ?: false, + excludeTasks = excludeTasks) } fun configureLogger(quiet: Boolean) { diff --git a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/data/NxBatchOptions.kt b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/data/NxBatchOptions.kt index 7589ae2268..59a87de791 100644 --- a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/data/NxBatchOptions.kt +++ b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/data/NxBatchOptions.kt @@ -4,5 +4,6 @@ data class NxBatchOptions( val workspaceRoot: String, val tasks: Map, val args: String, - val quiet: Boolean + val quiet: Boolean, + val excludeTasks: List ) diff --git a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/BuildListener.kt b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/BuildListener.kt index 488cca7edf..1337936063 100644 --- a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/BuildListener.kt +++ b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/BuildListener.kt @@ -25,6 +25,7 @@ fun buildListener( taskStartTimes[nxTaskId] = min(System.currentTimeMillis(), event.eventTime) } } + is TaskFinishEvent -> { val taskPath = event.descriptor.taskPath val success = @@ -33,10 +34,12 @@ fun buildListener( logger.info("✅ Task finished successfully: $taskPath") true } + is TaskFailureResult -> { logger.warning("❌ Task failed: $taskPath") false } + else -> true } diff --git a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/GradleRunner.kt b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/GradleRunner.kt index d7fbb113a4..d15f3d79b9 100644 --- a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/GradleRunner.kt +++ b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/GradleRunner.kt @@ -3,17 +3,20 @@ package dev.nx.gradle.runner import dev.nx.gradle.data.GradleTask import dev.nx.gradle.data.TaskResult import dev.nx.gradle.runner.OutputProcessor.buildTerminalOutput -import dev.nx.gradle.runner.OutputProcessor.splitOutputPerTask +import dev.nx.gradle.runner.OutputProcessor.finalizeTaskResults import dev.nx.gradle.util.logger import java.io.ByteArrayOutputStream +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import org.gradle.tooling.ProjectConnection import org.gradle.tooling.events.OperationType -fun runTasksInParallel( +suspend fun runTasksInParallel( connection: ProjectConnection, tasks: Map, additionalArgs: String, -): Map { + excludeTasks: List +): Map = coroutineScope { logger.info("▶️ Running all tasks in a single Gradle run: ${tasks.keys.joinToString(", ")}") val (testClassTasks, buildTasks) = tasks.entries.partition { it.value.testClassName != null } @@ -21,72 +24,76 @@ fun runTasksInParallel( logger.info("🧪 Test launcher tasks: ${testClassTasks.joinToString(", ") { it.key }}") logger.info("🛠️ Build launcher tasks: ${buildTasks.joinToString(", ") { it.key }}") - val allResults = mutableMapOf() - - val outputStream = ByteArrayOutputStream() - val errorStream = ByteArrayOutputStream() + val outputStream1 = ByteArrayOutputStream() + val errorStream1 = ByteArrayOutputStream() + val outputStream2 = ByteArrayOutputStream() + val errorStream2 = ByteArrayOutputStream() val args = buildList { // --info is for terminal per task // --continue is for continue running tasks if one failed in a batch - // --parallel and --build-cache are for performance + // --parallel is for performance // -Dorg.gradle.daemon.idletimeout=10000 is to kill daemon after 10 seconds - addAll( - listOf( - "--info", - "--continue", - "--parallel", - "--build-cache", - "-Dorg.gradle.daemon.idletimeout=10000")) + addAll(listOf("--info", "--continue", "-Dorg.gradle.daemon.idletimeout=10000")) addAll(additionalArgs.split(" ").filter { it.isNotBlank() }) + excludeTasks.forEach { + add("--exclude-task") + add(it) + } } + logger.info("🏳️ Args: ${args.joinToString(", ")}") - val taskNames = tasks.values.map { it.taskName }.distinct() - - if (buildTasks.isNotEmpty()) { - allResults.putAll( - runBuildLauncher( - connection, - buildTasks.associate { it.key to it.value }, - taskNames, - args, - outputStream, - errorStream)) + val buildJob = async { + if (buildTasks.isNotEmpty()) { + runBuildLauncher( + connection, + buildTasks.associate { it.key to it.value }, + args, + outputStream1, + errorStream1) + } else emptyMap() } - if (testClassTasks.isNotEmpty()) { - allResults.putAll( - runTestLauncher( - connection, - testClassTasks.associate { it.key to it.value }, - taskNames, - args, - outputStream, - errorStream)) + val testJob = async { + if (testClassTasks.isNotEmpty()) { + runTestLauncher( + connection, + testClassTasks.associate { it.key to it.value }, + args, + outputStream2, + errorStream2) + } else emptyMap() } - return allResults + val allResults = mutableMapOf() + allResults.putAll(buildJob.await()) + allResults.putAll(testJob.await()) + + return@coroutineScope allResults } fun runBuildLauncher( connection: ProjectConnection, tasks: Map, - taskNames: List, args: List, outputStream: ByteArrayOutputStream, errorStream: ByteArrayOutputStream ): Map { + val taskNames = tasks.values.map { it.taskName }.distinct().toTypedArray() + logger.info("📋 Collected ${taskNames.size} unique task names: ${taskNames.joinToString(", ")}") + val taskStartTimes = mutableMapOf() val taskResults = mutableMapOf() + val globalStart = System.currentTimeMillis() var globalOutput: String try { connection .newBuild() .apply { - forTasks(*taskNames.toTypedArray()) + forTasks(*taskNames) withArguments(*args.toTypedArray()) setStandardOutput(outputStream) setStandardError(errorStream) @@ -97,17 +104,20 @@ fun runBuildLauncher( } catch (e: Exception) { globalOutput = buildTerminalOutput(outputStream, errorStream) + "\nException occurred: ${e.message}" - logger.warning("\ud83d\udca5 Gradle run failed: ${e.message}") + logger.warning("\ud83d\udca5 Gradle run failed: ${e.message} $errorStream") } finally { outputStream.close() errorStream.close() } - val perTaskOutput = splitOutputPerTask(globalOutput) - tasks.forEach { (taskId, taskConfig) -> - val taskOutput = perTaskOutput[taskConfig.taskName] ?: globalOutput - taskResults[taskId]?.let { taskResults[taskId] = it.copy(terminalOutput = taskOutput) } - } + val globalEnd = System.currentTimeMillis() + finalizeTaskResults( + tasks = tasks, + taskResults = taskResults, + globalOutput = globalOutput, + errorStream = errorStream, + globalStart = globalStart, + globalEnd = globalEnd) logger.info("\u2705 Finished build tasks") return taskResults @@ -116,11 +126,13 @@ fun runBuildLauncher( fun runTestLauncher( connection: ProjectConnection, tasks: Map, - taskNames: List, args: List, outputStream: ByteArrayOutputStream, errorStream: ByteArrayOutputStream ): Map { + val taskNames = tasks.values.map { it.taskName }.distinct().toTypedArray() + logger.info("📋 Collected ${taskNames.size} unique task names: ${taskNames.joinToString(", ")}") + val taskStartTimes = mutableMapOf() val taskResults = mutableMapOf() val testTaskStatus = mutableMapOf() @@ -140,8 +152,14 @@ fun runTestLauncher( connection .newTestLauncher() .apply { - forTasks(*taskNames.toTypedArray()) - tasks.values.mapNotNull { it.testClassName }.forEach { withJvmTestClasses(it) } + forTasks(*taskNames) + tasks.values + .mapNotNull { it.testClassName } + .forEach { + logger.info("Registering test class: $it") + withArguments("--tests", it) + withJvmTestClasses(it) + } withArguments(*args.toTypedArray()) setStandardOutput(outputStream) setStandardError(errorStream) @@ -153,9 +171,10 @@ fun runTestLauncher( .run() globalOutput = buildTerminalOutput(outputStream, errorStream) } catch (e: Exception) { + logger.warning(errorStream.toString()) globalOutput = buildTerminalOutput(outputStream, errorStream) + "\nException occurred: ${e.message}" - logger.warning("\ud83d\udca5 Gradle test run failed: ${e.message}") + logger.warning("\ud83d\udca5 Gradle test run failed: ${e.message} $errorStream") } finally { outputStream.close() errorStream.close() @@ -175,11 +194,13 @@ fun runTestLauncher( } } - val perTaskOutput = splitOutputPerTask(globalOutput) - tasks.forEach { (taskId, taskConfig) -> - val taskOutput = perTaskOutput[taskConfig.taskName] ?: globalOutput - taskResults[taskId]?.let { taskResults[taskId] = it.copy(terminalOutput = taskOutput) } - } + finalizeTaskResults( + tasks = tasks, + taskResults = taskResults, + globalOutput = globalOutput, + errorStream = errorStream, + globalStart = globalStart, + globalEnd = globalEnd) logger.info("\u2705 Finished test tasks") return taskResults diff --git a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/OutputProcessor.kt b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/OutputProcessor.kt index 9e9459fe8b..d21e05fedb 100644 --- a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/OutputProcessor.kt +++ b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/OutputProcessor.kt @@ -1,5 +1,7 @@ package dev.nx.gradle.runner +import dev.nx.gradle.data.GradleTask +import dev.nx.gradle.data.TaskResult import java.io.ByteArrayOutputStream object OutputProcessor { @@ -12,7 +14,42 @@ object OutputProcessor { } } - fun splitOutputPerTask(globalOutput: String): Map { + fun finalizeTaskResults( + tasks: Map, + taskResults: MutableMap, + globalOutput: String, + errorStream: ByteArrayOutputStream, + globalStart: Long, + globalEnd: Long + ): Map { + val perTaskOutput = splitOutputPerTask(globalOutput) + + tasks.forEach { (taskId, taskConfig) -> + val baseOutput = perTaskOutput[taskConfig.taskName] ?: "" + val existingResult = taskResults[taskId] + + val outputWithErrors = + if (existingResult?.success == false) { + baseOutput + "\n" + errorStream.toString() + } else { + baseOutput + } + + val finalOutput = outputWithErrors.ifBlank { globalOutput } + + taskResults[taskId] = + existingResult?.copy(terminalOutput = finalOutput) + ?: TaskResult( + success = false, + startTime = globalStart, + endTime = globalEnd, + terminalOutput = finalOutput) + } + + return taskResults + } + + private fun splitOutputPerTask(globalOutput: String): Map { val unescapedOutput = globalOutput.replace("\\u003e", ">").replace("\\n", "\n") val taskHeaderRegex = Regex("(?=> Task (:[^\\s]+))") val sections = unescapedOutput.split(taskHeaderRegex) diff --git a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/TestListener.kt b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/TestListener.kt index 72fe90ec75..447af84cd5 100644 --- a/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/TestListener.kt +++ b/packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/TestListener.kt @@ -3,8 +3,6 @@ package dev.nx.gradle.runner import dev.nx.gradle.data.GradleTask import dev.nx.gradle.data.TaskResult import dev.nx.gradle.util.logger -import kotlin.math.max -import kotlin.math.min import org.gradle.tooling.events.ProgressEvent import org.gradle.tooling.events.task.TaskFinishEvent import org.gradle.tooling.events.task.TaskStartEvent @@ -22,38 +20,42 @@ fun testListener( is TaskStartEvent, is TaskFinishEvent -> buildListener(tasks, taskStartTimes, taskResults)(event) is TestStartEvent -> { - (event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className -> + ((event.descriptor as? JvmTestOperationDescriptor)?.className?.substringAfterLast('.')?.let { + simpleClassName -> tasks.entries - .find { entry -> entry.value.testClassName?.let { className.endsWith(it) } ?: false } + .find { entry -> entry.value.testClassName?.let { simpleClassName == it } ?: false } ?.key ?.let { nxTaskId -> - testStartTimes.compute(nxTaskId) { _, old -> - min(old ?: event.eventTime, event.eventTime) - } + testStartTimes.computeIfAbsent(nxTaskId) { event.eventTime } + logger.info("🏁 Test start at ${event.eventTime}: $nxTaskId $simpleClassName") } - } + }) } is TestFinishEvent -> { - (event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className -> + ((event.descriptor as? JvmTestOperationDescriptor)?.className?.substringAfterLast('.')?.let { + simpleClassName -> tasks.entries - .find { entry -> entry.value.testClassName?.let { className.endsWith(it) } ?: false } + .find { entry -> entry.value.testClassName?.let { simpleClassName == it } ?: false } ?.key ?.let { nxTaskId -> - testEndTimes.compute(nxTaskId) { _, old -> - max(old ?: event.eventTime, event.eventTime) - } + testEndTimes.compute(nxTaskId) { _, _ -> event.result.endTime } when (event.result) { - is TestSuccessResult -> logger.info("\u2705 Test passed: $nxTaskId $className") + is TestSuccessResult -> + logger.info( + "\u2705 Test passed at ${event.result.endTime}: $nxTaskId $simpleClassName") is TestFailureResult -> { testTaskStatus[nxTaskId] = false - logger.warning("\u274C Test failed: $nxTaskId $className") + logger.warning("\u274C Test failed: $nxTaskId $simpleClassName") } + is TestSkippedResult -> - logger.warning("\u26A0\uFE0F Test skipped: $nxTaskId $className") - else -> logger.warning("\u26A0\uFE0F Unknown test result: $nxTaskId $className") + logger.warning("\u26A0\uFE0F Test skipped: $nxTaskId $simpleClassName") + + else -> + logger.warning("\u26A0\uFE0F Unknown test result: $nxTaskId $simpleClassName") } } - } + }) } } } diff --git a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/NxProjectGraphReportPlugin.kt b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/NxProjectGraphReportPlugin.kt index 0b91fa86e0..4f2edc1485 100644 --- a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/NxProjectGraphReportPlugin.kt +++ b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/NxProjectGraphReportPlugin.kt @@ -19,14 +19,6 @@ class NxProjectGraphReportPlugin : Plugin { "default-hash" } - val cwdProperty = - project.findProperty("cwd")?.toString() - ?: run { - project.logger.warn( - "No 'cwd' property was provided for $project. Using default hash value: ${System.getProperty("user.dir")}") - System.getProperty("user.dir") - } - val workspaceRootProperty = project.findProperty("workspaceRoot")?.toString() ?: run { @@ -43,7 +35,6 @@ class NxProjectGraphReportPlugin : Plugin { task.projectRef.set(project) task.hash.set(hashProperty) task.targetNameOverrides.set(targetNameOverrides) - task.cwd.set(cwdProperty) task.workspaceRoot.set(workspaceRootProperty) task.description = "Create Nx project report for ${project.name}" diff --git a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/NxProjectReportTask.kt b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/NxProjectReportTask.kt index 71cc67313f..6450305fc4 100644 --- a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/NxProjectReportTask.kt +++ b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/NxProjectReportTask.kt @@ -24,16 +24,20 @@ abstract class NxProjectReportTask @Inject constructor(private val projectLayout @get:Input abstract val hash: Property - @get:Input abstract val cwd: Property - @get:Input abstract val workspaceRoot: Property + @get:Input abstract val atomized: Property + @get:Input abstract val targetNameOverrides: MapProperty // Don't compute report at configuration time, move it to execution time @get:Internal // Prevent Gradle from caching this reference abstract val projectRef: Property + init { + atomized.convention(true) + } + @get:OutputFile val outputFile: File get() = projectLayout.buildDirectory.file("nx/${projectName.get()}.json").get().asFile @@ -43,13 +47,14 @@ abstract class NxProjectReportTask @Inject constructor(private val projectLayout logger.info("${Date()} Apply task action NxProjectReportTask for ${projectName.get()}") logger.info("${Date()} Hash input: ${hash.get()}") logger.info("${Date()} Target Name Overrides ${targetNameOverrides.get()}") + logger.info("${Date()} Atomized: ${atomized.get()}") val project = projectRef.get() // Get project reference at execution time val report = createNodeForProject( project, targetNameOverrides.get(), workspaceRoot.get(), - cwd.get()) // Compute report at execution time + atomized.get()) // Compute report at execution time val reportJson = gson.toJson(report) if (outputFile.exists() && outputFile.readText() == reportJson) { diff --git a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/ProjectUtils.kt b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/ProjectUtils.kt index 61977c7902..9b81ae0dd5 100644 --- a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/ProjectUtils.kt +++ b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/ProjectUtils.kt @@ -9,7 +9,7 @@ fun createNodeForProject( project: Project, targetNameOverrides: Map, workspaceRoot: String, - cwd: String + atomized: Boolean ): GradleNodeReport { val logger = project.logger logger.info("${Date()} ${project.name} createNodeForProject: get nodes and dependencies") @@ -31,7 +31,8 @@ fun createNodeForProject( try { val gradleTargets: GradleTargets = - processTargetsForProject(project, dependencies, targetNameOverrides, workspaceRoot, cwd) + processTargetsForProject( + project, dependencies, targetNameOverrides, workspaceRoot, atomized) val projectRoot = project.projectDir.path val projectNode = ProjectNode( @@ -61,7 +62,7 @@ fun processTargetsForProject( dependencies: MutableSet, targetNameOverrides: Map, workspaceRoot: String, - cwd: String + atomized: Boolean ): GradleTargets { val targets: NxTargets = mutableMapOf() val targetGroups: TargetGroups = mutableMapOf() @@ -111,7 +112,7 @@ fun processTargetsForProject( targets[taskName] = target - if (hasCiTestTarget && task.name.startsWith("compileTest")) { + if (hasCiTestTarget && task.name.startsWith("compileTest") && atomized) { addTestCiTargets( task.inputs.sourceFiles, projectBuildPath, @@ -124,7 +125,7 @@ fun processTargetsForProject( ciTestTargetName!!) } - if (hasCiIntTestTarget && task.name.startsWith("compileIntTest")) { + if (hasCiIntTestTarget && task.name.startsWith("compileIntTest") && atomized) { addTestCiTargets( task.inputs.sourceFiles, projectBuildPath, @@ -137,14 +138,19 @@ fun processTargetsForProject( ciIntTestTargetName!!) } - if (task.name == "check" && (hasCiTestTarget || hasCiIntTestTarget)) { + if (task.name == "check") { val replacedDependencies = (target["dependsOn"] as? List<*>)?.map { dep -> - when (dep.toString()) { - testTargetName -> ciTestTargetName ?: dep - intTestTargetName -> ciIntTestTargetName ?: dep - else -> dep - }.toString() + val dependsOn = dep.toString() + if (hasCiTestTarget && dependsOn == "${project.name}:$testTargetName" && atomized) { + "${project.name}:$ciTestTargetName" + } else if (hasCiIntTestTarget && + dependsOn == "${project.name}:$intTestTargetName" && + atomized) { + "${project.name}:$ciIntTestTargetName" + } else { + dep + } } ?: emptyList() val newTarget: MutableMap = diff --git a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/TaskUtils.kt b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/TaskUtils.kt index 404126ecff..912161d8f8 100644 --- a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/TaskUtils.kt +++ b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/TaskUtils.kt @@ -180,12 +180,7 @@ fun getDependsOnForTask( // Check if this task name needs to be overridden val taskName = targetNameOverrides.getOrDefault(depTask.name + "TargetName", depTask.name) - val overriddenTaskName = - if (depProject == taskProject) { - taskName - } else { - "${depProject.name}:${taskName}" - } + val overriddenTaskName = "${depProject.name}:${taskName}" overriddenTaskName } diff --git a/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/CreateNodeForProjectTest.kt b/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/CreateNodeForProjectTest.kt index 9aa36e763a..3f890f452c 100644 --- a/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/CreateNodeForProjectTest.kt +++ b/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/CreateNodeForProjectTest.kt @@ -32,7 +32,7 @@ class CreateNodeForProjectTest { project = project, targetNameOverrides = targetNameOverrides, workspaceRoot = workspaceRoot, - cwd = "{projectRoot}") + atomized = true) // Assert val projectRoot = project.projectDir.absolutePath diff --git a/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/ProcessTaskUtilsTest.kt b/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/ProcessTaskUtilsTest.kt index b6309390dc..6d07f9f1db 100644 --- a/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/ProcessTaskUtilsTest.kt +++ b/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/ProcessTaskUtilsTest.kt @@ -57,7 +57,7 @@ class ProcessTaskUtilsTest { @Test fun `test getDependsOnForTask with direct dependsOn`() { - val project = ProjectBuilder.builder().build() + val project = ProjectBuilder.builder().withName("myApp").build() val taskA = project.tasks.register("taskA").get() val taskB = project.tasks.register("taskB").get() @@ -67,7 +67,7 @@ class ProcessTaskUtilsTest { val dependsOn = getDependsOnForTask(taskA, dependencies) assertNotNull(dependsOn) - assertTrue(dependsOn!!.contains("taskB")) + assertTrue(dependsOn!!.contains("myApp:taskB")) } @Test diff --git a/packages/gradle/src/executors/gradle/get-exclude-task.ts b/packages/gradle/src/executors/gradle/get-exclude-task.ts new file mode 100644 index 0000000000..1a300efee0 --- /dev/null +++ b/packages/gradle/src/executors/gradle/get-exclude-task.ts @@ -0,0 +1,67 @@ +import { ProjectGraph } from 'nx/src/config/project-graph'; + +/** + * Returns Gradle CLI arguments to exclude dependent tasks + * that are not part of the current execution set. + * + * For example, if a project defines `dependsOn: ['lint']` for the `test` target, + * and only `test` is running, this will return: ['lint'] + */ +export function getExcludeTasks( + projectGraph: ProjectGraph, + targets: { project: string; target: string; excludeDependsOn: boolean }[], + runningTaskIds: Set = new Set() +): Set { + const excludes = new Set(); + + for (const { project, target, excludeDependsOn } of targets) { + if (!excludeDependsOn) { + continue; + } + const taskDeps = + projectGraph.nodes[project]?.data?.targets?.[target]?.dependsOn ?? []; + + for (const dep of taskDeps) { + const taskId = typeof dep === 'string' ? dep : dep?.target; + if (taskId && !runningTaskIds.has(taskId)) { + const [projectName, targetName] = taskId.split(':'); + const taskName = + projectGraph.nodes[projectName]?.data?.targets?.[targetName]?.options + ?.taskName; + if (taskName) { + excludes.add(taskName); + } + } + } + } + + return excludes; +} + +export function getAllDependsOn( + projectGraph: ProjectGraph, + projectName: string, + targetName: string, + visited: Set = new Set() +): string[] { + const dependsOn = + projectGraph[projectName]?.data?.targets?.[targetName]?.dependsOn ?? []; + + const allDependsOn: string[] = []; + + for (const dependency of dependsOn) { + if (!visited.has(dependency)) { + visited.add(dependency); + + const [depProjectName, depTargetName] = dependency.split(':'); + allDependsOn.push(dependency); + + // Recursively get dependencies of the current dependency + allDependsOn.push( + ...getAllDependsOn(projectGraph, depProjectName, depTargetName, visited) + ); + } + } + + return allDependsOn; +} diff --git a/packages/gradle/src/executors/gradle/gradle-batch.impl.ts b/packages/gradle/src/executors/gradle/gradle-batch.impl.ts index f247a533a5..ab3b6dbeb1 100644 --- a/packages/gradle/src/executors/gradle/gradle-batch.impl.ts +++ b/packages/gradle/src/executors/gradle/gradle-batch.impl.ts @@ -1,10 +1,10 @@ import { ExecutorContext, output, TaskGraph, workspaceRoot } from '@nx/devkit'; -import { +import runCommandsImpl, { LARGE_BUFFER, RunCommandsOptions, } from 'nx/src/executors/run-commands/run-commands.impl'; import { BatchResults } from 'nx/src/tasks-runner/batch/batch-messages'; -import { gradleExecutorSchema } from './schema'; +import { GradleExecutorSchema } from './schema'; import { findGradlewFile } from '../../utils/exec-gradle'; import { dirname, join } from 'path'; import { execSync } from 'child_process'; @@ -12,6 +12,7 @@ import { createPseudoTerminal, PseudoTerminal, } from 'nx/src/tasks-runner/pseudo-terminal'; +import { getAllDependsOn, getExcludeTasks } from './get-exclude-task'; export const batchRunnerPath = join( __dirname, @@ -25,15 +26,16 @@ interface GradleTask { export default async function gradleBatch( taskGraph: TaskGraph, - inputs: Record, + inputs: Record, overrides: RunCommandsOptions, context: ExecutorContext ): Promise { try { const projectName = taskGraph.tasks[taskGraph.roots[0]]?.target?.project; let projectRoot = context.projectGraph.nodes[projectName]?.data?.root ?? ''; - const gradlewPath = findGradlewFile(join(projectRoot, 'project.json')); // find gradlew near project root - const root = join(context.root, dirname(gradlewPath)); + let gradlewPath = findGradlewFile(join(projectRoot, 'project.json')); // find gradlew near project root + gradlewPath = join(context.root, gradlewPath); + const root = dirname(gradlewPath); // set args with passed in args and overrides in command line const input = inputs[taskGraph.roots[0]]; @@ -48,74 +50,73 @@ export default async function gradleBatch( args.push(...overrides.__overrides_unparsed__); } - const gradlewTasksToRun: Record = Object.entries( - taskGraph.tasks - ).reduce((gradlewTasksToRun, [taskId, task]) => { - const gradlewTaskName = inputs[task.id].taskName; - const testClassName = inputs[task.id].testClassName; - gradlewTasksToRun[taskId] = { - taskName: gradlewTaskName, - testClassName: testClassName, - }; - return gradlewTasksToRun; - }, {}); - const gradlewBatchStart = performance.mark(`gradlew-batch:start`); + const taskIdsWithExclude = []; + const taskIdsWithoutExclude = []; - const usePseudoTerminal = - process.env.NX_NATIVE_COMMAND_RUNNER !== 'false' && - PseudoTerminal.isSupported(); - const command = `java -jar ${batchRunnerPath} --tasks='${JSON.stringify( - gradlewTasksToRun - )}' --workspaceRoot=${root} --args='${args - .join(' ') - .replaceAll("'", '"')}' ${ - process.env.NX_VERBOSE_LOGGING === 'true' ? '' : '--quiet' - }`; - let batchResults; - if (usePseudoTerminal) { - const terminal = createPseudoTerminal(); - await terminal.init(); - - const cp = terminal.runCommand(command, { - cwd: workspaceRoot, - jsEnv: process.env, - quiet: process.env.NX_VERBOSE_LOGGING !== 'true', - }); - const results = await cp.getResults(); - batchResults = results.terminalOutput; - - batchResults = batchResults.replace(command, ''); - const startIndex = batchResults.indexOf('{'); - const endIndex = batchResults.lastIndexOf('}'); - batchResults = batchResults.substring(startIndex, endIndex + 1); - } else { - batchResults = execSync(command, { - cwd: workspaceRoot, - windowsHide: true, - env: process.env, - maxBuffer: LARGE_BUFFER, - }).toString(); + const taskIds = Object.keys(taskGraph.tasks); + for (const taskId of taskIds) { + if (inputs[taskId].excludeDependsOn) { + taskIdsWithExclude.push(taskId); + } else { + taskIdsWithoutExclude.push(taskId); + } } - const gradlewBatchEnd = performance.mark(`gradlew-batch:end`); - performance.measure( - `gradlew-batch`, - gradlewBatchStart.name, - gradlewBatchEnd.name - ); - const gradlewBatchResults = JSON.parse( - batchResults.toString() - ) as BatchResults; - Object.keys(taskGraph.tasks).forEach((taskId) => { - if (!gradlewBatchResults[taskId]) { - gradlewBatchResults[taskId] = { + const allDependsOn = new Set(taskIds); + taskIdsWithoutExclude.forEach((taskId) => { + const [projectName, targetName] = taskId.split(':'); + const dependencies = getAllDependsOn( + context.projectGraph, + projectName, + targetName + ); + dependencies.forEach((dep) => allDependsOn.add(dep)); + }); + + const gradlewTasksToRun: Record = taskIds.reduce( + (gradlewTasksToRun, taskId) => { + const task = taskGraph.tasks[taskId]; + const gradlewTaskName = inputs[task.id].taskName; + const testClassName = inputs[task.id].testClassName; + gradlewTasksToRun[taskId] = { + taskName: gradlewTaskName, + testClassName: testClassName, + }; + return gradlewTasksToRun; + }, + {} + ); + + const excludeTasks = getExcludeTasks( + context.projectGraph, + taskIdsWithExclude.map((taskId) => { + const task = taskGraph.tasks[taskId]; + return { + project: task?.target?.project, + target: task?.target?.target, + excludeDependsOn: inputs[taskId]?.excludeDependsOn, + }; + }), + allDependsOn + ); + + const batchResults = await runTasksInBatch( + gradlewTasksToRun, + excludeTasks, + args, + root + ); + + taskIds.forEach((taskId) => { + if (!batchResults[taskId]) { + batchResults[taskId] = { success: false, terminalOutput: `Gradlew batch failed`, }; } }); - return gradlewBatchResults; + return batchResults; } catch (e) { output.error({ title: `Gradlew batch failed`, @@ -127,3 +128,59 @@ export default async function gradleBatch( }, {} as BatchResults); } } + +async function runTasksInBatch( + gradlewTasksToRun: Record, + excludeTasks: Set, + args: string[], + root: string +): Promise { + const gradlewBatchStart = performance.mark(`gradlew-batch:start`); + + const usePseudoTerminal = + process.env.NX_NATIVE_COMMAND_RUNNER !== 'false' && + PseudoTerminal.isSupported(); + const command = `java -jar ${batchRunnerPath} --tasks='${JSON.stringify( + gradlewTasksToRun + )}' --workspaceRoot=${root} --args='${args + .join(' ') + .replaceAll("'", '"')}' --excludeTasks='${Array.from(excludeTasks).join( + ',' + )}' ${process.env.NX_VERBOSE_LOGGING === 'true' ? '' : '--quiet'}`; + let batchResults; + if (usePseudoTerminal && process.env.NX_VERBOSE_LOGGING !== 'true') { + const terminal = createPseudoTerminal(); + await terminal.init(); + + const cp = terminal.runCommand(command, { + cwd: workspaceRoot, + jsEnv: process.env, + quiet: true, + }); + const results = await cp.getResults(); + terminal.shutdown(0); + batchResults = results.terminalOutput; + + batchResults = batchResults.replace(command, ''); + const startIndex = batchResults.indexOf('{'); + const endIndex = batchResults.lastIndexOf('}'); + // only keep the json part + batchResults = batchResults.substring(startIndex, endIndex + 1); + } else { + batchResults = execSync(command, { + cwd: workspaceRoot, + windowsHide: true, + env: process.env, + maxBuffer: LARGE_BUFFER, + }).toString(); + } + const gradlewBatchEnd = performance.mark(`gradlew-batch:end`); + performance.measure( + `gradlew-batch`, + gradlewBatchStart.name, + gradlewBatchEnd.name + ); + const gradlewBatchResults = JSON.parse(batchResults) as BatchResults; + + return gradlewBatchResults; +} diff --git a/packages/gradle/src/executors/gradle/gradle.impl.ts b/packages/gradle/src/executors/gradle/gradle.impl.ts index 1a277f7cb1..0217388cfb 100644 --- a/packages/gradle/src/executors/gradle/gradle.impl.ts +++ b/packages/gradle/src/executors/gradle/gradle.impl.ts @@ -1,11 +1,12 @@ import { ExecutorContext } from '@nx/devkit'; -import { gradleExecutorSchema } from './schema'; +import { GradleExecutorSchema } from './schema'; import { findGradlewFile } from '../../utils/exec-gradle'; import { dirname, join } from 'node:path'; import runCommandsImpl from 'nx/src/executors/run-commands/run-commands.impl'; +import { getExcludeTasks } from './get-exclude-task'; export default async function gradleExecutor( - options: gradleExecutorSchema, + options: GradleExecutorSchema, context: ExecutorContext ): Promise<{ success: boolean }> { let projectRoot = @@ -22,6 +23,19 @@ export default async function gradleExecutor( if (options.testClassName) { args.push(`--tests`, options.testClassName); } + + getExcludeTasks(context.projectGraph, [ + { + project: context.projectName, + target: context.targetName, + excludeDependsOn: options.excludeDependsOn, + }, + ]).forEach((task) => { + if (task) { + args.push('--exclude-task', task); + } + }); + try { const { success } = await runCommandsImpl( { diff --git a/packages/gradle/src/executors/gradle/schema.d.ts b/packages/gradle/src/executors/gradle/schema.d.ts index 75ce6ee6b1..5d5789e5df 100644 --- a/packages/gradle/src/executors/gradle/schema.d.ts +++ b/packages/gradle/src/executors/gradle/schema.d.ts @@ -1,5 +1,6 @@ -export interface gradleExecutorSchema { +export interface GradleExecutorSchema { taskName: string; testClassName?: string; args?: string[] | string; + excludeDependsOn: boolean; } diff --git a/packages/gradle/src/executors/gradle/schema.json b/packages/gradle/src/executors/gradle/schema.json index 2c1cf0693b..c8833c62a7 100644 --- a/packages/gradle/src/executors/gradle/schema.json +++ b/packages/gradle/src/executors/gradle/schema.json @@ -27,6 +27,12 @@ ], "description": "The arguments to pass to the Gradle task.", "examples": [["--warning-mode", "all"], "--stracktrace"] + }, + "excludeDependsOn": { + "type": "boolean", + "description": "If true, the tasks will not execute its dependsOn tasks (e.g. pass --exclude-task args to gradle command). If false, the task will execute its dependsOn tasks.", + "default": true, + "x-priority": "internal" } }, "required": ["taskName"] diff --git a/packages/gradle/src/plugin/utils/get-project-graph-lines.ts b/packages/gradle/src/plugin/utils/get-project-graph-lines.ts index 9b0fab22f6..d592d4bcfa 100644 --- a/packages/gradle/src/plugin/utils/get-project-graph-lines.ts +++ b/packages/gradle/src/plugin/utils/get-project-graph-lines.ts @@ -30,7 +30,6 @@ export async function getNxProjectGraphLines( '--warning-mode', 'none', ...gradlePluginOptionsArgs, - `-Pcwd=${dirname(gradlewFile)}`, `-PworkspaceRoot=${workspaceRoot}`, process.env.NX_VERBOSE_LOGGING ? '--info' : '', ]); @@ -53,7 +52,7 @@ export async function getNxProjectGraphLines( [ gradlewFile, new Error( - `Could not run 'nxProjectGraph' task. Please run 'nx generate @nx/gradle:init' to generate the necessary tasks.\n\r${e.toString()}` + `Could not run 'nxProjectGraph' task. Please run 'nx generate @nx/gradle:init' to add the necessary plugin dev.nx.gradle.project-graph.\n\r${e.toString()}` ), ], ],