1
1
package dev.nx.gradle
2
2
3
3
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
8
9
import java.io.File
9
- import java.util.logging.Logger
10
- import kotlin.math.max
11
- import kotlin.math.min
12
10
import kotlin.system.exitProcess
13
11
import org.gradle.tooling.GradleConnector
14
12
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" )
21
13
22
14
fun main (args : Array <String >) {
23
15
val options = parseArgs(args)
@@ -32,18 +24,16 @@ fun main(args: Array<String>) {
32
24
exitProcess(1 )
33
25
}
34
26
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
-
40
27
var connection: ProjectConnection ? = null
41
28
42
29
try {
43
30
connection =
44
31
GradleConnector .newConnector().forProjectDirectory(File (options.workspaceRoot)).connect()
45
32
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)
47
37
48
38
val reportJson = Gson ().toJson(results)
49
39
println (reportJson)
@@ -63,297 +53,3 @@ fun main(args: Array<String>) {
63
53
}
64
54
}
65
55
}
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) + " \n Exception 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