Skip to content

Commit 864932d

Browse files
committed
feat(mvnd): add Maven Daemon support as rtk mvnd subcommand
Adds a parallel 'rtk mvnd' subcommand that reuses the mvn filters (test, compile, checkstyle:check, dependency:tree, passthrough) but invokes the mvnd binary directly (bypassing mvnw, which the daemon does not use). - New MvnBinary enum (Mvn auto-detects mvnw; Mvnd always uses mvnd) - Threaded through run_test/run_compile/run_checkstyle/run_dep_tree/run_other - Tracking labels keep 'mvn' and 'mvnd' separate in rtk gain - Discover rule added for mvnd → rtk mvnd rewrites Addresses review comment on PR rtk-ai#1089.
1 parent 781ec7b commit 864932d

5 files changed

Lines changed: 165 additions & 69 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ rtk cargo test # Cargo tests (-90%)
181181
rtk rake test # Ruby minitest (-90%)
182182
rtk rspec # RSpec tests (JSON, -60%+)
183183
rtk mvn test # Maven tests (-99%)
184+
rtk mvnd test # Maven Daemon tests (same filter, same savings)
184185
rtk err <cmd> # Filter errors only from any command
185186
rtk test <cmd> # Generic test wrapper - failures only (-90%)
186187
```

src/cmds/java/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
55
## Specifics
66

7-
- **mvn_cmd.rs** handles Maven (`mvn`) and Maven Wrapper (`mvnw`) commands
8-
- Auto-detects `mvnw` wrapper in project root; falls back to system `mvn`
7+
- **mvn_cmd.rs** handles Maven (`mvn`), Maven Wrapper (`mvnw`), and Maven Daemon (`mvnd`) commands
8+
- `rtk mvn`: auto-detects `mvnw` wrapper in project root; falls back to system `mvn`
9+
- `rtk mvnd`: always invokes the Maven Daemon (`mvnd`) — the wrapper is bypassed because `mvnd` is a separate long-lived JVM daemon; metrics are tracked as `mvnd <goal>` in `rtk gain` so mvn/mvnd savings stay separate
910
- `mvn test` uses a state-machine parser (Preamble → Testing → Summary → Done) for 97-99%+ savings on real-world output
1011
- `mvn compile` uses line filtering to strip `[INFO]` noise, download progress, JVM/native-access warnings, and plugin chatter (jOOQ codegen, Liquibase, npm/React builds, typescript-generator). Also routes `process-classes` and `test-compile` through the same filter (same noise profile)
1112
- `mvn checkstyle:check` (aliased as `checkstyle`) compacts violation lines to `path:line:col [Rule] message`, strips mvn startup noise and Help-link boilerplate, keeps `N Checkstyle violations` summary and BUILD SUCCESS/FAILURE

src/cmds/java/mvn_cmd.rs

Lines changed: 129 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -95,46 +95,75 @@ fn is_mvn_startup_noise(line: &str) -> bool {
9595
false
9696
}
9797

98-
/// Auto-detect mvnw wrapper; fall back to system `mvn`.
99-
fn mvn_command() -> std::process::Command {
100-
if Path::new("mvnw").exists() {
101-
resolved_command("./mvnw")
102-
} else {
103-
resolved_command("mvn")
98+
/// Which Maven binary to invoke. `Mvn` auto-detects the `mvnw` wrapper and
99+
/// falls back to system `mvn`; `Mvnd` always uses the Maven Daemon (`mvnd`),
100+
/// which is incompatible with the wrapper.
101+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102+
pub enum MvnBinary {
103+
Mvn,
104+
Mvnd,
105+
}
106+
107+
impl MvnBinary {
108+
/// Display name used in tracking labels, verbose logs, and error messages.
109+
fn display(self) -> &'static str {
110+
match self {
111+
MvnBinary::Mvn => "mvn",
112+
MvnBinary::Mvnd => "mvnd",
113+
}
104114
}
105115
}
106116

107-
/// Run `mvn test` with state-machine filtered output.
108-
pub fn run_test(args: &[String], verbose: u8) -> Result<i32> {
109-
let mut cmd = mvn_command();
117+
/// Build the base command for the selected binary. For `Mvn`, auto-detects the
118+
/// `mvnw` wrapper and falls back to system `mvn`. For `Mvnd`, always invokes
119+
/// `mvnd` directly (the daemon does not use wrapper scripts).
120+
fn mvn_command(binary: MvnBinary) -> std::process::Command {
121+
match binary {
122+
MvnBinary::Mvn => {
123+
if Path::new("mvnw").exists() {
124+
resolved_command("./mvnw")
125+
} else {
126+
resolved_command("mvn")
127+
}
128+
}
129+
MvnBinary::Mvnd => resolved_command("mvnd"),
130+
}
131+
}
132+
133+
/// Run `<binary> test` with state-machine filtered output.
134+
pub fn run_test(binary: MvnBinary, args: &[String], verbose: u8) -> Result<i32> {
135+
let mut cmd = mvn_command(binary);
110136
cmd.arg("test");
111137

112138
for arg in args {
113139
cmd.arg(arg);
114140
}
115141

142+
let bin = binary.display();
116143
if verbose > 0 {
117-
eprintln!("Running: mvn test {}", args.join(" "));
144+
eprintln!("Running: {bin} test {}", args.join(" "));
118145
}
119146

120147
let started_at = std::time::SystemTime::now();
121148
let cwd = std::env::current_dir().unwrap_or_else(|e| {
122-
eprintln!("rtk mvn: could not determine cwd: {e}");
149+
eprintln!("rtk {bin}: could not determine cwd: {e}");
123150
std::path::PathBuf::from(".")
124151
});
125152
let app_pkgs = crate::cmds::java::pom_groupid::detect(&cwd);
126153

127154
let cwd_for_filter = cwd.clone();
128155

156+
let tool_name = format!("{bin} test");
157+
let tee_label = format!("{bin}_test");
129158
runner::run_filtered(
130159
cmd,
131-
"mvn test",
160+
&tool_name,
132161
&args.join(" "),
133162
move |raw: &str| {
134163
let filtered = filter_mvn_test(raw);
135164
enrich_with_reports(&filtered, &cwd_for_filter, started_at, &app_pkgs)
136165
},
137-
runner::RunOptions::with_tee("mvn_test"),
166+
runner::RunOptions::with_tee(&tee_label),
138167
)
139168
}
140169

@@ -143,101 +172,110 @@ pub fn run_test(args: &[String], verbose: u8) -> Result<i32> {
143172
/// `compile` is itself a Maven lifecycle phase (not a goal name we invented),
144173
/// so no implicit default is added when `args` is empty — `mvn compile` runs
145174
/// the compile phase directly.
146-
pub fn run_compile(args: &[String], verbose: u8) -> Result<i32> {
147-
run_compile_like("compile", args, verbose)
175+
pub fn run_compile(binary: MvnBinary, args: &[String], verbose: u8) -> Result<i32> {
176+
run_compile_like(binary, "compile", args, verbose)
148177
}
149178

150-
/// Shared implementation for compile-phase-like goals: runs `mvn <goal> <args>`
179+
/// Shared implementation for compile-phase-like goals: runs `<binary> <goal> <args>`
151180
/// through `filter_mvn_compile`. Used directly by `run_compile` and reused by
152181
/// `run_other` to route `process-classes` / `test-compile` through the same
153182
/// filter while preserving the original goal name in the invocation and in
154183
/// the tracking label.
155-
fn run_compile_like(goal: &str, args: &[String], verbose: u8) -> Result<i32> {
156-
let mut cmd = mvn_command();
184+
fn run_compile_like(binary: MvnBinary, goal: &str, args: &[String], verbose: u8) -> Result<i32> {
185+
let mut cmd = mvn_command(binary);
157186
cmd.arg(goal);
158187
for arg in args {
159188
cmd.arg(arg);
160189
}
161190

191+
let bin = binary.display();
162192
if verbose > 0 {
163-
eprintln!("Running: mvn {} {}", goal, args.join(" "));
193+
eprintln!("Running: {bin} {} {}", goal, args.join(" "));
164194
}
165195

166-
let (tool_name, tee_label) = compile_like_labels(goal);
196+
let (tool_name, tee_label) = compile_like_labels(binary, goal);
167197

168198
runner::run_filtered(
169199
cmd,
170-
tool_name,
200+
&tool_name,
171201
&args.join(" "),
172202
filter_mvn_compile,
173-
runner::RunOptions::with_tee(tee_label),
203+
runner::RunOptions::with_tee(&tee_label),
174204
)
175205
}
176206

177-
/// Run `mvn checkstyle:check` with compact output — strips mvn/JVM startup
207+
/// Run `<binary> checkstyle:check` with compact output — strips mvn/JVM startup
178208
/// noise, keeps violations and BUILD SUCCESS/FAILURE summary.
179-
pub fn run_checkstyle(args: &[String], verbose: u8) -> Result<i32> {
180-
let mut cmd = mvn_command();
209+
pub fn run_checkstyle(binary: MvnBinary, args: &[String], verbose: u8) -> Result<i32> {
210+
let mut cmd = mvn_command(binary);
181211
cmd.arg("checkstyle:check");
182212
for arg in args {
183213
cmd.arg(arg);
184214
}
185215

216+
let bin = binary.display();
186217
if verbose > 0 {
187-
eprintln!("Running: mvn checkstyle:check {}", args.join(" "));
218+
eprintln!("Running: {bin} checkstyle:check {}", args.join(" "));
188219
}
189220

221+
let tool_name = format!("{bin} checkstyle:check");
222+
let tee_label = format!("{bin}_checkstyle");
190223
runner::run_filtered(
191224
cmd,
192-
"mvn checkstyle:check",
225+
&tool_name,
193226
&args.join(" "),
194227
filter_mvn_checkstyle,
195-
runner::RunOptions::with_tee("mvn_checkstyle"),
228+
runner::RunOptions::with_tee(&tee_label),
196229
)
197230
}
198231

199-
/// Run `mvn dependency:tree` with filtered output — strips duplicates and boilerplate.
200-
pub fn run_dep_tree(args: &[String], verbose: u8) -> Result<i32> {
201-
let mut cmd = mvn_command();
232+
/// Run `<binary> dependency:tree` with filtered output — strips duplicates and boilerplate.
233+
pub fn run_dep_tree(binary: MvnBinary, args: &[String], verbose: u8) -> Result<i32> {
234+
let mut cmd = mvn_command(binary);
202235
cmd.arg("dependency:tree");
203236

204237
for arg in args {
205238
cmd.arg(arg);
206239
}
207240

241+
let bin = binary.display();
208242
if verbose > 0 {
209-
eprintln!("Running: mvn dependency:tree {}", args.join(" "));
243+
eprintln!("Running: {bin} dependency:tree {}", args.join(" "));
210244
}
211245

246+
let tool_name = format!("{bin} dependency:tree");
247+
let tee_label = format!("{bin}_dep_tree");
212248
runner::run_filtered(
213249
cmd,
214-
"mvn dependency:tree",
250+
&tool_name,
215251
&args.join(" "),
216252
filter_mvn_dep_tree,
217-
runner::RunOptions::with_tee("mvn_dep_tree"),
253+
runner::RunOptions::with_tee(&tee_label),
218254
)
219255
}
220256

221257
/// Goals whose output looks like `mvn compile` (same noise profile: plugin
222-
/// codegen, npm lifecycle, Liquibase, Docker). Tuples are
223-
/// `(goal, tool_name, tee_label)` — single source of truth for routing,
224-
/// tracking labels, and tee filenames.
225-
const COMPILE_LIKE_GOALS: &[(&str, &str, &str)] = &[
226-
("compile", "mvn compile", "mvn_compile"),
227-
("process-classes", "mvn process-classes", "mvn_process_classes"),
228-
("test-compile", "mvn test-compile", "mvn_test_compile"),
258+
/// codegen, npm lifecycle, Liquibase, Docker). Tuples are `(goal, tee_slug)`
259+
/// — tool names are prefixed with the active binary at runtime to keep mvn
260+
/// and mvnd metrics separate in `rtk gain`.
261+
const COMPILE_LIKE_GOALS: &[(&str, &str)] = &[
262+
("compile", "compile"),
263+
("process-classes", "process_classes"),
264+
("test-compile", "test_compile"),
229265
];
230266

231-
/// Look up the `(tool_name, tee_label)` pair for a compile-like goal. Callers
232-
/// are gated on `route_goal` / `COMPILE_LIKE_GOALS`, so the fallback is only
233-
/// reached if that invariant is violated.
234-
fn compile_like_labels(goal: &str) -> (&'static str, &'static str) {
235-
for &(g, tool, tee) in COMPILE_LIKE_GOALS {
267+
/// Look up the `(tool_name, tee_label)` pair for a compile-like goal, prefixed
268+
/// with the active binary (`mvn` or `mvnd`). Callers are gated on `route_goal`
269+
/// / `COMPILE_LIKE_GOALS`, so the fallback is only reached if that invariant
270+
/// is violated.
271+
fn compile_like_labels(binary: MvnBinary, goal: &str) -> (String, String) {
272+
let bin = binary.display();
273+
for &(g, tee_slug) in COMPILE_LIKE_GOALS {
236274
if g == goal {
237-
return (tool, tee);
275+
return (format!("{bin} {g}"), format!("{bin}_{tee_slug}"));
238276
}
239277
}
240-
("mvn compile", "mvn_compile")
278+
(format!("{bin} compile"), format!("{bin}_compile"))
241279
}
242280

243281
/// Routing decision for a raw mvn subcommand seen on `run_other` — i.e. the
@@ -253,7 +291,7 @@ enum GoalRouting {
253291
}
254292

255293
fn route_goal(subcommand: &str) -> GoalRouting {
256-
if COMPILE_LIKE_GOALS.iter().any(|(g, _, _)| *g == subcommand) {
294+
if COMPILE_LIKE_GOALS.iter().any(|(g, _)| *g == subcommand) {
257295
return GoalRouting::Compile;
258296
}
259297
if subcommand == "checkstyle:check" || subcommand == "checkstyle" {
@@ -277,46 +315,48 @@ fn trailing_args(args: &[OsString]) -> Vec<String> {
277315
/// `checkstyle:check` go through `filter_mvn_checkstyle`; everything else
278316
/// streams directly via `status()` (safe for long-running goals like
279317
/// `spring-boot:run`, and metric-only for rare ones like `package`).
280-
pub fn run_other(args: &[OsString], verbose: u8) -> Result<i32> {
318+
pub fn run_other(binary: MvnBinary, args: &[OsString], verbose: u8) -> Result<i32> {
319+
let bin = binary.display();
320+
281321
if args.is_empty() {
282-
anyhow::bail!("mvn: no subcommand specified");
322+
anyhow::bail!("{bin}: no subcommand specified");
283323
}
284324

285325
let subcommand = args[0].to_string_lossy();
286326

287327
if verbose > 0 {
288-
eprintln!("Running: mvn {} ...", subcommand);
328+
eprintln!("Running: {bin} {} ...", subcommand);
289329
}
290330

291331
match route_goal(&subcommand) {
292332
GoalRouting::Compile => {
293-
return run_compile_like(&subcommand, &trailing_args(args), verbose);
333+
return run_compile_like(binary, &subcommand, &trailing_args(args), verbose);
294334
}
295335
GoalRouting::Checkstyle => {
296-
return run_checkstyle(&trailing_args(args), verbose);
336+
return run_checkstyle(binary, &trailing_args(args), verbose);
297337
}
298338
GoalRouting::Passthrough => {}
299339
}
300340

301341
// Everything else: passthrough with streaming (safe for spring-boot:run etc.)
302342
let timer = tracking::TimedExecution::start();
303343

304-
let mut cmd = mvn_command();
344+
let mut cmd = mvn_command(binary);
305345
for arg in args {
306346
cmd.arg(arg);
307347
}
308348

309349
let status = cmd
310350
.status()
311-
.with_context(|| format!("Failed to run mvn {}", subcommand))?;
351+
.with_context(|| format!("Failed to run {bin} {}", subcommand))?;
312352

313353
let args_str = tracking::args_display(args);
314354
timer.track_passthrough(
315-
&format!("mvn {}", args_str),
316-
&format!("rtk mvn {} (passthrough)", args_str),
355+
&format!("{bin} {}", args_str),
356+
&format!("rtk {bin} {} (passthrough)", args_str),
317357
);
318358

319-
Ok(exit_code_from_status(&status, "mvn"))
359+
Ok(exit_code_from_status(&status, bin))
320360
}
321361

322362
// ---------------------------------------------------------------------------
@@ -1837,7 +1877,7 @@ mod tests {
18371877

18381878
#[test]
18391879
fn test_run_other_empty_args_errors() {
1840-
let result = run_other(&[], 0);
1880+
let result = run_other(MvnBinary::Mvn, &[], 0);
18411881
assert!(result.is_err());
18421882
let err_msg = result.unwrap_err().to_string();
18431883
assert!(
@@ -1847,6 +1887,35 @@ mod tests {
18471887
);
18481888
}
18491889

1890+
#[test]
1891+
fn test_run_other_empty_args_errors_mvnd() {
1892+
let result = run_other(MvnBinary::Mvnd, &[], 0);
1893+
assert!(result.is_err());
1894+
let err_msg = result.unwrap_err().to_string();
1895+
assert!(
1896+
err_msg.contains("mvnd: no subcommand"),
1897+
"expected 'mvnd: no subcommand' error, got: {}",
1898+
err_msg
1899+
);
1900+
}
1901+
1902+
#[test]
1903+
fn test_compile_like_labels_mvnd() {
1904+
let (tool, tee) = compile_like_labels(MvnBinary::Mvnd, "compile");
1905+
assert_eq!(tool, "mvnd compile");
1906+
assert_eq!(tee, "mvnd_compile");
1907+
1908+
let (tool, tee) = compile_like_labels(MvnBinary::Mvnd, "test-compile");
1909+
assert_eq!(tool, "mvnd test-compile");
1910+
assert_eq!(tee, "mvnd_test_compile");
1911+
}
1912+
1913+
#[test]
1914+
fn test_mvn_binary_display() {
1915+
assert_eq!(MvnBinary::Mvn.display(), "mvn");
1916+
assert_eq!(MvnBinary::Mvnd.display(), "mvnd");
1917+
}
1918+
18501919
// --- checkstyle filter tests ---
18511920

18521921
#[test]

src/discover/rules.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,15 @@ pub const RULES: &[RtkRule] = &[
680680
subcmd_savings: &[],
681681
subcmd_status: &[],
682682
},
683+
RtkRule {
684+
pattern: r"^mvnd\s+(test|compile|package|clean|install|dependency:tree|checkstyle:check|checkstyle)\b",
685+
rtk_cmd: "rtk mvnd",
686+
rewrite_prefixes: &["mvnd"],
687+
category: "Build",
688+
savings_pct: 90.0,
689+
subcmd_savings: &[],
690+
subcmd_status: &[],
691+
},
683692
RtkRule {
684693
pattern: r"^ping\b",
685694
rtk_cmd: "rtk ping",

0 commit comments

Comments
 (0)