Skip to content

Commit 247005c

Browse files
committed
feat(gradle): break down into files
1 parent 6f63c59 commit 247005c

File tree

11 files changed

+353
-318
lines changed

11 files changed

+353
-318
lines changed

packages/gradle/batch-runner/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"outputs": ["{projectRoot}/build"]
1010
},
1111
"lint": {
12-
"command": "./gradlew :batch-runner:check",
12+
"command": "./gradlew :batch-runner:ktfmtCheck",
1313
"cache": true
1414
},
1515
"format": {
Lines changed: 9 additions & 313 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
package dev.nx.gradle
22

33
import com.google.gson.Gson
4-
import com.google.gson.reflect.TypeToken
5-
import dev.nx.gradle.data.GradlewTask
6-
import dev.nx.gradle.data.NxBatchOptions
7-
import java.io.ByteArrayOutputStream
4+
import dev.nx.gradle.cli.configureLogger
5+
import dev.nx.gradle.cli.parseArgs
6+
import dev.nx.gradle.runner.runTasksInParallel
7+
import dev.nx.gradle.util.GradleTaskHelper
8+
import dev.nx.gradle.util.logger
89
import java.io.File
9-
import java.util.logging.Logger
10-
import kotlin.math.max
11-
import kotlin.math.min
1210
import kotlin.system.exitProcess
1311
import org.gradle.tooling.GradleConnector
1412
import org.gradle.tooling.ProjectConnection
15-
import org.gradle.tooling.events.OperationType
16-
import org.gradle.tooling.events.ProgressEvent
17-
import org.gradle.tooling.events.task.*
18-
import org.gradle.tooling.events.test.*
19-
20-
val logger: Logger = Logger.getLogger("NxBatchRunner")
2113

2214
fun main(args: Array<String>) {
2315
val options = parseArgs(args)
@@ -32,18 +24,16 @@ fun main(args: Array<String>) {
3224
exitProcess(1)
3325
}
3426

35-
logger.info("📂 Workspace: ${options.workspaceRoot}")
36-
logger.info("🧩 Tasks: ${options.tasks}")
37-
logger.info("⚙️ Extra Args: ${options.args}")
38-
logger.info("🔇 Quiet: ${options.quiet}")
39-
4027
var connection: ProjectConnection? = null
4128

4229
try {
4330
connection =
4431
GradleConnector.newConnector().forProjectDirectory(File(options.workspaceRoot)).connect()
4532

46-
val results = runTasksInParallel(connection, options.tasks, options.args)
33+
val gradleProject = connection.getModel(org.gradle.tooling.model.GradleProject::class.java)
34+
val allTaskNames = GradleTaskHelper.collectAllTaskNames(gradleProject)
35+
36+
val results = runTasksInParallel(connection, options.tasks, options.args, allTaskNames)
4737

4838
val reportJson = Gson().toJson(results)
4939
println(reportJson)
@@ -63,297 +53,3 @@ fun main(args: Array<String>) {
6353
}
6454
}
6555
}
66-
67-
fun parseArgs(args: Array<String>): NxBatchOptions {
68-
val argMap = mutableMapOf<String, String>()
69-
70-
args.forEach {
71-
when {
72-
it.startsWith("--") && it.contains("=") -> {
73-
val (key, value) = it.split("=", limit = 2)
74-
argMap[key] = value
75-
}
76-
it.startsWith("--") -> {
77-
argMap[it] = "true"
78-
}
79-
}
80-
}
81-
82-
val gson = Gson()
83-
val tasksJson = argMap["--tasks"]
84-
val tasksMap: Map<String, GradlewTask> =
85-
if (tasksJson != null) {
86-
val taskType = object : TypeToken<Map<String, GradlewTask>>() {}.type
87-
gson.fromJson(tasksJson, taskType)
88-
} else emptyMap()
89-
90-
return NxBatchOptions(
91-
workspaceRoot = argMap["--workspaceRoot"] ?: "",
92-
tasks = tasksMap,
93-
args = argMap["--args"] ?: "",
94-
quiet = argMap["--quiet"]?.toBoolean() ?: false)
95-
}
96-
97-
fun configureLogger(quiet: Boolean) {
98-
if (quiet) {
99-
logger.setLevel(java.util.logging.Level.OFF)
100-
logger.useParentHandlers = false
101-
logger.handlers.forEach { it.level = java.util.logging.Level.OFF }
102-
} else {
103-
logger.setLevel(java.util.logging.Level.INFO)
104-
}
105-
}
106-
107-
data class TaskResult(
108-
val success: Boolean,
109-
val startTime: Long,
110-
val endTime: Long,
111-
var terminalOutput: String
112-
)
113-
114-
fun runTasksInParallel(
115-
connection: ProjectConnection,
116-
tasks: Map<String, GradlewTask>,
117-
additionalArgs: String
118-
): Map<String, TaskResult> {
119-
logger.info("▶️ Running all tasks in a single Gradle run: ${tasks.keys.joinToString(", ")}")
120-
121-
val (testTasks, buildTasks) =
122-
tasks.entries.partition {
123-
it.value.testClassName != null || it.value.taskName.endsWith(":test")
124-
}
125-
126-
logger.info("🧪 Test tasks: ${testTasks.map { it.key }.joinToString(", ")}")
127-
logger.info("🏗️ Build tasks: ${buildTasks.map { it.key }.joinToString(", ")}")
128-
129-
val allResults = mutableMapOf<String, TaskResult>()
130-
131-
if (buildTasks.isNotEmpty()) {
132-
allResults.putAll(
133-
runLauncher(connection, buildTasks.associate { it.key to it.value }, additionalArgs, false))
134-
}
135-
136-
if (testTasks.isNotEmpty()) {
137-
allResults.putAll(
138-
runLauncher(connection, testTasks.associate { it.key to it.value }, additionalArgs, true))
139-
}
140-
141-
return allResults
142-
}
143-
144-
fun runLauncher(
145-
connection: ProjectConnection,
146-
tasks: Map<String, GradlewTask>,
147-
additionalArgs: String,
148-
useTestLauncher: Boolean
149-
): Map<String, TaskResult> {
150-
val label = if (useTestLauncher) "🧪 TestLauncher" else "🏗️ BuildLauncher"
151-
val allTaskNames = tasks.values.map { it.taskName }.distinct()
152-
logger.info("$label executing tasks: ${allTaskNames.joinToString(", ")}")
153-
154-
val taskStartTimes = mutableMapOf<String, Long>()
155-
val taskResults = mutableMapOf<String, TaskResult>()
156-
157-
val testTaskStatus = mutableMapOf<String, Boolean>()
158-
val testStartTimes = mutableMapOf<String, Long>()
159-
val testEndTimes = mutableMapOf<String, Long>()
160-
tasks.forEach { (nxTaskId, taskConfig) ->
161-
if (taskConfig.testClassName != null) {
162-
testTaskStatus[nxTaskId] = true
163-
}
164-
}
165-
166-
val buildListener: (ProgressEvent) -> Unit = { event ->
167-
when (event) {
168-
is TaskStartEvent -> {
169-
tasks.entries
170-
.find { it.value.taskName == event.descriptor.taskPath }
171-
?.key
172-
?.let { nxTaskId ->
173-
taskStartTimes[nxTaskId] = min(System.currentTimeMillis(), event.eventTime)
174-
}
175-
}
176-
is TaskFinishEvent -> {
177-
val taskPath = event.descriptor.taskPath
178-
val success =
179-
when (event.result) {
180-
is TaskSuccessResult -> {
181-
logger.info("✅ Task finished successfully: $taskPath")
182-
true
183-
}
184-
is TaskFailureResult -> {
185-
logger.warning("❌ Task failed: $taskPath")
186-
false
187-
}
188-
else -> true
189-
}
190-
191-
tasks.entries
192-
.find { it.value.taskName == taskPath }
193-
?.key
194-
?.let { nxTaskId ->
195-
val endTime = max(System.currentTimeMillis(), event.eventTime)
196-
val startTime = taskStartTimes[nxTaskId] ?: event.result.startTime
197-
taskResults[nxTaskId] = TaskResult(success, startTime, endTime, "")
198-
}
199-
}
200-
}
201-
}
202-
203-
val testListener: (ProgressEvent) -> Unit = { event ->
204-
when (event) {
205-
is TaskStartEvent,
206-
is TaskFinishEvent -> buildListener(event)
207-
is TestStartEvent -> {
208-
209-
(event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className ->
210-
tasks.entries
211-
.find { entry ->
212-
val testClass = entry.value.testClassName
213-
testClass != null && className.endsWith(testClass)
214-
}
215-
?.key
216-
?.let { nxTaskId ->
217-
testStartTimes.compute(nxTaskId) { _, old ->
218-
min(old ?: event.eventTime, event.eventTime)
219-
}
220-
}
221-
}
222-
}
223-
is TestFinishEvent -> {
224-
(event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className ->
225-
tasks.entries
226-
.find { entry ->
227-
val testClass = entry.value.testClassName
228-
testClass != null && className.endsWith(testClass)
229-
}
230-
?.key
231-
?.let { nxTaskId ->
232-
testEndTimes.compute(nxTaskId) { _, old ->
233-
max(old ?: event.eventTime, event.eventTime)
234-
}
235-
236-
when (event.result) {
237-
is TestSuccessResult -> {
238-
// do nothing, it already defaulted to true
239-
logger.info("✅ Test passed: $nxTaskId $className ${event.descriptor.name}")
240-
}
241-
is TestFailureResult -> {
242-
testTaskStatus[nxTaskId] = false
243-
logger.warning("❌ Test failed: $nxTaskId $className ${event.descriptor.name}")
244-
}
245-
is TestSkippedResult -> {
246-
logger.warning("⚠️ Test skipped: $nxTaskId $className ${event.descriptor.name}")
247-
}
248-
else -> {
249-
logger.warning(
250-
"⚠️ Test finished with unknown result: $nxTaskId $className ${event.descriptor.name}")
251-
}
252-
}
253-
}
254-
}
255-
}
256-
}
257-
}
258-
259-
val outputStream = ByteArrayOutputStream()
260-
val errorStream = ByteArrayOutputStream()
261-
262-
val args = buildList {
263-
addAll(listOf("--info", "--continue", "--parallel", "-Dorg.gradle.daemon.idletimeout=10000"))
264-
if (!useTestLauncher) addAll(listOf("--exclude-task", "test"))
265-
addAll(additionalArgs.split(" ").filter { it.isNotBlank() })
266-
}
267-
268-
logger.info("🛠️ Gradle args: ${args.joinToString(" ")}")
269-
270-
val globalStart = System.currentTimeMillis()
271-
var globalOutput: String
272-
273-
try {
274-
if (useTestLauncher) {
275-
connection
276-
.newTestLauncher()
277-
.apply {
278-
forTasks(*allTaskNames.toTypedArray())
279-
tasks.values.mapNotNull { it.testClassName }.forEach { withJvmTestClasses(it) }
280-
withArguments(*args.toTypedArray())
281-
setStandardOutput(outputStream)
282-
setStandardError(errorStream)
283-
addProgressListener(testListener, OperationType.TEST)
284-
}
285-
.run()
286-
} else {
287-
connection
288-
.newBuild()
289-
.apply {
290-
forTasks(*allTaskNames.toTypedArray())
291-
withArguments(*args.toTypedArray())
292-
setStandardOutput(outputStream)
293-
setStandardError(errorStream)
294-
addProgressListener(buildListener, OperationType.TASK)
295-
}
296-
.run()
297-
}
298-
globalOutput = buildTerminalOutput(outputStream, errorStream)
299-
} catch (e: Exception) {
300-
globalOutput =
301-
buildTerminalOutput(outputStream, errorStream) + "\nException occurred: ${e.message}"
302-
logger.warning("💥 Gradle run failed: ${e.message}")
303-
} finally {
304-
outputStream.close()
305-
errorStream.close()
306-
}
307-
308-
val globalEnd = System.currentTimeMillis()
309-
310-
tasks.forEach { (nxTaskId, taskConfig) ->
311-
val isTestTask = taskConfig.testClassName != null
312-
if (isTestTask) {
313-
val success = testTaskStatus[nxTaskId] ?: false
314-
val startTime = testStartTimes[nxTaskId] ?: globalStart
315-
val endTime = testEndTimes[nxTaskId] ?: globalEnd
316-
317-
if (!taskResults.containsKey(nxTaskId)) {
318-
taskResults[nxTaskId] = TaskResult(success, startTime, endTime, "")
319-
}
320-
}
321-
}
322-
val perTaskOutput = splitOutputPerTask(globalOutput)
323-
324-
tasks.forEach { (taskId, taskConfig) ->
325-
val taskOutput = perTaskOutput[taskConfig.taskName] ?: globalOutput
326-
taskResults[taskId]?.let { taskResults[taskId] = it.copy(terminalOutput = taskOutput) }
327-
}
328-
329-
logger.info("✅ Finished ${if (useTestLauncher) "test" else "build"} tasks")
330-
return taskResults
331-
}
332-
333-
fun buildTerminalOutput(stdOut: ByteArrayOutputStream, stdErr: ByteArrayOutputStream): String {
334-
val output = stdOut.toString("UTF-8")
335-
val errorOutput = stdErr.toString("UTF-8")
336-
return buildString {
337-
if (output.isNotBlank()) append(output).append("\n")
338-
if (errorOutput.isNotBlank()) append(errorOutput)
339-
}
340-
}
341-
342-
fun splitOutputPerTask(globalOutput: String): Map<String, String> {
343-
val unescapedOutput = globalOutput.replace("\\u003e", ">").replace("\\n", "\n")
344-
val taskHeaderRegex = Regex("(?=> Task (:[^\\s]+))")
345-
val sections = unescapedOutput.split(taskHeaderRegex)
346-
val taskOutputMap = mutableMapOf<String, String>()
347-
348-
for (section in sections) {
349-
val lines = section.trim().lines()
350-
if (lines.isEmpty()) continue
351-
val header = lines.firstOrNull { it.startsWith("> Task ") }
352-
if (header != null) {
353-
val taskMatch = Regex("> Task (:[^\\s]+)").find(header)
354-
val taskName = taskMatch?.groupValues?.get(1) ?: continue
355-
taskOutputMap[taskName] = section.trim()
356-
}
357-
}
358-
return taskOutputMap
359-
}

0 commit comments

Comments
 (0)