feat(gradle): exclude dependsOn tasks (#30913)

<!-- 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 -->
current gradle task executor will run gradle task as it is. by default,
gradle command will run the tasks itself and its all depends on tasks.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
- add excludeDependsOn in gradle executor schema with default value to
true: this allows gradle command to run tasks without its dependsOn
tasks. this improves performance time
- change project graph plugin (dev.nx.gradle.project-graph) to accept
option atomizer:
```
nxProjectReport {
	atomized = false
}
```
this will disabled atomized targets to be created. check-ci will not
have dependsOn task ci, it will be test instead.
it will not created any ci and ci--* targets, but check-ci will be
created, but dependsOn test:
<img width="605" alt="Screenshot 2025-05-20 at 3 00 39 PM"
src="https://github.com/user-attachments/assets/a2e0ae20-78a1-4848-a063-5825b169c219"
/>

this is what check-ci target looks like with atomized as true:
<img width="917" alt="Screenshot 2025-05-20 at 2 59 34 PM"
src="https://github.com/user-attachments/assets/33c6af0b-3e45-498d-96d0-4f46c54a8159"
/>

- change dependsOn targets to include both project name and task name.
e.g. `spring-boot:checkFormat
`, so when excludeDependsOn is true, it will exclude exact task
- in batch runner, run test runner and build runner as same time

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Emily Xiong 2025-05-26 13:59:04 -04:00 committed by GitHub
parent cb25df1c98
commit 5537df6411
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 401 additions and 176 deletions

View File

@ -24,6 +24,12 @@
], ],
"description": "The arguments to pass to the Gradle task.", "description": "The arguments to pass to the Gradle task.",
"examples": [["--warning-mode", "all"], "--stracktrace"] "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"], "required": ["taskName"],

View File

@ -9,6 +9,7 @@ import {
e2eCwd, e2eCwd,
readJson, readJson,
runCommand, runCommand,
createFile,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
import { mkdirSync, rmdirSync, writeFileSync } from 'fs'; import { mkdirSync, rmdirSync, writeFileSync } from 'fs';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
@ -37,6 +38,8 @@ describe('Nx Import Gradle', () => {
}); });
} }
createFile('.gitignore', '.kotlin/');
try { try {
rmdirSync(tempImportE2ERoot); rmdirSync(tempImportE2ERoot);
} catch {} } catch {}

View File

@ -21,6 +21,7 @@ dependencies {
val toolingApiVersion = "8.13" // Match the Gradle version you're working with val toolingApiVersion = "8.13" // Match the Gradle version you're working with
implementation("org.gradle:gradle-tooling-api:$toolingApiVersion") 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") runtimeOnly("org.slf4j:slf4j-simple:1.7.10")
implementation("com.google.code.gson:gson:2.10.1") implementation("com.google.code.gson:gson:2.10.1")
} }

View File

@ -7,17 +7,20 @@ import dev.nx.gradle.runner.runTasksInParallel
import dev.nx.gradle.util.logger import dev.nx.gradle.util.logger
import java.io.File import java.io.File
import kotlin.system.exitProcess import kotlin.system.exitProcess
import kotlinx.coroutines.runBlocking
import org.gradle.tooling.GradleConnector import org.gradle.tooling.GradleConnector
import org.gradle.tooling.ProjectConnection import org.gradle.tooling.ProjectConnection
fun main(args: Array<String>) { fun main(args: Array<String>) {
val options = parseArgs(args) val options = parseArgs(args)
configureLogger(options.quiet) configureLogger(options.quiet)
logger.info("NxBatchOptions: $options")
if (options.workspaceRoot.isBlank()) { if (options.workspaceRoot.isBlank()) {
logger.severe("❌ Missing required arguments --workspaceRoot") logger.severe("❌ Missing required arguments --workspaceRoot")
exitProcess(1) exitProcess(1)
} }
if (options.tasks.isEmpty()) { if (options.tasks.isEmpty()) {
logger.severe("❌ Missing required arguments --tasks") logger.severe("❌ Missing required arguments --tasks")
exitProcess(1) exitProcess(1)
@ -29,7 +32,9 @@ fun main(args: Array<String>) {
connection = connection =
GradleConnector.newConnector().forProjectDirectory(File(options.workspaceRoot)).connect() 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) val reportJson = Gson().toJson(results)
println(reportJson) println(reportJson)

View File

@ -29,11 +29,16 @@ fun parseArgs(args: Array<String>): NxBatchOptions {
gson.fromJson(tasksJson, taskType) gson.fromJson(tasksJson, taskType)
} else emptyMap() } else emptyMap()
val excludeTasks =
argMap["--excludeTasks"]?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
?: emptyList()
return NxBatchOptions( return NxBatchOptions(
workspaceRoot = argMap["--workspaceRoot"] ?: "", workspaceRoot = argMap["--workspaceRoot"] ?: "",
tasks = tasksMap, tasks = tasksMap,
args = argMap["--args"] ?: "", args = argMap["--args"] ?: "",
quiet = argMap["--quiet"]?.toBoolean() ?: false) quiet = argMap["--quiet"]?.toBoolean() ?: false,
excludeTasks = excludeTasks)
} }
fun configureLogger(quiet: Boolean) { fun configureLogger(quiet: Boolean) {

View File

@ -4,5 +4,6 @@ data class NxBatchOptions(
val workspaceRoot: String, val workspaceRoot: String,
val tasks: Map<String, GradleTask>, val tasks: Map<String, GradleTask>,
val args: String, val args: String,
val quiet: Boolean val quiet: Boolean,
val excludeTasks: List<String>
) )

View File

@ -25,6 +25,7 @@ fun buildListener(
taskStartTimes[nxTaskId] = min(System.currentTimeMillis(), event.eventTime) taskStartTimes[nxTaskId] = min(System.currentTimeMillis(), event.eventTime)
} }
} }
is TaskFinishEvent -> { is TaskFinishEvent -> {
val taskPath = event.descriptor.taskPath val taskPath = event.descriptor.taskPath
val success = val success =
@ -33,10 +34,12 @@ fun buildListener(
logger.info("✅ Task finished successfully: $taskPath") logger.info("✅ Task finished successfully: $taskPath")
true true
} }
is TaskFailureResult -> { is TaskFailureResult -> {
logger.warning("❌ Task failed: $taskPath") logger.warning("❌ Task failed: $taskPath")
false false
} }
else -> true else -> true
} }

View File

@ -3,17 +3,20 @@ package dev.nx.gradle.runner
import dev.nx.gradle.data.GradleTask import dev.nx.gradle.data.GradleTask
import dev.nx.gradle.data.TaskResult import dev.nx.gradle.data.TaskResult
import dev.nx.gradle.runner.OutputProcessor.buildTerminalOutput 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 dev.nx.gradle.util.logger
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.gradle.tooling.ProjectConnection import org.gradle.tooling.ProjectConnection
import org.gradle.tooling.events.OperationType import org.gradle.tooling.events.OperationType
fun runTasksInParallel( suspend fun runTasksInParallel(
connection: ProjectConnection, connection: ProjectConnection,
tasks: Map<String, GradleTask>, tasks: Map<String, GradleTask>,
additionalArgs: String, additionalArgs: String,
): Map<String, TaskResult> { excludeTasks: List<String>
): Map<String, TaskResult> = coroutineScope {
logger.info("▶️ Running all tasks in a single Gradle run: ${tasks.keys.joinToString(", ")}") logger.info("▶️ Running all tasks in a single Gradle run: ${tasks.keys.joinToString(", ")}")
val (testClassTasks, buildTasks) = tasks.entries.partition { it.value.testClassName != null } 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("🧪 Test launcher tasks: ${testClassTasks.joinToString(", ") { it.key }}")
logger.info("🛠️ Build launcher tasks: ${buildTasks.joinToString(", ") { it.key }}") logger.info("🛠️ Build launcher tasks: ${buildTasks.joinToString(", ") { it.key }}")
val allResults = mutableMapOf<String, TaskResult>() val outputStream1 = ByteArrayOutputStream()
val errorStream1 = ByteArrayOutputStream()
val outputStream = ByteArrayOutputStream() val outputStream2 = ByteArrayOutputStream()
val errorStream = ByteArrayOutputStream() val errorStream2 = ByteArrayOutputStream()
val args = buildList { val args = buildList {
// --info is for terminal per task // --info is for terminal per task
// --continue is for continue running tasks if one failed in a batch // --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 // -Dorg.gradle.daemon.idletimeout=10000 is to kill daemon after 10 seconds
addAll( addAll(listOf("--info", "--continue", "-Dorg.gradle.daemon.idletimeout=10000"))
listOf(
"--info",
"--continue",
"--parallel",
"--build-cache",
"-Dorg.gradle.daemon.idletimeout=10000"))
addAll(additionalArgs.split(" ").filter { it.isNotBlank() }) addAll(additionalArgs.split(" ").filter { it.isNotBlank() })
excludeTasks.forEach {
add("--exclude-task")
add(it)
}
} }
logger.info("🏳️ Args: ${args.joinToString(", ")}") logger.info("🏳️ Args: ${args.joinToString(", ")}")
val taskNames = tasks.values.map { it.taskName }.distinct() val buildJob = async {
if (buildTasks.isNotEmpty()) {
if (buildTasks.isNotEmpty()) { runBuildLauncher(
allResults.putAll( connection,
runBuildLauncher( buildTasks.associate { it.key to it.value },
connection, args,
buildTasks.associate { it.key to it.value }, outputStream1,
taskNames, errorStream1)
args, } else emptyMap()
outputStream,
errorStream))
} }
if (testClassTasks.isNotEmpty()) { val testJob = async {
allResults.putAll( if (testClassTasks.isNotEmpty()) {
runTestLauncher( runTestLauncher(
connection, connection,
testClassTasks.associate { it.key to it.value }, testClassTasks.associate { it.key to it.value },
taskNames, args,
args, outputStream2,
outputStream, errorStream2)
errorStream)) } else emptyMap()
} }
return allResults val allResults = mutableMapOf<String, TaskResult>()
allResults.putAll(buildJob.await())
allResults.putAll(testJob.await())
return@coroutineScope allResults
} }
fun runBuildLauncher( fun runBuildLauncher(
connection: ProjectConnection, connection: ProjectConnection,
tasks: Map<String, GradleTask>, tasks: Map<String, GradleTask>,
taskNames: List<String>,
args: List<String>, args: List<String>,
outputStream: ByteArrayOutputStream, outputStream: ByteArrayOutputStream,
errorStream: ByteArrayOutputStream errorStream: ByteArrayOutputStream
): Map<String, TaskResult> { ): Map<String, TaskResult> {
val taskNames = tasks.values.map { it.taskName }.distinct().toTypedArray()
logger.info("📋 Collected ${taskNames.size} unique task names: ${taskNames.joinToString(", ")}")
val taskStartTimes = mutableMapOf<String, Long>() val taskStartTimes = mutableMapOf<String, Long>()
val taskResults = mutableMapOf<String, TaskResult>() val taskResults = mutableMapOf<String, TaskResult>()
val globalStart = System.currentTimeMillis()
var globalOutput: String var globalOutput: String
try { try {
connection connection
.newBuild() .newBuild()
.apply { .apply {
forTasks(*taskNames.toTypedArray()) forTasks(*taskNames)
withArguments(*args.toTypedArray()) withArguments(*args.toTypedArray())
setStandardOutput(outputStream) setStandardOutput(outputStream)
setStandardError(errorStream) setStandardError(errorStream)
@ -97,17 +104,20 @@ fun runBuildLauncher(
} catch (e: Exception) { } catch (e: Exception) {
globalOutput = globalOutput =
buildTerminalOutput(outputStream, errorStream) + "\nException occurred: ${e.message}" 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 { } finally {
outputStream.close() outputStream.close()
errorStream.close() errorStream.close()
} }
val perTaskOutput = splitOutputPerTask(globalOutput) val globalEnd = System.currentTimeMillis()
tasks.forEach { (taskId, taskConfig) -> finalizeTaskResults(
val taskOutput = perTaskOutput[taskConfig.taskName] ?: globalOutput tasks = tasks,
taskResults[taskId]?.let { taskResults[taskId] = it.copy(terminalOutput = taskOutput) } taskResults = taskResults,
} globalOutput = globalOutput,
errorStream = errorStream,
globalStart = globalStart,
globalEnd = globalEnd)
logger.info("\u2705 Finished build tasks") logger.info("\u2705 Finished build tasks")
return taskResults return taskResults
@ -116,11 +126,13 @@ fun runBuildLauncher(
fun runTestLauncher( fun runTestLauncher(
connection: ProjectConnection, connection: ProjectConnection,
tasks: Map<String, GradleTask>, tasks: Map<String, GradleTask>,
taskNames: List<String>,
args: List<String>, args: List<String>,
outputStream: ByteArrayOutputStream, outputStream: ByteArrayOutputStream,
errorStream: ByteArrayOutputStream errorStream: ByteArrayOutputStream
): Map<String, TaskResult> { ): Map<String, TaskResult> {
val taskNames = tasks.values.map { it.taskName }.distinct().toTypedArray()
logger.info("📋 Collected ${taskNames.size} unique task names: ${taskNames.joinToString(", ")}")
val taskStartTimes = mutableMapOf<String, Long>() val taskStartTimes = mutableMapOf<String, Long>()
val taskResults = mutableMapOf<String, TaskResult>() val taskResults = mutableMapOf<String, TaskResult>()
val testTaskStatus = mutableMapOf<String, Boolean>() val testTaskStatus = mutableMapOf<String, Boolean>()
@ -140,8 +152,14 @@ fun runTestLauncher(
connection connection
.newTestLauncher() .newTestLauncher()
.apply { .apply {
forTasks(*taskNames.toTypedArray()) forTasks(*taskNames)
tasks.values.mapNotNull { it.testClassName }.forEach { withJvmTestClasses(it) } tasks.values
.mapNotNull { it.testClassName }
.forEach {
logger.info("Registering test class: $it")
withArguments("--tests", it)
withJvmTestClasses(it)
}
withArguments(*args.toTypedArray()) withArguments(*args.toTypedArray())
setStandardOutput(outputStream) setStandardOutput(outputStream)
setStandardError(errorStream) setStandardError(errorStream)
@ -153,9 +171,10 @@ fun runTestLauncher(
.run() .run()
globalOutput = buildTerminalOutput(outputStream, errorStream) globalOutput = buildTerminalOutput(outputStream, errorStream)
} catch (e: Exception) { } catch (e: Exception) {
logger.warning(errorStream.toString())
globalOutput = globalOutput =
buildTerminalOutput(outputStream, errorStream) + "\nException occurred: ${e.message}" 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 { } finally {
outputStream.close() outputStream.close()
errorStream.close() errorStream.close()
@ -175,11 +194,13 @@ fun runTestLauncher(
} }
} }
val perTaskOutput = splitOutputPerTask(globalOutput) finalizeTaskResults(
tasks.forEach { (taskId, taskConfig) -> tasks = tasks,
val taskOutput = perTaskOutput[taskConfig.taskName] ?: globalOutput taskResults = taskResults,
taskResults[taskId]?.let { taskResults[taskId] = it.copy(terminalOutput = taskOutput) } globalOutput = globalOutput,
} errorStream = errorStream,
globalStart = globalStart,
globalEnd = globalEnd)
logger.info("\u2705 Finished test tasks") logger.info("\u2705 Finished test tasks")
return taskResults return taskResults

View File

@ -1,5 +1,7 @@
package dev.nx.gradle.runner package dev.nx.gradle.runner
import dev.nx.gradle.data.GradleTask
import dev.nx.gradle.data.TaskResult
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
object OutputProcessor { object OutputProcessor {
@ -12,7 +14,42 @@ object OutputProcessor {
} }
} }
fun splitOutputPerTask(globalOutput: String): Map<String, String> { fun finalizeTaskResults(
tasks: Map<String, GradleTask>,
taskResults: MutableMap<String, TaskResult>,
globalOutput: String,
errorStream: ByteArrayOutputStream,
globalStart: Long,
globalEnd: Long
): Map<String, TaskResult> {
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<String, String> {
val unescapedOutput = globalOutput.replace("\\u003e", ">").replace("\\n", "\n") val unescapedOutput = globalOutput.replace("\\u003e", ">").replace("\\n", "\n")
val taskHeaderRegex = Regex("(?=> Task (:[^\\s]+))") val taskHeaderRegex = Regex("(?=> Task (:[^\\s]+))")
val sections = unescapedOutput.split(taskHeaderRegex) val sections = unescapedOutput.split(taskHeaderRegex)

View File

@ -3,8 +3,6 @@ package dev.nx.gradle.runner
import dev.nx.gradle.data.GradleTask import dev.nx.gradle.data.GradleTask
import dev.nx.gradle.data.TaskResult import dev.nx.gradle.data.TaskResult
import dev.nx.gradle.util.logger 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.ProgressEvent
import org.gradle.tooling.events.task.TaskFinishEvent import org.gradle.tooling.events.task.TaskFinishEvent
import org.gradle.tooling.events.task.TaskStartEvent import org.gradle.tooling.events.task.TaskStartEvent
@ -22,38 +20,42 @@ fun testListener(
is TaskStartEvent, is TaskStartEvent,
is TaskFinishEvent -> buildListener(tasks, taskStartTimes, taskResults)(event) is TaskFinishEvent -> buildListener(tasks, taskStartTimes, taskResults)(event)
is TestStartEvent -> { is TestStartEvent -> {
(event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className -> ((event.descriptor as? JvmTestOperationDescriptor)?.className?.substringAfterLast('.')?.let {
simpleClassName ->
tasks.entries tasks.entries
.find { entry -> entry.value.testClassName?.let { className.endsWith(it) } ?: false } .find { entry -> entry.value.testClassName?.let { simpleClassName == it } ?: false }
?.key ?.key
?.let { nxTaskId -> ?.let { nxTaskId ->
testStartTimes.compute(nxTaskId) { _, old -> testStartTimes.computeIfAbsent(nxTaskId) { event.eventTime }
min(old ?: event.eventTime, event.eventTime) logger.info("🏁 Test start at ${event.eventTime}: $nxTaskId $simpleClassName")
}
} }
} })
} }
is TestFinishEvent -> { is TestFinishEvent -> {
(event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className -> ((event.descriptor as? JvmTestOperationDescriptor)?.className?.substringAfterLast('.')?.let {
simpleClassName ->
tasks.entries tasks.entries
.find { entry -> entry.value.testClassName?.let { className.endsWith(it) } ?: false } .find { entry -> entry.value.testClassName?.let { simpleClassName == it } ?: false }
?.key ?.key
?.let { nxTaskId -> ?.let { nxTaskId ->
testEndTimes.compute(nxTaskId) { _, old -> testEndTimes.compute(nxTaskId) { _, _ -> event.result.endTime }
max(old ?: event.eventTime, event.eventTime)
}
when (event.result) { 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 -> { is TestFailureResult -> {
testTaskStatus[nxTaskId] = false testTaskStatus[nxTaskId] = false
logger.warning("\u274C Test failed: $nxTaskId $className") logger.warning("\u274C Test failed: $nxTaskId $simpleClassName")
} }
is TestSkippedResult -> is TestSkippedResult ->
logger.warning("\u26A0\uFE0F Test skipped: $nxTaskId $className") logger.warning("\u26A0\uFE0F Test skipped: $nxTaskId $simpleClassName")
else -> logger.warning("\u26A0\uFE0F Unknown test result: $nxTaskId $className")
else ->
logger.warning("\u26A0\uFE0F Unknown test result: $nxTaskId $simpleClassName")
} }
} }
} })
} }
} }
} }

View File

@ -19,14 +19,6 @@ class NxProjectGraphReportPlugin : Plugin<Project> {
"default-hash" "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 = val workspaceRootProperty =
project.findProperty("workspaceRoot")?.toString() project.findProperty("workspaceRoot")?.toString()
?: run { ?: run {
@ -43,7 +35,6 @@ class NxProjectGraphReportPlugin : Plugin<Project> {
task.projectRef.set(project) task.projectRef.set(project)
task.hash.set(hashProperty) task.hash.set(hashProperty)
task.targetNameOverrides.set(targetNameOverrides) task.targetNameOverrides.set(targetNameOverrides)
task.cwd.set(cwdProperty)
task.workspaceRoot.set(workspaceRootProperty) task.workspaceRoot.set(workspaceRootProperty)
task.description = "Create Nx project report for ${project.name}" task.description = "Create Nx project report for ${project.name}"

View File

@ -24,16 +24,20 @@ abstract class NxProjectReportTask @Inject constructor(private val projectLayout
@get:Input abstract val hash: Property<String> @get:Input abstract val hash: Property<String>
@get:Input abstract val cwd: Property<String>
@get:Input abstract val workspaceRoot: Property<String> @get:Input abstract val workspaceRoot: Property<String>
@get:Input abstract val atomized: Property<Boolean>
@get:Input abstract val targetNameOverrides: MapProperty<String, String> @get:Input abstract val targetNameOverrides: MapProperty<String, String>
// Don't compute report at configuration time, move it to execution time // Don't compute report at configuration time, move it to execution time
@get:Internal // Prevent Gradle from caching this reference @get:Internal // Prevent Gradle from caching this reference
abstract val projectRef: Property<Project> abstract val projectRef: Property<Project>
init {
atomized.convention(true)
}
@get:OutputFile @get:OutputFile
val outputFile: File val outputFile: File
get() = projectLayout.buildDirectory.file("nx/${projectName.get()}.json").get().asFile 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()} Apply task action NxProjectReportTask for ${projectName.get()}")
logger.info("${Date()} Hash input: ${hash.get()}") logger.info("${Date()} Hash input: ${hash.get()}")
logger.info("${Date()} Target Name Overrides ${targetNameOverrides.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 project = projectRef.get() // Get project reference at execution time
val report = val report =
createNodeForProject( createNodeForProject(
project, project,
targetNameOverrides.get(), targetNameOverrides.get(),
workspaceRoot.get(), workspaceRoot.get(),
cwd.get()) // Compute report at execution time atomized.get()) // Compute report at execution time
val reportJson = gson.toJson(report) val reportJson = gson.toJson(report)
if (outputFile.exists() && outputFile.readText() == reportJson) { if (outputFile.exists() && outputFile.readText() == reportJson) {

View File

@ -9,7 +9,7 @@ fun createNodeForProject(
project: Project, project: Project,
targetNameOverrides: Map<String, String>, targetNameOverrides: Map<String, String>,
workspaceRoot: String, workspaceRoot: String,
cwd: String atomized: Boolean
): GradleNodeReport { ): GradleNodeReport {
val logger = project.logger val logger = project.logger
logger.info("${Date()} ${project.name} createNodeForProject: get nodes and dependencies") logger.info("${Date()} ${project.name} createNodeForProject: get nodes and dependencies")
@ -31,7 +31,8 @@ fun createNodeForProject(
try { try {
val gradleTargets: GradleTargets = val gradleTargets: GradleTargets =
processTargetsForProject(project, dependencies, targetNameOverrides, workspaceRoot, cwd) processTargetsForProject(
project, dependencies, targetNameOverrides, workspaceRoot, atomized)
val projectRoot = project.projectDir.path val projectRoot = project.projectDir.path
val projectNode = val projectNode =
ProjectNode( ProjectNode(
@ -61,7 +62,7 @@ fun processTargetsForProject(
dependencies: MutableSet<Dependency>, dependencies: MutableSet<Dependency>,
targetNameOverrides: Map<String, String>, targetNameOverrides: Map<String, String>,
workspaceRoot: String, workspaceRoot: String,
cwd: String atomized: Boolean
): GradleTargets { ): GradleTargets {
val targets: NxTargets = mutableMapOf() val targets: NxTargets = mutableMapOf()
val targetGroups: TargetGroups = mutableMapOf() val targetGroups: TargetGroups = mutableMapOf()
@ -111,7 +112,7 @@ fun processTargetsForProject(
targets[taskName] = target targets[taskName] = target
if (hasCiTestTarget && task.name.startsWith("compileTest")) { if (hasCiTestTarget && task.name.startsWith("compileTest") && atomized) {
addTestCiTargets( addTestCiTargets(
task.inputs.sourceFiles, task.inputs.sourceFiles,
projectBuildPath, projectBuildPath,
@ -124,7 +125,7 @@ fun processTargetsForProject(
ciTestTargetName!!) ciTestTargetName!!)
} }
if (hasCiIntTestTarget && task.name.startsWith("compileIntTest")) { if (hasCiIntTestTarget && task.name.startsWith("compileIntTest") && atomized) {
addTestCiTargets( addTestCiTargets(
task.inputs.sourceFiles, task.inputs.sourceFiles,
projectBuildPath, projectBuildPath,
@ -137,14 +138,19 @@ fun processTargetsForProject(
ciIntTestTargetName!!) ciIntTestTargetName!!)
} }
if (task.name == "check" && (hasCiTestTarget || hasCiIntTestTarget)) { if (task.name == "check") {
val replacedDependencies = val replacedDependencies =
(target["dependsOn"] as? List<*>)?.map { dep -> (target["dependsOn"] as? List<*>)?.map { dep ->
when (dep.toString()) { val dependsOn = dep.toString()
testTargetName -> ciTestTargetName ?: dep if (hasCiTestTarget && dependsOn == "${project.name}:$testTargetName" && atomized) {
intTestTargetName -> ciIntTestTargetName ?: dep "${project.name}:$ciTestTargetName"
else -> dep } else if (hasCiIntTestTarget &&
}.toString() dependsOn == "${project.name}:$intTestTargetName" &&
atomized) {
"${project.name}:$ciIntTestTargetName"
} else {
dep
}
} ?: emptyList() } ?: emptyList()
val newTarget: MutableMap<String, Any?> = val newTarget: MutableMap<String, Any?> =

View File

@ -180,12 +180,7 @@ fun getDependsOnForTask(
// Check if this task name needs to be overridden // Check if this task name needs to be overridden
val taskName = targetNameOverrides.getOrDefault(depTask.name + "TargetName", depTask.name) val taskName = targetNameOverrides.getOrDefault(depTask.name + "TargetName", depTask.name)
val overriddenTaskName = val overriddenTaskName = "${depProject.name}:${taskName}"
if (depProject == taskProject) {
taskName
} else {
"${depProject.name}:${taskName}"
}
overriddenTaskName overriddenTaskName
} }

View File

@ -32,7 +32,7 @@ class CreateNodeForProjectTest {
project = project, project = project,
targetNameOverrides = targetNameOverrides, targetNameOverrides = targetNameOverrides,
workspaceRoot = workspaceRoot, workspaceRoot = workspaceRoot,
cwd = "{projectRoot}") atomized = true)
// Assert // Assert
val projectRoot = project.projectDir.absolutePath val projectRoot = project.projectDir.absolutePath

View File

@ -57,7 +57,7 @@ class ProcessTaskUtilsTest {
@Test @Test
fun `test getDependsOnForTask with direct dependsOn`() { 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 taskA = project.tasks.register("taskA").get()
val taskB = project.tasks.register("taskB").get() val taskB = project.tasks.register("taskB").get()
@ -67,7 +67,7 @@ class ProcessTaskUtilsTest {
val dependsOn = getDependsOnForTask(taskA, dependencies) val dependsOn = getDependsOnForTask(taskA, dependencies)
assertNotNull(dependsOn) assertNotNull(dependsOn)
assertTrue(dependsOn!!.contains("taskB")) assertTrue(dependsOn!!.contains("myApp:taskB"))
} }
@Test @Test

View File

@ -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<string> = new Set()
): Set<string> {
const excludes = new Set<string>();
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<string> = 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;
}

View File

@ -1,10 +1,10 @@
import { ExecutorContext, output, TaskGraph, workspaceRoot } from '@nx/devkit'; import { ExecutorContext, output, TaskGraph, workspaceRoot } from '@nx/devkit';
import { import runCommandsImpl, {
LARGE_BUFFER, LARGE_BUFFER,
RunCommandsOptions, RunCommandsOptions,
} from 'nx/src/executors/run-commands/run-commands.impl'; } from 'nx/src/executors/run-commands/run-commands.impl';
import { BatchResults } from 'nx/src/tasks-runner/batch/batch-messages'; 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 { findGradlewFile } from '../../utils/exec-gradle';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
@ -12,6 +12,7 @@ import {
createPseudoTerminal, createPseudoTerminal,
PseudoTerminal, PseudoTerminal,
} from 'nx/src/tasks-runner/pseudo-terminal'; } from 'nx/src/tasks-runner/pseudo-terminal';
import { getAllDependsOn, getExcludeTasks } from './get-exclude-task';
export const batchRunnerPath = join( export const batchRunnerPath = join(
__dirname, __dirname,
@ -25,15 +26,16 @@ interface GradleTask {
export default async function gradleBatch( export default async function gradleBatch(
taskGraph: TaskGraph, taskGraph: TaskGraph,
inputs: Record<string, gradleExecutorSchema>, inputs: Record<string, GradleExecutorSchema>,
overrides: RunCommandsOptions, overrides: RunCommandsOptions,
context: ExecutorContext context: ExecutorContext
): Promise<BatchResults> { ): Promise<BatchResults> {
try { try {
const projectName = taskGraph.tasks[taskGraph.roots[0]]?.target?.project; const projectName = taskGraph.tasks[taskGraph.roots[0]]?.target?.project;
let projectRoot = context.projectGraph.nodes[projectName]?.data?.root ?? ''; let projectRoot = context.projectGraph.nodes[projectName]?.data?.root ?? '';
const gradlewPath = findGradlewFile(join(projectRoot, 'project.json')); // find gradlew near project root let gradlewPath = findGradlewFile(join(projectRoot, 'project.json')); // find gradlew near project root
const root = join(context.root, dirname(gradlewPath)); gradlewPath = join(context.root, gradlewPath);
const root = dirname(gradlewPath);
// set args with passed in args and overrides in command line // set args with passed in args and overrides in command line
const input = inputs[taskGraph.roots[0]]; const input = inputs[taskGraph.roots[0]];
@ -48,74 +50,73 @@ export default async function gradleBatch(
args.push(...overrides.__overrides_unparsed__); args.push(...overrides.__overrides_unparsed__);
} }
const gradlewTasksToRun: Record<string, GradleTask> = Object.entries( const taskIdsWithExclude = [];
taskGraph.tasks const taskIdsWithoutExclude = [];
).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 usePseudoTerminal = const taskIds = Object.keys(taskGraph.tasks);
process.env.NX_NATIVE_COMMAND_RUNNER !== 'false' && for (const taskId of taskIds) {
PseudoTerminal.isSupported(); if (inputs[taskId].excludeDependsOn) {
const command = `java -jar ${batchRunnerPath} --tasks='${JSON.stringify( taskIdsWithExclude.push(taskId);
gradlewTasksToRun } else {
)}' --workspaceRoot=${root} --args='${args taskIdsWithoutExclude.push(taskId);
.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 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) => { const allDependsOn = new Set<string>(taskIds);
if (!gradlewBatchResults[taskId]) { taskIdsWithoutExclude.forEach((taskId) => {
gradlewBatchResults[taskId] = { const [projectName, targetName] = taskId.split(':');
const dependencies = getAllDependsOn(
context.projectGraph,
projectName,
targetName
);
dependencies.forEach((dep) => allDependsOn.add(dep));
});
const gradlewTasksToRun: Record<string, GradleTask> = 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, success: false,
terminalOutput: `Gradlew batch failed`, terminalOutput: `Gradlew batch failed`,
}; };
} }
}); });
return gradlewBatchResults; return batchResults;
} catch (e) { } catch (e) {
output.error({ output.error({
title: `Gradlew batch failed`, title: `Gradlew batch failed`,
@ -127,3 +128,59 @@ export default async function gradleBatch(
}, {} as BatchResults); }, {} as BatchResults);
} }
} }
async function runTasksInBatch(
gradlewTasksToRun: Record<string, GradleTask>,
excludeTasks: Set<string>,
args: string[],
root: string
): Promise<BatchResults> {
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;
}

View File

@ -1,11 +1,12 @@
import { ExecutorContext } from '@nx/devkit'; import { ExecutorContext } from '@nx/devkit';
import { gradleExecutorSchema } from './schema'; import { GradleExecutorSchema } from './schema';
import { findGradlewFile } from '../../utils/exec-gradle'; import { findGradlewFile } from '../../utils/exec-gradle';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import runCommandsImpl from 'nx/src/executors/run-commands/run-commands.impl'; import runCommandsImpl from 'nx/src/executors/run-commands/run-commands.impl';
import { getExcludeTasks } from './get-exclude-task';
export default async function gradleExecutor( export default async function gradleExecutor(
options: gradleExecutorSchema, options: GradleExecutorSchema,
context: ExecutorContext context: ExecutorContext
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
let projectRoot = let projectRoot =
@ -22,6 +23,19 @@ export default async function gradleExecutor(
if (options.testClassName) { if (options.testClassName) {
args.push(`--tests`, 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 { try {
const { success } = await runCommandsImpl( const { success } = await runCommandsImpl(
{ {

View File

@ -1,5 +1,6 @@
export interface gradleExecutorSchema { export interface GradleExecutorSchema {
taskName: string; taskName: string;
testClassName?: string; testClassName?: string;
args?: string[] | string; args?: string[] | string;
excludeDependsOn: boolean;
} }

View File

@ -27,6 +27,12 @@
], ],
"description": "The arguments to pass to the Gradle task.", "description": "The arguments to pass to the Gradle task.",
"examples": [["--warning-mode", "all"], "--stracktrace"] "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"] "required": ["taskName"]

View File

@ -30,7 +30,6 @@ export async function getNxProjectGraphLines(
'--warning-mode', '--warning-mode',
'none', 'none',
...gradlePluginOptionsArgs, ...gradlePluginOptionsArgs,
`-Pcwd=${dirname(gradlewFile)}`,
`-PworkspaceRoot=${workspaceRoot}`, `-PworkspaceRoot=${workspaceRoot}`,
process.env.NX_VERBOSE_LOGGING ? '--info' : '', process.env.NX_VERBOSE_LOGGING ? '--info' : '',
]); ]);
@ -53,7 +52,7 @@ export async function getNxProjectGraphLines(
[ [
gradlewFile, gradlewFile,
new Error( 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()}`
), ),
], ],
], ],