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.",
"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"],

View File

@ -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 {}

View File

@ -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")
}

View File

@ -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<String>) {
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<String>) {
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)

View File

@ -29,11 +29,16 @@ fun parseArgs(args: Array<String>): 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) {

View File

@ -4,5 +4,6 @@ data class NxBatchOptions(
val workspaceRoot: String,
val tasks: Map<String, GradleTask>,
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)
}
}
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
}

View File

@ -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<String, GradleTask>,
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(", ")}")
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<String, TaskResult>()
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()
val buildJob = async {
if (buildTasks.isNotEmpty()) {
allResults.putAll(
runBuildLauncher(
connection,
buildTasks.associate { it.key to it.value },
taskNames,
args,
outputStream,
errorStream))
outputStream1,
errorStream1)
} else emptyMap()
}
val testJob = async {
if (testClassTasks.isNotEmpty()) {
allResults.putAll(
runTestLauncher(
connection,
testClassTasks.associate { it.key to it.value },
taskNames,
args,
outputStream,
errorStream))
outputStream2,
errorStream2)
} else emptyMap()
}
return allResults
val allResults = mutableMapOf<String, TaskResult>()
allResults.putAll(buildJob.await())
allResults.putAll(testJob.await())
return@coroutineScope allResults
}
fun runBuildLauncher(
connection: ProjectConnection,
tasks: Map<String, GradleTask>,
taskNames: List<String>,
args: List<String>,
outputStream: ByteArrayOutputStream,
errorStream: ByteArrayOutputStream
): 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 taskResults = mutableMapOf<String, TaskResult>()
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<String, GradleTask>,
taskNames: List<String>,
args: List<String>,
outputStream: ByteArrayOutputStream,
errorStream: ByteArrayOutputStream
): 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 taskResults = mutableMapOf<String, TaskResult>()
val testTaskStatus = mutableMapOf<String, Boolean>()
@ -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

View File

@ -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<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 taskHeaderRegex = Regex("(?=> Task (:[^\\s]+))")
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.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")
}
}
})
}
}
}

View File

@ -19,14 +19,6 @@ class NxProjectGraphReportPlugin : Plugin<Project> {
"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<Project> {
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}"

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 cwd: Property<String>
@get:Input abstract val workspaceRoot: Property<String>
@get:Input abstract val atomized: Property<Boolean>
@get:Input abstract val targetNameOverrides: MapProperty<String, String>
// Don't compute report at configuration time, move it to execution time
@get:Internal // Prevent Gradle from caching this reference
abstract val projectRef: Property<Project>
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) {

View File

@ -9,7 +9,7 @@ fun createNodeForProject(
project: Project,
targetNameOverrides: Map<String, String>,
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<Dependency>,
targetNameOverrides: Map<String, String>,
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<String, Any?> =

View File

@ -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
}

View File

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

View File

@ -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

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 {
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<string, gradleExecutorSchema>,
inputs: Record<string, GradleExecutorSchema>,
overrides: RunCommandsOptions,
context: ExecutorContext
): Promise<BatchResults> {
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,9 +50,32 @@ export default async function gradleBatch(
args.push(...overrides.__overrides_unparsed__);
}
const gradlewTasksToRun: Record<string, GradleTask> = Object.entries(
taskGraph.tasks
).reduce((gradlewTasksToRun, [taskId, task]) => {
const taskIdsWithExclude = [];
const taskIdsWithoutExclude = [];
const taskIds = Object.keys(taskGraph.tasks);
for (const taskId of taskIds) {
if (inputs[taskId].excludeDependsOn) {
taskIdsWithExclude.push(taskId);
} else {
taskIdsWithoutExclude.push(taskId);
}
}
const allDependsOn = new Set<string>(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<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] = {
@ -58,7 +83,58 @@ export default async function gradleBatch(
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 batchResults;
} catch (e) {
output.error({
title: `Gradlew batch failed`,
bodyLines: [e.toString()],
});
return taskGraph.roots.reduce((acc, key) => {
acc[key] = { success: false, terminalOutput: e.toString() };
return acc;
}, {} 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 =
@ -68,25 +144,27 @@ export default async function gradleBatch(
gradlewTasksToRun
)}' --workspaceRoot=${root} --args='${args
.join(' ')
.replaceAll("'", '"')}' ${
process.env.NX_VERBOSE_LOGGING === 'true' ? '' : '--quiet'
}`;
.replaceAll("'", '"')}' --excludeTasks='${Array.from(excludeTasks).join(
','
)}' ${process.env.NX_VERBOSE_LOGGING === 'true' ? '' : '--quiet'}`;
let batchResults;
if (usePseudoTerminal) {
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: process.env.NX_VERBOSE_LOGGING !== 'true',
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, {
@ -102,28 +180,7 @@ export default async function gradleBatch(
gradlewBatchStart.name,
gradlewBatchEnd.name
);
const gradlewBatchResults = JSON.parse(
batchResults.toString()
) as BatchResults;
Object.keys(taskGraph.tasks).forEach((taskId) => {
if (!gradlewBatchResults[taskId]) {
gradlewBatchResults[taskId] = {
success: false,
terminalOutput: `Gradlew batch failed`,
};
}
});
const gradlewBatchResults = JSON.parse(batchResults) as BatchResults;
return gradlewBatchResults;
} catch (e) {
output.error({
title: `Gradlew batch failed`,
bodyLines: [e.toString()],
});
return taskGraph.roots.reduce((acc, key) => {
acc[key] = { success: false, terminalOutput: e.toString() };
return acc;
}, {} as BatchResults);
}
}

View File

@ -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(
{

View File

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

View File

@ -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"]

View File

@ -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()}`
),
],
],