@@ -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
255293fn 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]
0 commit comments