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:
parent
cb25df1c98
commit
5537df6411
@ -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"],
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
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<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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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?> =
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ class CreateNodeForProjectTest {
|
||||
project = project,
|
||||
targetNameOverrides = targetNameOverrides,
|
||||
workspaceRoot = workspaceRoot,
|
||||
cwd = "{projectRoot}")
|
||||
atomized = true)
|
||||
|
||||
// Assert
|
||||
val projectRoot = project.projectDir.absolutePath
|
||||
|
||||
@ -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
|
||||
|
||||
67
packages/gradle/src/executors/gradle/get-exclude-task.ts
Normal file
67
packages/gradle/src/executors/gradle/get-exclude-task.ts
Normal 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;
|
||||
}
|
||||
@ -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,74 +50,73 @@ export default async function gradleBatch(
|
||||
args.push(...overrides.__overrides_unparsed__);
|
||||
}
|
||||
|
||||
const gradlewTasksToRun: Record<string, GradleTask> = 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<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] = {
|
||||
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<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;
|
||||
}
|
||||
|
||||
@ -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(
|
||||
{
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export interface gradleExecutorSchema {
|
||||
export interface GradleExecutorSchema {
|
||||
taskName: string;
|
||||
testClassName?: string;
|
||||
args?: string[] | string;
|
||||
excludeDependsOn: boolean;
|
||||
}
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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()}`
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user