1010import io .jenkins .tools .pluginmodernizer .core .model .Recipe ;
1111import io .jenkins .tools .pluginmodernizer .core .utils .JdkFetcher ;
1212import jakarta .inject .Inject ;
13+ import java .io .BufferedReader ;
1314import java .io .IOException ;
15+ import java .io .InputStreamReader ;
16+ import java .nio .charset .StandardCharsets ;
1417import java .nio .file .Files ;
1518import java .nio .file .Path ;
1619import java .util .ArrayList ;
@@ -55,7 +58,7 @@ public class MavenInvoker {
5558 AtomicReference <String > version = new AtomicReference <>();
5659 try {
5760 InvocationRequest request = new DefaultInvocationRequest ();
58- request .setMavenHome (config . getMavenHome ().toFile ());
61+ request .setMavenHome (getEffectiveMavenHome ().toFile ());
5962 request .setBatchMode (true );
6063 request .addArg ("-q" );
6164 request .addArg ("--version" );
@@ -181,13 +184,16 @@ private void validatePom(Plugin plugin) {
181184 * @throws IllegalArgumentException if the Maven home directory is not set or invalid.
182185 */
183186 public void validateMaven () {
184- Path mavenHome = config . getMavenHome ();
185- if ( mavenHome == null ) {
186- throw new ModernizerException (
187- "Neither MAVEN_HOME nor M2_HOME environment variables are set. Or use --maven-home if running from CLI " );
188- }
187+ Path mavenHome = getEffectiveMavenHome ();
188+
189+ Path mvnUnix = mavenHome . resolve ( "bin/mvn" );
190+ Path mvnCmd = mavenHome . resolve ( "bin/mvn.cmd " );
191+ Path mvnBat = mavenHome . resolve ( "bin/mvn.bat" );
189192
190- if (!Files .isDirectory (mavenHome ) || !Files .isExecutable (mavenHome .resolve ("bin/mvn" ))) {
193+ if (!Files .isDirectory (mavenHome )
194+ || (!mvnUnix .toFile ().canExecute ()
195+ && !mvnCmd .toFile ().exists ()
196+ && !mvnBat .toFile ().exists ())) {
191197 throw new ModernizerException ("Invalid Maven home directory at '%s'." .formatted (mavenHome ));
192198 }
193199
@@ -200,6 +206,123 @@ public void validateMaven() {
200206 }
201207 }
202208
209+ @ SuppressWarnings ("OS_COMMAND_INJECTION" )
210+ @ Nullable
211+ private Path detectMavenHome () {
212+ String os = System .getProperty ("os.name" );
213+ if (os == null ) {
214+ os = "" ;
215+ }
216+
217+ ProcessBuilder processBuilder ;
218+ if (os .toLowerCase ().contains ("win" )) {
219+ processBuilder = new ProcessBuilder ("where" , "mvn" );
220+ } else {
221+ processBuilder = new ProcessBuilder ("which" , "mvn" );
222+ }
223+ processBuilder .redirectErrorStream (true );
224+
225+ String mvnPath = null ;
226+ StringBuilder output = new StringBuilder ();
227+ Process process = null ;
228+
229+ try {
230+ process = processBuilder .start ();
231+
232+ try (BufferedReader reader =
233+ new BufferedReader (new InputStreamReader (process .getInputStream (), StandardCharsets .UTF_8 ))) {
234+ String line ;
235+ while ((line = reader .readLine ()) != null ) {
236+ if (output .length () > 0 ) {
237+ output .append ('\n' );
238+ }
239+ output .append (line );
240+
241+ if (mvnPath == null && !line .isBlank ()) {
242+ mvnPath = line ;
243+ }
244+ }
245+ }
246+
247+ int exitCode = process .waitFor ();
248+ if (exitCode != 0 || mvnPath == null || mvnPath .isBlank ()) {
249+ LOG .debug ("Maven not found in PATH (exitCode=" + exitCode + ", output=" + sanitize (output .toString ())
250+ + ")" );
251+ return null ;
252+ }
253+
254+ Path mvn = Path .of (mvnPath ).toRealPath ();
255+ Path binDir = mvn .getParent ();
256+ if (binDir == null ) {
257+ LOG .debug ("Failed to detect Maven home from mvn path (no parent): " + sanitize (mvnPath ));
258+ return null ;
259+ }
260+
261+ Path mavenHome = binDir .getParent ();
262+ if (mavenHome == null ) {
263+ LOG .debug ("Failed to detect Maven home from mvn path (no grandparent): " + sanitize (mvnPath ));
264+ return null ;
265+ }
266+ return mavenHome ;
267+ } catch (InterruptedException e ) {
268+ Thread .currentThread ().interrupt ();
269+ LOG .debug ("Interrupted while detecting Maven from PATH" , e );
270+ return null ;
271+ } catch (Exception e ) {
272+ LOG .debug ("Failed to detect Maven from PATH" , e );
273+ return null ;
274+ } finally {
275+ if (process != null ) {
276+ process .destroy ();
277+ }
278+ }
279+ }
280+
281+ private boolean isValidMavenHome (Path mavenHome ) {
282+ if (mavenHome == null || !Files .isDirectory (mavenHome )) {
283+ return false ;
284+ }
285+
286+ Path mvnUnix = mavenHome .resolve ("bin/mvn" );
287+ Path mvnCmd = mavenHome .resolve ("bin/mvn.cmd" );
288+ Path mvnBat = mavenHome .resolve ("bin/mvn.bat" );
289+ return mvnUnix .toFile ().canExecute ()
290+ || mvnCmd .toFile ().exists ()
291+ || mvnBat .toFile ().exists ();
292+ }
293+
294+ private String sanitize (String input ) {
295+ return input == null ? null : input .replaceAll ("[\\ r\\ n]" , "" );
296+ }
297+
298+ private Path getEffectiveMavenHome () {
299+ Path configured = config .getConfiguredMavenHome ();
300+ if (configured != null ) {
301+ if (isValidMavenHome (configured )) {
302+ return configured ;
303+ }
304+ LOG .warn ("Configured Maven home is invalid: " + sanitize (configured .toString ())
305+ + ". Falling back to PATH detection." );
306+ }
307+
308+ Path cachedDetected = config .getDetectedMavenHome ();
309+ if (cachedDetected != null ) {
310+ if (isValidMavenHome (cachedDetected )) {
311+ return cachedDetected ;
312+ }
313+ LOG .debug ("Cached detected Maven home is invalid: " + sanitize (cachedDetected .toString ()));
314+ }
315+
316+ Path detected = detectMavenHome ();
317+ if (detected != null && isValidMavenHome (detected )) {
318+ LOG .info ("Detected Maven home from PATH: " + sanitize (detected .toString ()));
319+ config .setMavenHome (detected );
320+ return detected ;
321+ }
322+
323+ throw new ModernizerException ("Maven not found. Please set MAVEN_HOME or ensure 'mvn' is available in PATH." );
324+ }
325+
203326 /**
204327 * Validate the Maven version.
205328 * @throws IllegalArgumentException if the Maven version is too old or cannot be determined.
@@ -228,10 +351,10 @@ public void validateMavenVersion() {
228351 */
229352 private InvocationRequest createInvocationRequest (Plugin plugin , String ... args ) {
230353 InvocationRequest request = new DefaultInvocationRequest ();
231- request .setMavenHome (config . getMavenHome ().toFile ());
354+ request .setMavenHome (getEffectiveMavenHome ().toFile ());
232355 request .setPomFile (plugin .getLocalRepository ().resolve ("pom.xml" ).toFile ());
233356 request .addArgs (List .of (args ));
234- if (config .isDebug ()) {
357+ if (Config .isDebug ()) {
235358 request .addArg ("-X" );
236359 }
237360 return request ;
@@ -244,12 +367,12 @@ private InvocationRequest createInvocationRequest(Plugin plugin, String... args)
244367 */
245368 private void handleInvocationResult (Plugin plugin , InvocationResult result ) {
246369 if (result .getExitCode () != 0 ) {
247- LOG .error (plugin .getMarker (), "Build fail with code: {}" , result .getExitCode ());
370+ LOG .error (plugin .getMarker (), "Build failed with code: {}" , result .getExitCode ());
248371 if (result .getExecutionException () != null ) {
249372 plugin .addError ("Maven generic exception occurred" , result .getExecutionException ());
250373 } else {
251374 String errorMessage ;
252- if (config .isDebug ()) {
375+ if (Config .isDebug ()) {
253376 errorMessage = "Build failed with code: " + result .getExitCode ();
254377 } else {
255378 errorMessage = "Build failed" ;
0 commit comments