Skip to content

Commit c926ffb

Browse files
committed
feat(maven): add batch executor for multi-task Maven execution
Implement a batch executor for the Maven plugin that allows Nx to execute multiple Maven targets in a single invocation for better performance. Key features: - Single executor (maven.impl.ts) for individual task execution - Batch executor (maven-batch.impl.ts) for multi-task execution - Task grouping by identical phase/goals combinations - Automatic Maven executable detection (mvnw > mvn) - Comprehensive error handling and result tracking - TypeScript-based MVP with foundation for Kotlin enhancement The batch executor follows the same pattern as the Gradle batch executor, grouping tasks with identical targets to minimize Maven process overhead while maintaining per-task result tracking. Future enhancements can include: - Full Kotlin/Java batch runner implementation - Enhanced result parsing for per-module tracking - Parallel execution of different phase groups - Maven Invoker API integration for tighter control
1 parent 66a437a commit c926ffb

File tree

22 files changed

+1720
-24
lines changed

22 files changed

+1720
-24
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Manifest-Version: 1.0
2+
Archiver-Version: Plexus Archiver
3+
Created-By: Apache Maven 3.8.7
4+
Built-By: jason
5+
Build-Jdk: 21.0.8
6+
Main-Class: dev.nx.maven.NxMavenBatchRunnerKt
7+
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
5+
http://maven.apache.org/xsd/maven-4.0.0.xsd">
6+
<modelVersion>4.0.0</modelVersion>
7+
8+
<groupId>dev.nx.maven</groupId>
9+
<artifactId>batch-runner</artifactId>
10+
<version>0.0.7-SNAPSHOT</version>
11+
<packaging>jar</packaging>
12+
13+
<name>Nx Maven Batch Runner</name>
14+
<description>Batch runner for Nx Maven plugin using Maven Invoker API</description>
15+
16+
<properties>
17+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
18+
<maven.compiler.source>11</maven.compiler.source>
19+
<maven.compiler.target>11</maven.compiler.target>
20+
<kotlin.version>1.9.20</kotlin.version>
21+
</properties>
22+
23+
<dependencies>
24+
<!-- Maven Invoker API for programmatic Maven execution -->
25+
<dependency>
26+
<groupId>org.apache.maven.shared</groupId>
27+
<artifactId>maven-invoker</artifactId>
28+
<version>3.1.0</version>
29+
</dependency>
30+
31+
<!-- Logging -->
32+
<dependency>
33+
<groupId>org.slf4j</groupId>
34+
<artifactId>slf4j-api</artifactId>
35+
<version>2.0.9</version>
36+
</dependency>
37+
<dependency>
38+
<groupId>org.slf4j</groupId>
39+
<artifactId>slf4j-simple</artifactId>
40+
<version>2.0.9</version>
41+
</dependency>
42+
43+
<!-- JSON parsing -->
44+
<dependency>
45+
<groupId>com.google.code.gson</groupId>
46+
<artifactId>gson</artifactId>
47+
<version>2.10.1</version>
48+
</dependency>
49+
50+
<!-- Kotlin -->
51+
<dependency>
52+
<groupId>org.jetbrains.kotlin</groupId>
53+
<artifactId>kotlin-stdlib</artifactId>
54+
<version>${kotlin.version}</version>
55+
</dependency>
56+
</dependencies>
57+
58+
<build>
59+
<plugins>
60+
<!-- Kotlin Compiler -->
61+
<plugin>
62+
<groupId>org.jetbrains.kotlin</groupId>
63+
<artifactId>kotlin-maven-plugin</artifactId>
64+
<version>${kotlin.version}</version>
65+
<executions>
66+
<execution>
67+
<id>compile</id>
68+
<phase>compile</phase>
69+
<goals>
70+
<goal>compile</goal>
71+
</goals>
72+
</execution>
73+
<execution>
74+
<id>test-compile</id>
75+
<phase>test-compile</phase>
76+
<goals>
77+
<goal>test-compile</goal>
78+
</goals>
79+
</execution>
80+
</executions>
81+
</plugin>
82+
83+
<!-- Java Compiler -->
84+
<plugin>
85+
<groupId>org.apache.maven.plugins</groupId>
86+
<artifactId>maven-compiler-plugin</artifactId>
87+
<version>3.11.0</version>
88+
<configuration>
89+
<source>11</source>
90+
<target>11</target>
91+
</configuration>
92+
</plugin>
93+
94+
<!-- Shade Plugin for uber JAR -->
95+
<plugin>
96+
<groupId>org.apache.maven.plugins</groupId>
97+
<artifactId>maven-shade-plugin</artifactId>
98+
<version>3.5.0</version>
99+
<executions>
100+
<execution>
101+
<phase>package</phase>
102+
<goals>
103+
<goal>shade</goal>
104+
</goals>
105+
<configuration>
106+
<outputFile>${project.build.directory}/batch-runner.jar</outputFile>
107+
<transformers>
108+
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
109+
<mainClass>dev.nx.maven.NxMavenBatchRunnerKt</mainClass>
110+
</transformer>
111+
</transformers>
112+
<filters>
113+
<filter>
114+
<artifact>*:*</artifact>
115+
<excludes>
116+
<exclude>META-INF/*.SF</exclude>
117+
<exclude>META-INF/*.DSA</exclude>
118+
<exclude>META-INF/*.RSA</exclude>
119+
</excludes>
120+
</filter>
121+
</filters>
122+
</configuration>
123+
</execution>
124+
</executions>
125+
</plugin>
126+
</plugins>
127+
128+
<sourceDirectory>src/main/kotlin</sourceDirectory>
129+
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
130+
</build>
131+
</project>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "maven-batch-runner",
3+
"projectType": "application",
4+
"sourceRoot": "packages/maven/batch-runner/src",
5+
"targets": {
6+
"package": {
7+
"executor": "nx:run-commands",
8+
"options": {
9+
"command": "cd packages/maven/batch-runner && mvn clean package -q",
10+
"cwd": "."
11+
},
12+
"outputs": ["{projectRoot}/target/*.jar"]
13+
}
14+
},
15+
"tags": []
16+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package dev.nx.maven
2+
3+
import com.google.gson.Gson
4+
import dev.nx.maven.cli.ArgParser
5+
import dev.nx.maven.data.TaskResult
6+
import dev.nx.maven.runner.MavenInvokerRunner
7+
import org.slf4j.LoggerFactory
8+
import kotlin.system.exitProcess
9+
10+
private val log = LoggerFactory.getLogger("NxMavenBatchRunner")
11+
private val gson = Gson()
12+
13+
fun main(args: Array<String>) {
14+
try {
15+
// Parse arguments
16+
val options = ArgParser.parseArgs(args)
17+
18+
// Validate required arguments
19+
if (options.workspaceRoot.isBlank()) {
20+
log.error("❌ Missing required argument: --workspaceRoot")
21+
exitProcess(1)
22+
}
23+
24+
if (options.tasks.isEmpty()) {
25+
log.error("❌ Missing required argument: --tasks")
26+
exitProcess(1)
27+
}
28+
29+
log.info("🚀 Starting Maven batch execution")
30+
log.info(" Workspace: ${options.workspaceRoot}")
31+
log.info(" Tasks: ${options.tasks.size}")
32+
log.info(" Quiet: ${options.quiet}, Verbose: ${options.verbose}")
33+
34+
// Run batch execution
35+
val runner = MavenInvokerRunner(options)
36+
val results = runner.runBatch()
37+
38+
// Output results as JSON to specified file
39+
val jsonResults = results.mapValues { (_, result) ->
40+
mapOf(
41+
"success" to result.success,
42+
"terminalOutput" to result.terminalOutput
43+
)
44+
}
45+
46+
val resultsJson = gson.toJson(jsonResults)
47+
48+
// Output JSON to stdout for parent process to capture
49+
// Log to stderr to avoid mixing with JSON output
50+
System.err.println("📝 Results ready, outputting JSON to stdout")
51+
System.err.flush()
52+
53+
// Output JSON result on stdout - must be valid JSON for parent process
54+
println(resultsJson)
55+
System.out.flush()
56+
57+
// Summary
58+
val successCount = results.count { it.value.success }
59+
val failureCount = results.size - successCount
60+
61+
log.info("📊 Summary: ✅ $successCount succeeded, ❌ $failureCount failed")
62+
63+
// Exit with appropriate code
64+
val hasFailures = results.any { !it.value.success }
65+
exitProcess(if (hasFailures) 1 else 0)
66+
67+
} catch (e: Exception) {
68+
log.error("💥 Fatal error: ${e.message}", e)
69+
exitProcess(1)
70+
}
71+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package dev.nx.maven.cli
2+
3+
import com.google.gson.Gson
4+
import com.google.gson.reflect.TypeToken
5+
import dev.nx.maven.data.MavenBatchOptions
6+
import dev.nx.maven.data.MavenBatchTask
7+
import java.io.File
8+
9+
object ArgParser {
10+
private val gson = Gson()
11+
12+
fun parseArgs(args: Array<String>): MavenBatchOptions {
13+
var workspaceRoot = ""
14+
var tasksJson = ""
15+
var argsJson = "[]"
16+
var resultsFile = ""
17+
var quiet = false
18+
var verbose = false
19+
20+
var i = 0
21+
while (i < args.size) {
22+
when {
23+
args[i] == "--workspaceRoot" && i + 1 < args.size -> {
24+
workspaceRoot = args[++i]
25+
}
26+
args[i].startsWith("--workspaceRoot=") -> {
27+
workspaceRoot = args[i].substringAfter("=")
28+
}
29+
args[i] == "--tasks" && i + 1 < args.size -> {
30+
tasksJson = args[++i]
31+
}
32+
args[i].startsWith("--tasks=") -> {
33+
tasksJson = args[i].substringAfter("=")
34+
}
35+
args[i] == "--args" && i + 1 < args.size -> {
36+
argsJson = args[++i]
37+
}
38+
args[i].startsWith("--args=") -> {
39+
argsJson = args[i].substringAfter("=")
40+
}
41+
args[i] == "--resultsFile" && i + 1 < args.size -> {
42+
resultsFile = args[++i]
43+
}
44+
args[i].startsWith("--resultsFile=") -> {
45+
resultsFile = args[i].substringAfter("=")
46+
}
47+
args[i] == "--quiet" -> {
48+
quiet = true
49+
}
50+
args[i] == "--verbose" -> {
51+
verbose = true
52+
}
53+
}
54+
i++
55+
}
56+
57+
// Read task graph from stdin
58+
val taskGraphJson = System.`in`.bufferedReader().readText()
59+
60+
// Parse tasks JSON
61+
val tasksMap = if (tasksJson.isNotEmpty()) {
62+
try {
63+
val type = object : TypeToken<Map<String, Map<String, Any>>>() {}.type
64+
val rawTasks: Map<String, Map<String, Any>> = gson.fromJson(tasksJson, type)
65+
66+
rawTasks.mapValues { (taskId, taskData) ->
67+
MavenBatchTask(
68+
id = taskId,
69+
phase = (taskData["phase"] as? String),
70+
goals = (taskData["goals"] as? List<*>)?.mapNotNull { it as? String } ?: emptyList(),
71+
args = (taskData["args"] as? List<*>)?.mapNotNull { it as? String } ?: emptyList(),
72+
project = (taskData["project"] as? String)
73+
)
74+
}
75+
} catch (e: Exception) {
76+
throw IllegalArgumentException("Failed to parse tasks JSON: $tasksJson", e)
77+
}
78+
} else {
79+
emptyMap()
80+
}
81+
82+
// Parse args JSON
83+
val argsList = if (argsJson.isNotEmpty() && argsJson != "[]") {
84+
try {
85+
val type = object : TypeToken<List<String>>() {}.type
86+
gson.fromJson<List<String>>(argsJson, type)
87+
} catch (e: Exception) {
88+
throw IllegalArgumentException("Failed to parse args JSON: $argsJson", e)
89+
}
90+
} else {
91+
emptyList()
92+
}
93+
94+
// Trim quotes from arguments if present
95+
val cleanWorkspaceRoot = workspaceRoot.trim().trim('"')
96+
val cleanResultsFile = resultsFile.trim().trim('"')
97+
98+
if (cleanWorkspaceRoot.isBlank()) {
99+
throw IllegalArgumentException("workspaceRoot is required")
100+
}
101+
102+
return MavenBatchOptions(
103+
workspaceRoot = cleanWorkspaceRoot,
104+
tasks = tasksMap,
105+
args = argsList,
106+
resultsFile = cleanResultsFile,
107+
quiet = quiet,
108+
verbose = verbose,
109+
taskGraph = taskGraphJson
110+
)
111+
}
112+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.nx.maven.data
2+
3+
data class MavenBatchOptions(
4+
val workspaceRoot: String,
5+
val tasks: Map<String, MavenBatchTask>,
6+
val args: List<String> = emptyList(),
7+
val resultsFile: String = "",
8+
val workspaceDataDirectory: String = "",
9+
val quiet: Boolean = false,
10+
val verbose: Boolean = false,
11+
val taskGraph: String = "{}"
12+
)
13+
14+
data class MavenBatchTask(
15+
val id: String,
16+
val phase: String? = null,
17+
val goals: List<String> = emptyList(),
18+
val args: List<String> = emptyList(),
19+
val project: String? = null
20+
)
21+
22+
data class TaskResult(
23+
val taskId: String,
24+
val success: Boolean,
25+
val terminalOutput: String
26+
)

0 commit comments

Comments
 (0)