diff --git a/src/main/java/org/apache/maven/shared/verifier/ForkedLauncher.java b/src/main/java/org/apache/maven/shared/verifier/ForkedLauncher.java index b5afd80..a5226bc 100644 --- a/src/main/java/org/apache/maven/shared/verifier/ForkedLauncher.java +++ b/src/main/java/org/apache/maven/shared/verifier/ForkedLauncher.java @@ -67,29 +67,37 @@ class ForkedLauncher implements MavenLauncher { } ForkedLauncher(String mavenHome, Map envVars, boolean debugJvm, boolean wrapper) { + this(mavenHome, envVars, debugJvm, wrapper, null); + } + + ForkedLauncher( + String mavenHome, Map envVars, boolean debugJvm, boolean wrapper, String mvnExecutable) { this.mavenHome = mavenHome; this.envVars = envVars; + if (mvnExecutable != null && !mvnExecutable.isEmpty()) { + executable = mvnExecutable; + } else { + if (wrapper) { + final StringBuilder script = new StringBuilder(); - if (wrapper) { - final StringBuilder script = new StringBuilder(); - - if (!isWindows()) { - script.append("./"); - } - - script.append("mvnw"); + if (!isWindows()) { + script.append("./"); + } - if (debugJvm) { - script.append("Debug"); - } - executable = script.toString(); - } else { - String script = "mvn" + (debugJvm ? "Debug" : ""); + script.append("mvnw"); - if (mavenHome != null) { - executable = new File(mavenHome, "bin/" + script).getPath(); + if (debugJvm) { + script.append("Debug"); + } + executable = script.toString(); } else { - executable = script; + String script = "mvn" + (debugJvm ? "Debug" : ""); + + if (mavenHome != null) { + executable = new File(mavenHome, "bin/" + script).getPath(); + } else { + executable = script; + } } } } @@ -119,10 +127,8 @@ public int run( cmd.setWorkingDirectory(workingDirectory); - for (Object o : systemProperties.keySet()) { - String key = (String) o; - String value = systemProperties.getProperty(key); - cmd.createArg().setValue("-D" + key + "=" + value); + for (Map.Entry entry : systemProperties.entrySet()) { + cmd.createArg().setValue("-D" + entry.getKey() + "=" + entry.getValue()); } for (String cliArg : cliArgs) { diff --git a/src/main/java/org/apache/maven/shared/verifier/Verifier.java b/src/main/java/org/apache/maven/shared/verifier/Verifier.java index a7ac3a0..e6b9946 100644 --- a/src/main/java/org/apache/maven/shared/verifier/Verifier.java +++ b/src/main/java/org/apache/maven/shared/verifier/Verifier.java @@ -172,6 +172,8 @@ public class Verifier { private String mavenHome; + private String mvnExecutable; + // will launch mvn with -X private boolean mavenDebug = false; @@ -1220,7 +1222,7 @@ protected MavenLauncher getMavenLauncher(Map envVars) throws Lau return embeddedLauncher; } else { - return new ForkedLauncher(mavenHome, envVars, debugJvm, useWrapper); + return new ForkedLauncher(mavenHome, envVars, debugJvm, useWrapper, mvnExecutable); } } @@ -1272,10 +1274,7 @@ private static String getLogContents(File logFile) { private void findLocalRepo(String settingsFile) throws VerificationException { if (localRepo == null) { - localRepo = System.getProperty("maven.repo.local"); - } - - if (localRepo == null) { + // Settings file is the primary configuration source — consult it first. try { localRepo = retrieveLocalRepo(settingsFile); } catch (SettingsBuildingException e) { @@ -1283,6 +1282,16 @@ private void findLocalRepo(String settingsFile) throws VerificationException { } } + if (localRepo == null) { + // System property is the last resort before the hardcoded default. + // It is intentionally checked AFTER the settings file because + // Embedded3xLauncher.run() calls System.setProperties(null) during embedded + // Maven executions, creating a race window where this property may transiently + // hold a value injected by a concurrent Maven invocation running in the same JVM. + // Prefer Builder.localRepository() for an entirely race-free alternative. + localRepo = System.getProperty("maven.repo.local"); + } + if (localRepo == null) { localRepo = USER_HOME + "/.m2/repository"; } @@ -1619,4 +1628,101 @@ private void setForkMode() { forkMode = "auto"; } } + + /** + * Builder for creating {@link Verifier} instances with explicit configuration. + *

+ * When {@link #localRepository(String)} is set, the local repository is pre-populated + * before {@code findLocalRepo()} runs, so the system property + * {@code maven.repo.local} is never consulted. This eliminates the race condition that + * occurs when multiple {@code Verifier} instances are constructed concurrently in the + * same JVM while {@link Embedded3xLauncher#run} is manipulating the JVM-global system + * properties table ({@code System.setProperties(null)}). + */ + public static class Builder { + private final String basedir; + private String settingsFile; + private String localRepository; + private String forkMode; + private Boolean forkJvm; + private String mvnExecutable; + private String[] defaultCliArguments; + + public Builder(String basedir) { + this.basedir = basedir; + } + + public Builder settingsFile(String settingsFile) { + this.settingsFile = settingsFile; + return this; + } + + /** + * Sets the local repository path explicitly. + * When provided, the system property {@code maven.repo.local} and the settings file + * are both bypassed for local repository resolution, making construction race-free. + */ + public Builder localRepository(String localRepository) { + this.localRepository = localRepository; + return this; + } + + /** + * Sets the forkMode explicitly. + * When provided, the system property verifier.forkMode is bypassed. + */ + public Builder forkMode(String forkMode) { + this.forkMode = forkMode; + return this; + } + + /** + * Sets the forkJvm explicitly. + */ + public Builder forkJvm(Boolean forkJvm) { + this.forkJvm = forkJvm; + return this; + } + + /** + * Sets the maven full path executable explicitly. + * When provided, the maven executable is used directly without any lookup or resolution. + */ + public Builder mvnExecutable(String mvnExecutable) { + this.mvnExecutable = mvnExecutable; + return this; + } + + /** + * Sets the default CLI arguments to be used for every execution + */ + public Builder defaultCliArguments(String[] defaultCliArguments) { + this.defaultCliArguments = defaultCliArguments; + return this; + } + + public Verifier build() throws VerificationException { + return new Verifier(this); + } + } + + private Verifier(Builder builder) throws VerificationException { + this.basedir = builder.basedir; + this.forkMode = builder.forkMode == null ? System.getProperty("verifier.forkMode") : builder.forkMode; + setForkMode(); + this.forkJvm = builder.forkJvm; + // Pre-set localRepo before findLocalRepo() so that the settings-file and + // system-property lookups in findLocalRepo() are both skipped when an + // explicit value has been provided via the builder. + this.localRepo = builder.localRepository; + if (builder.settingsFile != null) { + this.settingsFile = builder.settingsFile; + } + findLocalRepo(this.settingsFile); + this.mavenHome = System.getProperty("maven.home"); + this.useWrapper = Files.exists(Paths.get(getBasedir(), "mvnw")); + this.defaultCliArguments = + builder.defaultCliArguments == null ? DEFAULT_CLI_ARGUMENTS.clone() : builder.defaultCliArguments; + this.mvnExecutable = builder.mvnExecutable; + } }