diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java index 50e5777de2fa..4c2f2d65ea8b 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java @@ -46,8 +46,6 @@ import org.apache.maven.api.settings.Settings; import org.apache.maven.cling.invoker.mvnup.UpgradeContext; import org.apache.maven.impl.standalone.ApiRunner; -import org.codehaus.plexus.components.secdispatcher.Dispatcher; -import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher; import org.eclipse.aether.spi.connector.transport.TransporterFactory; import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor; import org.eclipse.aether.spi.io.PathProcessor; @@ -257,18 +255,9 @@ protected static String remoteResolutionUnsupportedReason(UpgradeContext context } private Session createMaven4Session(UpgradeContext context) { - // Reuse the operator's real user home (settings.xml) and local repository so remote - // resolution follows the configured repository posture instead of the standalone - // test defaults. When settings were never loaded (unit tests, embedding), keep the - // isolated test user home. - boolean settingsLoaded = context.effectiveSettings != null; - Session session = ApiRunner.createSession( - injector -> { - injector.bindInstance(Dispatcher.class, new LegacyDispatcher()); - injector.bindImplicit(TransporterFactoryConfig.class); - }, - context.localRepositoryPath, - !settingsLoaded); + Session session = ApiRunner.createSession(injector -> { + injector.bindImplicit(TransporterFactoryConfig.class); + }); if (remoteResolutionUnsupportedReason(context) != null) { // No remote repositories at all, rather than resolving from hardcoded public diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java index 5620c4c4268e..97a8853cd7b8 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java @@ -18,7 +18,6 @@ */ package org.apache.maven.impl.resolver; -import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.util.ArrayList; diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java index 25740a0ff506..24f6b3488bc1 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java @@ -281,8 +281,6 @@ private Versioning readVersions( return (versioning != null) ? versioning : Versioning.newInstance(); } - - private void invalidMetadata( RepositorySystemSession session, RequestTrace trace, diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java index b6a0a711e817..f18d9f09a515 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java @@ -80,7 +80,7 @@ public MavenSessionBuilderSupplier(RepositorySystem repositorySystem, boolean ma : Maven4ScopeManagerConfiguration.INSTANCE); } - protected DependencyTraverser getDependencyTraverser() { + public DependencyTraverser getDependencyTraverser() { return new FatArtifactTraverser(); } @@ -99,7 +99,7 @@ public DependencyManager getDependencyManager(boolean transitive) { return new ClassicDependencyManager(getScopeManager()); } - protected DependencySelector getDependencySelector() { + public DependencySelector getDependencySelector() { return new AndDependencySelector( ScopeDependencySelector.legacy( null, Arrays.asList(DependencyScope.TEST.id(), DependencyScope.PROVIDED.id())), @@ -107,7 +107,7 @@ protected DependencySelector getDependencySelector() { new ExclusionDependencySelector()); } - protected DependencyGraphTransformer getDependencyGraphTransformer() { + public DependencyGraphTransformer getDependencyGraphTransformer() { return new ChainedDependencyGraphTransformer( new TypeCollector(), new ConflictResolver( @@ -126,13 +126,13 @@ protected DependencyGraphTransformer getDependencyGraphTransformer() { *
* Important: this "static" list of types should be in-sync with core provided types. */ - protected ArtifactTypeRegistry getArtifactTypeRegistry() { + public ArtifactTypeRegistry getArtifactTypeRegistry() { DefaultArtifactTypeRegistry stereotypes = new DefaultArtifactTypeRegistry(); new DefaultTypeProvider().types().forEach(t -> stereotypes.add(t.toArtifactType())); return stereotypes; } - protected ArtifactDescriptorPolicy getArtifactDescriptorPolicy() { + public ArtifactDescriptorPolicy getArtifactDescriptorPolicy() { return new SimpleArtifactDescriptorPolicy(true, true); } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java index 3421eadf1bf6..b64dafa86944 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java @@ -18,6 +18,9 @@ */ package org.apache.maven.impl.standalone; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; @@ -29,11 +32,14 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Properties; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.maven.api.Artifact; +import org.apache.maven.api.Constants; import org.apache.maven.api.Lifecycle; import org.apache.maven.api.MonotonicClock; import org.apache.maven.api.Packaging; @@ -45,6 +51,7 @@ import org.apache.maven.api.Version; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Provides; import org.apache.maven.api.di.SessionScoped; import org.apache.maven.api.model.PluginContainer; @@ -57,32 +64,69 @@ import org.apache.maven.api.services.RepositoryFactory; import org.apache.maven.api.services.SettingsBuilder; import org.apache.maven.api.services.TypeRegistry; +import org.apache.maven.api.services.VersionParser; +import org.apache.maven.api.settings.Mirror; +import org.apache.maven.api.settings.Proxy; +import org.apache.maven.api.settings.Server; import org.apache.maven.api.settings.Settings; import org.apache.maven.api.spi.TypeProvider; import org.apache.maven.api.toolchain.ToolchainModel; import org.apache.maven.di.Injector; import org.apache.maven.di.Key; import org.apache.maven.di.impl.DIException; +import org.apache.maven.di.impl.InjectorImpl; import org.apache.maven.impl.AbstractSession; import org.apache.maven.impl.InternalSession; import org.apache.maven.impl.di.SessionScope; +import org.apache.maven.impl.model.DefaultInterpolator; +import org.apache.maven.impl.resolver.MavenSessionBuilderSupplier; import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration; +import org.codehaus.plexus.components.secdispatcher.Cipher; +import org.codehaus.plexus.components.secdispatcher.Dispatcher; +import org.codehaus.plexus.components.secdispatcher.MasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.cipher.AESGCMNoPadding; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterDispatcher; +import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.PinEntryMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterSource; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.internal.impl.scope.ScopeManagerImpl; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.LocalRepositoryManager; +import org.eclipse.aether.util.repository.AuthenticationBuilder; +import org.eclipse.aether.util.repository.DefaultAuthenticationSelector; +import org.eclipse.aether.util.repository.DefaultMirrorSelector; +import org.eclipse.aether.util.repository.DefaultProxySelector; /** * Provides functionality for running Maven API in a standalone mode. *
* This class serves as the main entry point for executing Maven operations outside * of the standard Maven build environment. It provides methods for creating and - * managing Maven sessions in a simplified context, primarily for testing and - * specialized execution scenarios. + * managing Maven sessions in a simplified context, suitable for tools, IDE integrations, + * and specialized execution scenarios. *
* + *The standalone session reads and applies the user's {@code settings.xml} including:
+ *It also loads {@code maven-system.properties} from the Maven configuration directory + * ({@code ${maven.home}/conf} or as configured via {@code maven.installation.conf}/{@code maven.conf}) + * and from the user's Maven home ({@code ~/.m2/}), merging both into the session's system properties. + * User properties are loaded from {@code ~/.m2/maven-user.properties} and exposed via + * {@link Session#getUserProperties()}.
+ * *Example usage:
*
* Session session = ApiRunner.createSession();
@@ -99,6 +143,27 @@
*/
public class ApiRunner {
+ /**
+ * Controls how the standalone session handles settings encryption/decryption.
+ *
+ * Maven settings ({@code settings.xml}) may contain encrypted server passwords.
+ * Decryption requires the {@code plexus-sec-dispatcher} library on the classpath.
+ * This enum lets callers control the behavior when dispatchers are (or are not) available.
+ */
+ public enum SecurityMode {
+ /** Do not attempt to configure security dispatchers. Encrypted passwords are passed through as-is. */
+ NONE,
+ /** Try to configure security dispatchers; silently skip if the required classes are not on the classpath. */
+ IF_AVAILABLE,
+ /**
+ * Try to configure security dispatchers; warn to {@code System.err} if the required classes
+ * are not on the classpath. This is the default.
+ */
+ IF_AVAILABLE_WARN,
+ /** Security dispatchers are required; throw {@link MavenException} if they cannot be configured. */
+ REQUIRED
+ }
+
/**
* Creates a new Maven session with default configuration.
*
@@ -126,29 +191,27 @@ public static Session createSession(Consumer injectorConsumer) {
* @return a new {@link Session} instance
*/
public static Session createSession(Consumer injectorConsumer, Path localRepo) {
- return createSession(injectorConsumer, localRepo, true);
+ return createSession(injectorConsumer, localRepo, SecurityMode.IF_AVAILABLE_WARN);
}
/**
- * Creates a new Maven session with custom injector configuration, local repository path
- * and control over {@code user.home} isolation.
+ * Creates a new Maven session with custom injector configuration, local repository path,
+ * and security mode.
*
* @param injectorConsumer consumer function to customize the injector
* @param localRepo path to the local repository
- * @param isolateUserHome when {@code true}, the session remaps {@code user.home} to the
- * {@code target} directory so the invoking user's {@code settings.xml} and local
- * repository cannot interfere with tests; callers that want the operator's real
- * configuration honored (such as the mvnup tool) must pass {@code false}
+ * @param securityMode controls how encrypted passwords in settings are handled
* @return a new {@link Session} instance
*/
- public static Session createSession(Consumer injectorConsumer, Path localRepo, boolean isolateUserHome) {
+ public static Session createSession(
+ Consumer injectorConsumer, Path localRepo, SecurityMode securityMode) {
Injector injector = Injector.create();
injector.bindInstance(Injector.class, injector);
injector.bindImplicit(ApiRunner.class);
injector.bindImplicit(RepositorySystemSupplier.class);
injector.bindInstance(LocalRepoProvider.class, () -> localRepo);
- injector.bindInstance(UserHomeIsolation.class, () -> isolateUserHome);
injector.discover(ApiRunner.class.getClassLoader());
+ configureSecurityDispatchers(injector, securityMode != null ? securityMode : SecurityMode.IF_AVAILABLE_WARN);
if (injectorConsumer != null) {
injectorConsumer.accept(injector);
}
@@ -161,14 +224,45 @@ public static Session createSession(Consumer injectorConsumer, Path lo
}
/**
- * Controls whether the standalone session isolates {@code user.home} from the invoking
- * user's environment.
+ * Attempts to bind security dispatcher classes for settings password decryption.
+ * The dispatchers (legacy and master) are loaded from the {@code plexus-sec-dispatcher}
+ * library. If the library is not on the classpath, the behavior depends on the
+ * {@link SecurityMode}.
+ *
+ * If dispatchers are already bound (e.g., via {@code discover()} in tests or by a
+ * custom {@code injectorConsumer}), this method is a no-op to avoid duplicate bindings.
*/
- interface UserHomeIsolation {
- /**
- * @return {@code true} to remap {@code user.home} away from the real user home
- */
- boolean isolated();
+ private static void configureSecurityDispatchers(Injector injector, SecurityMode mode) {
+ if (mode == SecurityMode.NONE) {
+ return;
+ }
+ try {
+ Class.forName("org.codehaus.plexus.components.secdispatcher.Dispatcher");
+ // Skip if dispatchers are already bound (e.g., from discover() scanning test classes)
+ if (!hasExistingDispatchers(injector)) {
+ injector.bindImplicit(SecDispatcherBindings.class);
+ }
+ } catch (ClassNotFoundException | NoClassDefFoundError e) {
+ switch (mode) {
+ case REQUIRED:
+ throw new MavenException(
+ "Security dispatchers required but plexus-sec-dispatcher is not on the classpath", e);
+ case IF_AVAILABLE_WARN:
+ System.err.println("WARNING: plexus-sec-dispatcher not available on classpath; "
+ + "encrypted passwords in settings.xml will not be decrypted");
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private static boolean hasExistingDispatchers(Injector injector) {
+ if (injector instanceof InjectorImpl impl) {
+ Set> bindings = impl.getAllBindings(Dispatcher.class);
+ return bindings != null && !bindings.isEmpty();
+ }
+ return false;
}
/**
@@ -190,6 +284,9 @@ static class DefaultSession extends AbstractSession {
private final Map systemProperties;
private final Instant startTime = MonotonicClock.now();
+ private Settings settings;
+ private Version mavenVersion;
+ private Map userProperties = Map.of();
DefaultSession(RepositorySystemSession session, RepositorySystem repositorySystem, Lookup lookup) {
this(session, repositorySystem, Collections.emptyList(), null, lookup);
@@ -209,12 +306,29 @@ protected DefaultSession(
@Override
protected Session newSession(RepositorySystemSession session, List repositories) {
- return new DefaultSession(session, repositorySystem, repositories, null, lookup);
+ DefaultSession newSession = new DefaultSession(session, repositorySystem, repositories, null, lookup);
+ newSession.settings = this.settings;
+ newSession.mavenVersion = this.mavenVersion;
+ newSession.userProperties = this.userProperties;
+ return newSession;
+ }
+
+ void setSettings(Settings settings) {
+ this.settings = settings;
+ }
+
+ void setMavenVersion(Version mavenVersion) {
+ this.mavenVersion = mavenVersion;
+ }
+
+ void setUserProperties(Map userProperties) {
+ this.userProperties = userProperties != null ? userProperties : Map.of();
}
@Override
+ @Nonnull
public Settings getSettings() {
- return Settings.newInstance();
+ return settings != null ? settings : Settings.newInstance();
}
@Override
@@ -225,7 +339,7 @@ public Collection getToolchains() {
@Override
public Map getUserProperties() {
- return Map.of();
+ return userProperties;
}
@Override
@@ -245,7 +359,7 @@ public Map getEffectiveProperties(Project project) {
@Override
public Version getMavenVersion() {
- return null;
+ return mavenVersion;
}
@Override
@@ -393,33 +507,50 @@ public List computePhases(Lifecycle lifecycle) {
@Provides
@SuppressWarnings("unused")
- static Session newSession(
- RepositorySystem system,
- Lookup lookup,
- @Nullable LocalRepoProvider localRepoProvider,
- @Nullable UserHomeIsolation userHomeIsolation) {
+ static Session newSession(RepositorySystem system, Lookup lookup, @Nullable LocalRepoProvider localRepoProvider) {
Map properties = new HashMap<>();
// Env variables prefixed with "env."
System.getenv().forEach((k, v) -> properties.put("env." + k, v));
// Java System properties
System.getProperties().forEach((k, v) -> properties.put(k.toString(), v.toString()));
- // Test isolation shim: do not let the invoking user's settings interfere with unit
- // tests. Callers that want the operator's real settings.xml and local repository
- // honored must create the session with isolateUserHome=false (see createSession).
- if (userHomeIsolation == null || userHomeIsolation.isolated()) {
- properties.put("user.home", "target");
- }
-
Path userHome = Paths.get(properties.get("user.home"));
Path mavenUserHome = userHome.resolve(".m2");
Path mavenSystemHome = properties.containsKey("maven.home")
? Paths.get(properties.get("maven.home"))
: properties.containsKey("env.MAVEN_HOME") ? Paths.get(properties.get("env.MAVEN_HOME")) : null;
+ // Load maven-system.properties: installation-level first (sets paths like maven.user.conf),
+ // then user-level (~/.m2/) for custom overrides. The installation-level file is a bootstrapper
+ // that in the CLI includes user and project-level files via ${includes}; since we don't support
+ // that directive, we load the user-level file explicitly.
+ Path mavenConf = resolveMavenConf(properties);
+ if (mavenConf != null) {
+ Map systemFileProps =
+ loadMavenProperties(mavenConf.resolve("maven-system.properties"), properties);
+ properties.putAll(systemFileProps);
+ }
+ properties.putAll(loadMavenProperties(mavenUserHome.resolve("maven-system.properties"), properties));
+
+ // Load maven-user.properties from the user-level location (~/.m2/) only.
+ // The installation-level maven-user.properties (in ${maven.conf}/) contains Maven-internal
+ // configuration (cache config, conflict resolver) that is not appropriate for standalone use;
+ // it is a CLI bootstrapper that includes user and project-level files via ${includes}.
+ Map userProperties = new HashMap<>();
+ userProperties.putAll(loadMavenProperties(mavenUserHome.resolve("maven-user.properties"), properties));
+
+ // Configure the resolver session with dependency resolution machinery
+ MavenSessionBuilderSupplier sessionBuilderSupplier = new MavenSessionBuilderSupplier(system, false);
DefaultRepositorySystemSession rsession = new DefaultRepositorySystemSession(h -> false);
rsession.setScopeManager(new ScopeManagerImpl(Maven4ScopeManagerConfiguration.INSTANCE));
+ rsession.setDependencyTraverser(sessionBuilderSupplier.getDependencyTraverser());
+ rsession.setDependencyManager(sessionBuilderSupplier.getDependencyManager(true));
+ rsession.setDependencySelector(sessionBuilderSupplier.getDependencySelector());
+ rsession.setDependencyGraphTransformer(sessionBuilderSupplier.getDependencyGraphTransformer());
+ rsession.setArtifactTypeRegistry(sessionBuilderSupplier.getArtifactTypeRegistry());
+ rsession.setArtifactDescriptorPolicy(sessionBuilderSupplier.getArtifactDescriptorPolicy());
rsession.setSystemProperties(properties);
+ rsession.setUserProperties(userProperties);
rsession.setConfigProperties(properties);
DefaultSession session = new DefaultSession(
@@ -437,6 +568,13 @@ static Session newSession(
mavenUserHome.resolve("settings.xml"))
.getEffectiveSettings();
+ // Store the effective settings and user properties on the session
+ session.setSettings(settings);
+ session.setUserProperties(userProperties);
+
+ // Set the Maven version
+ session.setMavenVersion(detectMavenVersion(lookup));
+
// local repository
String localRepository = settings.getLocalRepository() != null
&& !settings.getLocalRepository().isEmpty()
@@ -446,15 +584,56 @@ static Session newSession(
: mavenUserHome.resolve("repository").toString();
LocalRepositoryManager llm = system.newLocalRepositoryManager(rsession, new LocalRepository(localRepository));
rsession.setLocalRepositoryManager(llm);
- // active proxies
- // TODO
- // active profiles
-
- Profile profile = session.getService(SettingsBuilder.class)
- .convert(org.apache.maven.api.settings.Profile.newBuilder()
- .repositories(settings.getRepositories())
- .pluginRepositories(settings.getPluginRepositories())
- .build());
+
+ // Apply offline mode from settings
+ if (settings.isOffline()) {
+ rsession.setOffline(true);
+ }
+
+ // Apply proxy configuration from settings
+ DefaultProxySelector proxySelector = new DefaultProxySelector();
+ for (Proxy proxy : settings.getProxies()) {
+ if (proxy.isActive()) {
+ AuthenticationBuilder authBuilder = new AuthenticationBuilder();
+ authBuilder.addUsername(proxy.getUsername()).addPassword(proxy.getPassword());
+ proxySelector.add(
+ new org.eclipse.aether.repository.Proxy(
+ proxy.getProtocol(), proxy.getHost(), proxy.getPort(), authBuilder.build()),
+ proxy.getNonProxyHosts());
+ }
+ }
+ rsession.setProxySelector(proxySelector);
+
+ // Apply mirror configuration from settings
+ DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
+ for (Mirror mirror : settings.getMirrors()) {
+ mirrorSelector.add(
+ mirror.getId(),
+ mirror.getUrl(),
+ mirror.getLayout(),
+ false,
+ mirror.isBlocked(),
+ mirror.getMirrorOf(),
+ mirror.getMirrorOfLayouts());
+ }
+ rsession.setMirrorSelector(mirrorSelector);
+
+ // Apply server authentication from settings
+ DefaultAuthenticationSelector authSelector = new DefaultAuthenticationSelector();
+ for (Server server : settings.getServers()) {
+ AuthenticationBuilder authBuilder = new AuthenticationBuilder();
+ authBuilder.addUsername(server.getUsername()).addPassword(server.getPassword());
+ authBuilder.addPrivateKey(server.getPrivateKey(), server.getPassphrase());
+ authSelector.add(server.getId(), authBuilder.build());
+ }
+ rsession.setAuthenticationSelector(authSelector);
+
+ // Build repositories from active profiles in settings
+ SettingsBuilder settingsBuilder = session.getService(SettingsBuilder.class);
+ Profile profile = settingsBuilder.convert(org.apache.maven.api.settings.Profile.newBuilder()
+ .repositories(settings.getRepositories())
+ .pluginRepositories(settings.getPluginRepositories())
+ .build());
RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class);
List repositories = profile.getRepositories().stream()
.map(repositoryFactory::createRemote)
@@ -462,16 +641,164 @@ static Session newSession(
InternalSession s = (InternalSession) session.withRemoteRepositories(repositories);
InternalSession.associate(rsession, s);
return s;
+ }
- // List repositories = repositoryFactory.createRemote();
+ /**
+ * Resolves the Maven configuration directory following the same lookup order as the Maven CLI:
+ * {@code maven.installation.conf} → {@code maven.conf} → {@code ${maven.home}/conf} → {@code ${MAVEN_HOME}/conf}.
+ *
+ * @param properties the system properties (env + Java system properties)
+ * @return the maven conf directory path, or {@code null} if it cannot be determined
+ */
+ private static Path resolveMavenConf(Map properties) {
+ String installConf = properties.get(Constants.MAVEN_INSTALLATION_CONF);
+ if (installConf != null) {
+ return Paths.get(installConf);
+ }
+ String mavenConf = properties.get("maven.conf");
+ if (mavenConf != null) {
+ return Paths.get(mavenConf);
+ }
+ String mavenHome = properties.get(Constants.MAVEN_HOME);
+ if (mavenHome != null) {
+ return Paths.get(mavenHome).resolve("conf");
+ }
+ String envMavenHome = properties.get("env.MAVEN_HOME");
+ if (envMavenHome != null) {
+ return Paths.get(envMavenHome).resolve("conf");
+ }
+ return null;
+ }
+
+ /**
+ * Loads properties from a file and interpolates {@code ${...}} references against the given
+ * fallback properties. If the file does not exist or cannot be read, an empty map is returned.
+ *
+ * @param path the properties file to load
+ * @param fallback fallback values for interpolation (typically system properties)
+ * @return the loaded and interpolated properties
+ */
+ private static Map loadMavenProperties(Path path, Map fallback) {
+ if (path == null || !Files.exists(path)) {
+ return new HashMap<>();
+ }
+ Properties fileProps = new Properties();
+ try (InputStream is = Files.newInputStream(path)) {
+ fileProps.load(is);
+ } catch (IOException e) {
+ // Silently ignore: properties files are optional configuration and the standalone
+ // API has no logger. Failing to read a file (permissions, concurrent deletion)
+ // should not prevent session creation — callers get the same behavior as if the
+ // file did not exist.
+ return new HashMap<>();
+ }
+ Map result = new HashMap<>();
+ fileProps.forEach((k, v) -> result.put(k.toString(), v.toString()));
+ // Interpolate ${...} references against the loaded properties + fallback
+ for (Map.Entry entry : result.entrySet()) {
+ String value = entry.getValue();
+ if (value != null && value.contains("${")) {
+ entry.setValue(
+ DefaultInterpolator.substVars(value, entry.getKey(), null, result, fallback::get, null, false));
+ }
+ }
+ return result;
+ }
- // session.getService(SettingsBuilder.class).convert()
+ /**
+ * Detects the Maven version by reading the pom.properties resource from the classpath.
+ * Falls back to reading from maven-impl's own pom.properties if maven-core is not available.
+ * If no version can be determined, returns {@code 0.0.0} as a sentinel value so that
+ * {@link Session#getMavenVersion()} is never null.
+ *
+ * @param lookup the lookup service
+ * @return the detected Maven version, never {@code null}
+ */
+ @Nonnull
+ private static Version detectMavenVersion(Lookup lookup) {
+ String version = loadVersionFromProperties("META-INF/maven/org.apache.maven/maven-core/pom.properties");
+ if (version == null) {
+ version = loadVersionFromProperties("META-INF/maven/org.apache.maven/maven-impl/pom.properties");
+ }
+ if (version == null) {
+ version = "0.0.0";
+ }
+ try {
+ return lookup.lookup(VersionParser.class).parseVersion(version);
+ } catch (Exception e) {
+ // Should not happen with "0.0.0", but be safe
+ return lookup.lookup(VersionParser.class).parseVersion("0.0.0");
+ }
+ }
- // settings.getDelegate().getRepositories().stream()
- // .map(r -> SettingsUtilsV4.)
- // defaultSession.getService(RepositoryFactory.class).createRemote()
- // return defaultSession;
+ private static String loadVersionFromProperties(String resource) {
+ try (InputStream is = ApiRunner.class.getResourceAsStream("/" + resource)) {
+ if (is != null) {
+ Properties props = new Properties();
+ props.load(is);
+ String version = props.getProperty("version", "").trim();
+ if (!version.isEmpty() && !version.startsWith("${")) {
+ return version;
+ }
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ return null;
}
record DumbPackaging(String id, Type type, Map plugins) implements Packaging {}
+
+ /**
+ * Provides the security dispatcher bindings needed for decrypting encrypted passwords
+ * in settings.xml. This class is only loaded when {@code plexus-sec-dispatcher} is confirmed
+ * to be on the classpath (see {@link #configureSecurityDispatchers}).
+ *
+ * Supports both legacy ({@code {...}}) and Maven 4 master-key-based encrypted passwords.
+ */
+ @SuppressWarnings("unused")
+ static class SecDispatcherBindings {
+
+ @Provides
+ @Named(LegacyDispatcher.NAME)
+ static Dispatcher legacyDispatcher() {
+ return new LegacyDispatcher();
+ }
+
+ @Provides
+ @Named(MasterDispatcher.NAME)
+ static Dispatcher masterDispatcher(Map ciphers, Map sources) {
+ return new MasterDispatcher(ciphers, sources);
+ }
+
+ @Provides
+ @Named(AESGCMNoPadding.CIPHER_ALG)
+ static Cipher aesCipher() {
+ return new AESGCMNoPadding();
+ }
+
+ @Provides
+ @Named(EnvMasterSource.NAME)
+ static MasterSource envSource() {
+ return new EnvMasterSource();
+ }
+
+ @Provides
+ @Named(GpgAgentMasterSource.NAME)
+ static MasterSource gpgAgentSource() {
+ return new GpgAgentMasterSource();
+ }
+
+ @Provides
+ @Named(PinEntryMasterSource.NAME)
+ static MasterSource pinEntrySource() {
+ return new PinEntryMasterSource();
+ }
+
+ @Provides
+ @Named(SystemPropertyMasterSource.NAME)
+ static MasterSource systemPropertySource() {
+ return new SystemPropertyMasterSource();
+ }
+ }
}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java
index 28aa6386ecf6..c4cc9282e45b 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java
@@ -61,7 +61,7 @@ private static Model newModel(String groupId, String artifactId, String version)
}
@Test
- void noRelocationReturnsNull() {
+ void noRelocationReturnsNull() throws Exception {
Model model = Model.newBuilder().build();
Artifact result = source.relocatedTarget(null, newResult(), model);
assertNull(result);
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/ApiRunnerSettingsTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/ApiRunnerSettingsTest.java
new file mode 100644
index 000000000000..4e4bf9f9aac8
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/ApiRunnerSettingsTest.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.impl.standalone;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.maven.api.Session;
+import org.apache.maven.api.settings.Server;
+import org.apache.maven.api.settings.Settings;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link ApiRunner} settings handling and {@link ApiRunner.SecurityMode}.
+ */
+class ApiRunnerSettingsTest {
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void testSecurityModeNone() {
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+ assertNotNull(session);
+ assertNotNull(session.getSettings());
+ }
+
+ @Test
+ void testSecurityModeIfAvailable() {
+ // plexus-sec-dispatcher is on the test classpath, so dispatchers should be bound
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.IF_AVAILABLE);
+ assertNotNull(session);
+ assertNotNull(session.getSettings());
+ }
+
+ @Test
+ void testSecurityModeIfAvailableWarn() {
+ // Default mode — same as IF_AVAILABLE when sec-dispatcher is on classpath
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.IF_AVAILABLE_WARN);
+ assertNotNull(session);
+ assertNotNull(session.getSettings());
+ }
+
+ @Test
+ void testSecurityModeRequired() {
+ // plexus-sec-dispatcher is on the test classpath, so this should succeed
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.REQUIRED);
+ assertNotNull(session);
+ assertNotNull(session.getSettings());
+ }
+
+ @Test
+ void testDefaultSecurityMode() {
+ // Default createSession() uses IF_AVAILABLE_WARN
+ Session session = ApiRunner.createSession();
+ assertNotNull(session);
+ assertNotNull(session.getSettings());
+ }
+
+ @Test
+ void testMavenVersionIsNeverNull() {
+ Session session = ApiRunner.createSession();
+ assertNotNull(session.getMavenVersion(), "getMavenVersion() should never return null");
+ }
+
+ @Test
+ void testSettingsServersApplied() throws IOException {
+ Path m2Dir = tempDir.resolve(".m2");
+ Files.createDirectories(m2Dir);
+ Files.writeString(m2Dir.resolve("settings.xml"), """
+
+
+
+
+ my-repo
+ myuser
+ mypassword
+
+
+
+ """);
+
+ String oldHome = System.getProperty("user.home");
+ try {
+ System.setProperty("user.home", tempDir.toString());
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+ Settings settings = session.getSettings();
+ assertNotNull(settings);
+ assertEquals(1, settings.getServers().size());
+ Server server = settings.getServers().get(0);
+ assertEquals("my-repo", server.getId());
+ assertEquals("myuser", server.getUsername());
+ assertEquals("mypassword", server.getPassword());
+ } finally {
+ System.setProperty("user.home", oldHome);
+ }
+ }
+
+ @Test
+ void testSettingsMirrorsApplied() throws IOException {
+ Path m2Dir = tempDir.resolve(".m2");
+ Files.createDirectories(m2Dir);
+ Files.writeString(m2Dir.resolve("settings.xml"), """
+
+
+
+
+ my-mirror
+ https://mirror.example.com/maven2
+ central
+
+
+
+ """);
+
+ String oldHome = System.getProperty("user.home");
+ try {
+ System.setProperty("user.home", tempDir.toString());
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+ Settings settings = session.getSettings();
+ assertNotNull(settings);
+ assertEquals(1, settings.getMirrors().size());
+ assertEquals("my-mirror", settings.getMirrors().get(0).getId());
+ assertEquals(
+ "https://mirror.example.com/maven2",
+ settings.getMirrors().get(0).getUrl());
+ assertEquals("central", settings.getMirrors().get(0).getMirrorOf());
+ } finally {
+ System.setProperty("user.home", oldHome);
+ }
+ }
+
+ @Test
+ void testSettingsRepositories() throws IOException {
+ Path m2Dir = tempDir.resolve(".m2");
+ Files.createDirectories(m2Dir);
+ // Maven 4 top-level repositories in settings
+ Files.writeString(m2Dir.resolve("settings.xml"), """
+
+
+
+
+ custom-repo
+ https://repo.example.com/maven2
+
+
+
+ """);
+
+ String oldHome = System.getProperty("user.home");
+ try {
+ System.setProperty("user.home", tempDir.toString());
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+ Settings settings = session.getSettings();
+ assertNotNull(settings);
+ assertTrue(
+ settings.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())),
+ "Effective settings should include top-level repository");
+ // Session should also expose them as remote repositories
+ assertTrue(
+ session.getRemoteRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())),
+ "Session should include repository from settings");
+ } finally {
+ System.setProperty("user.home", oldHome);
+ }
+ }
+
+ @Test
+ void testLegacyEncryptedPasswordDecrypted() throws IOException {
+ Path m2Dir = tempDir.resolve(".m2");
+ Files.createDirectories(m2Dir);
+ // Legacy master password from the existing IT test resources (mng-8379)
+ // Master password "testtest" encrypted with the default settings.security key
+ Files.writeString(m2Dir.resolve("settings-security.xml"), """
+
+
+ {1wQaa6S/o8MH7FnaTNL53XmhT5O0SEGXQi3gC49o6OY=}
+
+ """);
+ // Server password "testtest" encrypted with the master password above
+ Files.writeString(m2Dir.resolve("settings.xml"), """
+
+
+
+
+ testserver
+ testuser
+ {BteqUEnqHecHM7MZfnj9FwLcYbdInWxou1C929Txa0A=}
+
+
+
+ """);
+
+ String oldHome = System.getProperty("user.home");
+ try {
+ System.setProperty("user.home", tempDir.toString());
+ // With dispatchers active, the legacy password should be decrypted
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.IF_AVAILABLE);
+ Settings settings = session.getSettings();
+ assertNotNull(settings);
+ assertEquals(1, settings.getServers().size());
+ Server server = settings.getServers().get(0);
+ assertEquals("testserver", server.getId());
+ assertEquals("testuser", server.getUsername());
+ assertEquals("testtest", server.getPassword());
+ } finally {
+ System.setProperty("user.home", oldHome);
+ }
+ }
+
+ @Test
+ void testPlaintextPasswordUnchangedWithSecurityModeNone() throws IOException {
+ // SecurityMode.NONE prevents ApiRunner from binding its own SecDispatcherBindings.
+ // Note: in a test environment, discover() may still find a test-scoped SecDispatcherProvider;
+ // this test verifies that plaintext passwords pass through correctly in all modes.
+ Path m2Dir = tempDir.resolve(".m2");
+ Files.createDirectories(m2Dir);
+ Files.writeString(m2Dir.resolve("settings.xml"), """
+
+
+
+
+ testserver
+ testuser
+ plaintext-password
+
+
+
+ """);
+
+ String oldHome = System.getProperty("user.home");
+ try {
+ System.setProperty("user.home", tempDir.toString());
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+ Settings settings = session.getSettings();
+ assertNotNull(settings);
+ assertEquals(1, settings.getServers().size());
+ Server server = settings.getServers().get(0);
+ assertEquals("plaintext-password", server.getPassword());
+ } finally {
+ System.setProperty("user.home", oldHome);
+ }
+ }
+
+ @Test
+ void testSettingsOfflineMode() throws IOException {
+ Path m2Dir = tempDir.resolve(".m2");
+ Files.createDirectories(m2Dir);
+ Files.writeString(m2Dir.resolve("settings.xml"), """
+
+
+ true
+
+ """);
+
+ String oldHome = System.getProperty("user.home");
+ try {
+ System.setProperty("user.home", tempDir.toString());
+ Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+ Settings settings = session.getSettings();
+ assertNotNull(settings);
+ assertTrue(settings.isOffline(), "Settings should have offline mode enabled");
+ } finally {
+ System.setProperty("user.home", oldHome);
+ }
+ }
+}