diff --git a/macos/Sources/Lithe/Views/Run/MavenView.swift b/macos/Sources/Lithe/Views/Run/MavenView.swift index f10eb8a0b..443516fd8 100644 --- a/macos/Sources/Lithe/Views/Run/MavenView.swift +++ b/macos/Sources/Lithe/Views/Run/MavenView.swift @@ -12,6 +12,7 @@ struct MavenView: View { @State private var customGoal = "" @State private var customProfile = "" @State private var settingsPath = "" + @State private var localRepositoryPath = "" @State private var mavenExecutablePath = "" @State private var javaHomePath = "" @@ -621,6 +622,13 @@ struct MavenView: View { model.platformUI.chooseFile(title: "Choose Maven settings.xml", prompt: "Choose") } ) + settingsPathRow( + title: "Local Repository", + value: $localRepositoryPath, + choose: { + model.platformUI.chooseDirectory(title: "Choose Maven Local Repository", prompt: "Choose") + } + ) settingsPathRow( title: "Maven Home or Executable", value: $mavenExecutablePath, @@ -722,6 +730,7 @@ struct MavenView: View { private func presentSettings() { settingsPath = feature.settingsPath ?? "" + localRepositoryPath = feature.localRepositoryPath ?? "" mavenExecutablePath = feature.mavenExecutablePath ?? "" javaHomePath = feature.javaHomePath ?? "" isSettingsSheetPresented = true @@ -730,6 +739,7 @@ struct MavenView: View { private func saveSettings() { feature.updateLocalConfiguration( settingsPath: settingsPath, + localRepositoryPath: localRepositoryPath, mavenExecutablePath: mavenExecutablePath, javaHomePath: javaHomePath ) diff --git a/macos/Sources/LitheCoreContracts/Execution/MavenContracts.swift b/macos/Sources/LitheCoreContracts/Execution/MavenContracts.swift index f116e6c15..4f4374ae9 100644 --- a/macos/Sources/LitheCoreContracts/Execution/MavenContracts.swift +++ b/macos/Sources/LitheCoreContracts/Execution/MavenContracts.swift @@ -219,17 +219,20 @@ package struct MavenLocalConfiguration: Codable, Equatable, Sendable { package var version: Int package var settingsPath: String? + package var localRepositoryPath: String? package var mavenExecutablePath: String? package var javaHomePath: String? package init( version: Int = currentVersion, settingsPath: String? = nil, + localRepositoryPath: String? = nil, mavenExecutablePath: String? = nil, javaHomePath: String? = nil ) { self.version = version self.settingsPath = settingsPath + self.localRepositoryPath = localRepositoryPath self.mavenExecutablePath = mavenExecutablePath self.javaHomePath = javaHomePath } @@ -255,6 +258,7 @@ package struct MavenLaunchContext: Codable, Equatable, Sendable { package let reactorPath: String package let profiles: [String] package let settingsPath: String? + package let localRepositoryPath: String? package let skipTests: Bool package let mavenExecutablePath: String? package let javaHomePath: String? @@ -264,6 +268,7 @@ package struct MavenLaunchContext: Codable, Equatable, Sendable { reactorPath: String, profiles: [String], settingsPath: String?, + localRepositoryPath: String? = nil, skipTests: Bool, mavenExecutablePath: String?, javaHomePath: String? @@ -272,6 +277,7 @@ package struct MavenLaunchContext: Codable, Equatable, Sendable { self.reactorPath = reactorPath self.profiles = profiles self.settingsPath = settingsPath + self.localRepositoryPath = localRepositoryPath self.skipTests = skipTests self.mavenExecutablePath = mavenExecutablePath self.javaHomePath = javaHomePath @@ -316,6 +322,9 @@ package func redactedMavenArgumentsForDisplay(_ arguments: [String]) -> [String] } else if argument.hasPrefix("--settings=") || argument.hasPrefix("-s=") { result.append(String(argument.prefix { $0 != "=" }) + "=") index += 1 + } else if argument.hasPrefix("-Dmaven.repo.local=") { + result.append("-Dmaven.repo.local=") + index += 1 } else { result.append(argument) index += 1 diff --git a/macos/Sources/LitheExecutionModule/Application/ExecutionFeatureModels.swift b/macos/Sources/LitheExecutionModule/Application/ExecutionFeatureModels.swift index 93cbe0206..cca15c2f0 100644 --- a/macos/Sources/LitheExecutionModule/Application/ExecutionFeatureModels.swift +++ b/macos/Sources/LitheExecutionModule/Application/ExecutionFeatureModels.swift @@ -30,6 +30,7 @@ package final class MavenFeatureModel: ObservableObject { package var selectedProfiles: Set { service.selectedProfiles } package var skipTests: Bool { service.skipTests } package var settingsPath: String? { service.settingsPath } + package var localRepositoryPath: String? { service.localRepositoryPath } package var mavenExecutablePath: String? { service.mavenExecutablePath } package var javaHomePath: String? { service.javaHomePath } package var configurationSaveError: String? { service.configurationSaveError } @@ -67,11 +68,13 @@ package final class MavenFeatureModel: ObservableObject { package func updateLocalConfiguration( settingsPath: String?, + localRepositoryPath: String?, mavenExecutablePath: String?, javaHomePath: String? ) { service.updateLocalConfiguration( settingsPath: settingsPath, + localRepositoryPath: localRepositoryPath, mavenExecutablePath: mavenExecutablePath, javaHomePath: javaHomePath ) diff --git a/macos/Sources/LitheExecutionModule/Services/MavenService.swift b/macos/Sources/LitheExecutionModule/Services/MavenService.swift index 66a1ab549..7f7d4306e 100644 --- a/macos/Sources/LitheExecutionModule/Services/MavenService.swift +++ b/macos/Sources/LitheExecutionModule/Services/MavenService.swift @@ -15,6 +15,7 @@ package final class MavenService: ObservableObject { @Published package private(set) var customProfiles: [String] = [] @Published package private(set) var skipTests = false @Published package private(set) var settingsPath: String? + @Published package private(set) var localRepositoryPath: String? @Published package private(set) var mavenExecutablePath: String? @Published package private(set) var javaHomePath: String? @Published package private(set) var configurationSaveError: String? @@ -48,6 +49,7 @@ package final class MavenService: ObservableObject { reactorPath: reactorPath, profiles: selectedProfiles.sorted(), settingsPath: settingsPath, + localRepositoryPath: localRepositoryPath, skipTests: skipTests, mavenExecutablePath: mavenExecutablePath, javaHomePath: javaHomePath @@ -209,16 +211,20 @@ package final class MavenService: ObservableObject { package func updateLocalConfiguration( settingsPath: String?, + localRepositoryPath: String?, mavenExecutablePath: String?, javaHomePath: String? ) { let settings = normalizedLocalPath(settingsPath) + let localRepository = normalizedLocalPath(localRepositoryPath) let executable = normalizedLocalPath(mavenExecutablePath) let javaHome = normalizedLocalPath(javaHomePath) guard settings != self.settingsPath + || localRepository != self.localRepositoryPath || executable != self.mavenExecutablePath || javaHome != self.javaHomePath else { return } self.settingsPath = settings + self.localRepositoryPath = localRepository self.mavenExecutablePath = executable self.javaHomePath = javaHome configurationDidChange() @@ -263,6 +269,7 @@ package final class MavenService: ObservableObject { customProfiles = [] skipTests = false settingsPath = nil + localRepositoryPath = nil mavenExecutablePath = nil javaHomePath = nil configurationFingerprint = nil @@ -292,6 +299,7 @@ package final class MavenService: ObservableObject { reactorPath: reactorPath, profiles: selectedProfiles.sorted(), settingsPath: settingsPath, + localRepositoryPath: localRepositoryPath, skipTests: skipTests, mavenExecutablePath: mavenExecutablePath, javaHomePath: javaHomePath @@ -426,6 +434,7 @@ package final class MavenService: ObservableObject { customProfiles = normalizedProfiles(portable?.customProfiles ?? []) skipTests = portable?.skipTests ?? false settingsPath = normalizedLocalPath(stored?.local?.settingsPath) + localRepositoryPath = normalizedLocalPath(stored?.local?.localRepositoryPath) mavenExecutablePath = normalizedLocalPath(stored?.local?.mavenExecutablePath) javaHomePath = normalizedLocalPath(stored?.local?.javaHomePath) } @@ -482,6 +491,7 @@ package final class MavenService: ObservableObject { ), local: MavenLocalConfiguration( settingsPath: settingsPath, + localRepositoryPath: localRepositoryPath, mavenExecutablePath: mavenExecutablePath, javaHomePath: javaHomePath ) diff --git a/rust/lithe-core/src/execution/configuration.rs b/rust/lithe-core/src/execution/configuration.rs index 8508af92f..7d871778e 100644 --- a/rust/lithe-core/src/execution/configuration.rs +++ b/rust/lithe-core/src/execution/configuration.rs @@ -2403,7 +2403,10 @@ fn detect_requirements( if maven_root.join("mvnw").exists() { maven.wrapper = Some("./mvnw".to_string()); } - maven.version = maven_wrapper_version(maven_root); + // Wrapper distribution is a floor for system Maven, not an exact pin. A + // newer installed Maven (for example 3.9.x against a 3.6.x wrapper URL) + // remains valid for launch planning and run diagnostics. + maven.minimum_version = maven_wrapper_version(maven_root); let mut toolchains = BTreeMap::new(); let consumes = |toolchain: &str| { configurations.iter().any(|configuration| { @@ -2485,11 +2488,12 @@ fn toolchain_diagnostics( .as_deref() .or(requirement.version.as_deref()); if let Some(required) = required_version { - if !version_satisfies( - &candidate.version, - required, - requirement.minimum_version.is_some(), - ) { + // Maven wrapper properties historically landed in `version`. Treat + // that field as a minimum for Maven so already-written requirement + // documents do not block newer system Maven installs. + let treat_as_minimum = requirement.minimum_version.is_some() + || (requirement.kind == "maven" && requirement.version.is_some()); + if !version_satisfies(&candidate.version, required, treat_as_minimum) { append_toolchain_diagnostics( &mut diagnostics, &consumer_ids, diff --git a/rust/lithe-core/src/lsp/interface/engine.rs b/rust/lithe-core/src/lsp/interface/engine.rs index 55125f238..45b20266b 100644 --- a/rust/lithe-core/src/lsp/interface/engine.rs +++ b/rust/lithe-core/src/lsp/interface/engine.rs @@ -3560,6 +3560,7 @@ mod tests { "enterprise".to_string(), ], settings_path: Some("/local/settings.xml".to_string()), + local_repository_path: None, skip_tests: true, maven_executable_path: Some("/local/maven/bin/mvn".to_string()), java_home_path: Some("/local/jdk".to_string()), diff --git a/rust/lithe-core/src/project/maven.rs b/rust/lithe-core/src/project/maven.rs index c9c9e5ebf..342768e78 100644 --- a/rust/lithe-core/src/project/maven.rs +++ b/rust/lithe-core/src/project/maven.rs @@ -45,6 +45,8 @@ pub struct MavenLaunchContextRequest { #[serde(default)] pub settings_path: Option, #[serde(default)] + pub local_repository_path: Option, + #[serde(default)] pub skip_tests: bool, #[serde(default)] pub maven_executable_path: Option, @@ -82,6 +84,7 @@ struct ValidatedMavenContext { canonical_reactor: PathBuf, profiles: Vec, settings_path: Option, + local_repository_path: Option, skip_tests: bool, maven_executable_path: Option, java_home_path: Option, @@ -134,6 +137,7 @@ pub(crate) fn launch_plan_with_arguments( let arguments = maven_arguments( &validated.profiles, validated.settings_path.as_deref(), + validated.local_repository_path.as_deref(), module.as_deref(), also_make, validated.skip_tests, @@ -143,6 +147,7 @@ pub(crate) fn launch_plan_with_arguments( &validated.reactor_path, &validated.profiles, validated.settings_path.as_deref(), + validated.local_repository_path.as_deref(), validated.skip_tests, validated.maven_executable_path.as_deref(), validated.java_home_path.as_deref(), @@ -240,6 +245,10 @@ fn validated_maven_context( canonical_reactor, profiles: normalized_profiles(context.profiles)?, settings_path: normalized_local_path(context.settings_path, "Maven settings")?, + local_repository_path: normalized_local_path( + context.local_repository_path, + "Maven local repository", + )?, skip_tests: context.skip_tests, maven_executable_path: normalized_local_path( context.maven_executable_path, @@ -253,6 +262,7 @@ fn validated_maven_context( pub(crate) fn maven_arguments( profiles: &[String], settings_path: Option<&str>, + local_repository_path: Option<&str>, module: Option<&str>, also_make: bool, skip_tests: bool, @@ -265,6 +275,9 @@ pub(crate) fn maven_arguments( if let Some(settings_path) = settings_path { arguments.extend(["-s".to_string(), settings_path.to_string()]); } + if let Some(local_repository_path) = local_repository_path { + arguments.push(format!("-Dmaven.repo.local={local_repository_path}")); + } if let Some(module) = module.filter(|value| *value != ".") { arguments.extend(["-pl".to_string(), module.to_string()]); if also_make { @@ -359,6 +372,7 @@ fn maven_context_fingerprint( reactor_path: &str, profiles: &[String], settings_path: Option<&str>, + local_repository_path: Option<&str>, skip_tests: bool, maven_executable_path: Option<&str>, java_home_path: Option<&str>, @@ -369,6 +383,7 @@ fn maven_context_fingerprint( reactor_path.to_string(), profiles.join(","), settings_path.unwrap_or_default().to_string(), + local_repository_path.unwrap_or_default().to_string(), skip_tests.to_string(), maven_executable_path.unwrap_or_default().to_string(), java_home_path.unwrap_or_default().to_string(), diff --git a/rust/lithe-core/src/tests/languages.rs b/rust/lithe-core/src/tests/languages.rs index e4808d33f..fd8803a04 100644 --- a/rust/lithe-core/src/tests/languages.rs +++ b/rust/lithe-core/src/tests/languages.rs @@ -354,6 +354,7 @@ fn maven_jdt_configuration_includes_module_java_source_paths() { reactor_path: ".".to_string(), profiles: Vec::new(), settings_path: None, + local_repository_path: None, skip_tests: false, maven_executable_path: None, java_home_path: None, diff --git a/rust/lithe-core/src/tests/run_configuration.rs b/rust/lithe-core/src/tests/run_configuration.rs index a7cd33954..db3af5728 100644 --- a/rust/lithe-core/src/tests/run_configuration.rs +++ b/rust/lithe-core/src/tests/run_configuration.rs @@ -206,9 +206,13 @@ fn run_configuration_generation_uses_a_maven_project_below_the_workspace() { "./mvnw" ); assert_eq!( - response["data"]["toolchainRequirements"]["toolchains"]["project-maven"]["version"], + response["data"]["toolchainRequirements"]["toolchains"]["project-maven"]["minimumVersion"], "3.9.9" ); + assert!( + response["data"]["toolchainRequirements"]["toolchains"]["project-maven"]["version"] + .is_null() + ); fs::create_dir_all(root.join(".lithe/run")).unwrap(); fs::write( @@ -1576,9 +1580,13 @@ fn run_configuration_generation_detects_declared_toolchain_versions() { "temurin" ); assert_eq!( - generated["data"]["toolchainRequirements"]["toolchains"]["project-maven"]["version"], + generated["data"]["toolchainRequirements"]["toolchains"]["project-maven"]["minimumVersion"], "3.9.9" ); + assert!( + generated["data"]["toolchainRequirements"]["toolchains"]["project-maven"]["version"] + .is_null() + ); fs::remove_dir_all(root).unwrap(); } @@ -1748,6 +1756,67 @@ fn run_configuration_inspection_summarizes_changed_inputs() { fs::remove_dir_all(root).unwrap(); } +#[test] +fn maven_wrapper_version_accepts_newer_system_maven() { + let root = temporary_root("run-config-maven-wrapper-minimum"); + fs::create_dir_all(root.join(".lithe/run")).unwrap(); + fs::create_dir_all(root.join(".lithe/toolchains")).unwrap(); + fs::write( + root.join(".lithe/run/generated.json"), + r#"{"version":1,"configurations":[{"id":"spring","name":"Spring","type":"spring-boot.maven","toolchains":{"java":"project-jdk","maven":"project-maven"}}]}"#, + ) + .unwrap(); + // Legacy documents stored the wrapper distribution under `version`. + // That value is a floor for system Maven, not an exact pin. + fs::write( + root.join(".lithe/toolchains/requirements.json"), + r#"{"version":1,"toolchains":{"project-maven":{"type":"maven","version":"3.6.3","java":"project-jdk"}}}"#, + ) + .unwrap(); + + let resolve = |version: &str| -> Value { + serde_json::from_str(&execute_json( + &serde_json::json!({ + "id": "resolve-maven", + "command": "runConfig.resolve", + "payload": { + "root": root, + "toolchainCandidates": [{ + "id": "project-maven", + "type": "maven", + "version": version, + "vendor": "" + }] + } + }) + .to_string(), + )) + .unwrap() + }; + + let newer = resolve("3.9.16"); + assert!( + newer["data"]["diagnostics"] + .as_array() + .unwrap() + .iter() + .all(|value| value["code"] != "toolchainVersionMismatch"), + "{newer}" + ); + + let older = resolve("3.5.4"); + assert!( + older["data"]["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|value| value["code"] == "toolchainVersionMismatch"), + "{older}" + ); + + fs::remove_dir_all(root).unwrap(); +} + #[test] fn run_configuration_resolve_matches_toolchains_and_rejects_unsafe_paths() { let root = temporary_root("run-config-toolchain-resolution"); diff --git a/scripts/verify-shared-contracts.sh b/scripts/verify-shared-contracts.sh index cad96cac0..b9063c747 100755 --- a/scripts/verify-shared-contracts.sh +++ b/scripts/verify-shared-contracts.sh @@ -34,7 +34,7 @@ windows_lithe_theme="windows/tauri/src/extensions/themes/builtin/lithe.json" abort "Maven portable required fields differ from v1" unless portable.fetch("required").sort == portable_fields abort "Maven launch-context schema ID mismatch" unless launch.fetch("$id").end_with?("/maven-launch-context-v1.schema.json") - launch_fields = %w[javaHomePath mavenExecutablePath profiles reactorPath settingsPath skipTests version] + launch_fields = %w[javaHomePath localRepositoryPath mavenExecutablePath profiles reactorPath settingsPath skipTests version] abort "Maven launch-context fields differ from v1" unless launch.fetch("properties").keys.sort == launch_fields.sort abort "Maven launch-context required fields differ from v1" unless launch.fetch("required").sort == %w[profiles reactorPath skipTests version] diff --git a/shared/contracts/application-boundary.md b/shared/contracts/application-boundary.md index dabe1dffb..29aece89e 100644 --- a/shared/contracts/application-boundary.md +++ b/shared/contracts/application-boundary.md @@ -319,10 +319,10 @@ assemble Maven arguments. Portable profile and Skip Tests defaults conform to [`maven-portable-configuration-v1.schema.json`](maven-portable-configuration-v1.schema.json). The transient Core request conforms to [`maven-launch-context-v1.schema.json`](maven-launch-context-v1.schema.json). -External `settings.xml`, Maven executable, and Maven JDK paths remain in a -machine-local store. They may be supplied transiently to Core for planning and -fingerprinting, but Core never opens `settings.xml` or serializes those paths -into the portable project context. +External `settings.xml`, local repository, Maven executable, and Maven JDK +paths remain in a machine-local store. They may be supplied transiently to Core +for planning and fingerprinting, but Core never opens `settings.xml` or +serializes those paths into the portable project context. The Java language-server startup consumes that same context. Core exposes the selected `settings.xml` to JDT LS as diff --git a/shared/contracts/maven-launch-context-v1.schema.json b/shared/contracts/maven-launch-context-v1.schema.json index f0bca7d08..74d995733 100644 --- a/shared/contracts/maven-launch-context-v1.schema.json +++ b/shared/contracts/maven-launch-context-v1.schema.json @@ -13,6 +13,7 @@ "items": { "type": "string", "minLength": 1 } }, "settingsPath": { "type": ["string", "null"] }, + "localRepositoryPath": { "type": ["string", "null"] }, "skipTests": { "type": "boolean" }, "mavenExecutablePath": { "type": ["string", "null"] }, "javaHomePath": { "type": ["string", "null"] } diff --git a/shared/contracts/rust-core-api.md b/shared/contracts/rust-core-api.md index 39ca24f57..36cbc5316 100644 --- a/shared/contracts/rust-core-api.md +++ b/shared/contracts/rust-core-api.md @@ -935,18 +935,19 @@ entry is a lifecycle or custom goal. Later entries may be ordinary Maven CLI arguments such as `-Dname=value` or `-q`. They remain separate process arguments and are never interpreted by a shell. Context version 1 contains the workspace-relative `reactorPath`, -selected `profiles`, optional platform-local `settingsPath`, `skipTests`, and -optional Maven/JDK paths used only for the configuration fingerprint. The -response contains the `project-maven` toolchain reference, an argument array, -the workspace-relative reactor working directory, and a deterministic SHA-256 -configuration fingerprint. Profiles are sorted and de-duplicated. Module plans -from `maven.launchPlan` and Maven-backed Run and Debug plans use -`-pl -am`. Settings use `-s`; skipped tests use +selected `profiles`, optional platform-local `settingsPath` and +`localRepositoryPath`, `skipTests`, and optional Maven/JDK paths used only for +the configuration fingerprint. The response contains the `project-maven` +toolchain reference, an argument array, the workspace-relative reactor working +directory, and a deterministic SHA-256 configuration fingerprint. Profiles are +sorted and de-duplicated. Module plans from `maven.launchPlan` and Maven-backed +Run and Debug plans use `-pl -am`. Settings use `-s`; a local +repository override uses `-Dmaven.repo.local=`; skipped tests use `-DskipTests`. Explicit Run `cwd`, Profiles, and `extensions.maven.skipTests` -values override the project context, including `skipTests: false`. -The core never reads `settings.xml` and never copies its path into a portable -project document. Maven itself continues to read `.mvn/maven.config`; the plan -does not expand or duplicate that file's arguments. Fixtures are in +values override the project context, including `skipTests: false`. The core +never reads `settings.xml` and never copies its path into a portable project +document. Maven itself continues to read `.mvn/maven.config`; the plan does not +expand or duplicate that file's arguments. Fixtures are in `shared/fixtures/maven/launch-plan-v1.json`. `maven.diagnostics` accepts `{ "root": string, "output": string }` and returns diff --git a/shared/fixtures/maven/launch-plan-v1.json b/shared/fixtures/maven/launch-plan-v1.json index 3dfa65a25..8d95427e4 100644 --- a/shared/fixtures/maven/launch-plan-v1.json +++ b/shared/fixtures/maven/launch-plan-v1.json @@ -8,6 +8,7 @@ "reactorPath": "projects/demo", "profiles": ["qa", "dev", "dev"], "settingsPath": "/Users/example/.m2/settings.xml", + "localRepositoryPath": "/Users/example/.m2/custom-repository", "skipTests": true, "mavenExecutablePath": "/opt/apache-maven/bin/mvn", "javaHomePath": "/Library/Java/JavaVirtualMachines/example/Contents/Home" @@ -24,6 +25,7 @@ "dev,qa", "-s", "/Users/example/.m2/settings.xml", + "-Dmaven.repo.local=/Users/example/.m2/custom-repository", "-pl", "service-api", "-am", @@ -31,7 +33,7 @@ "verify" ], "workingDirectory": "projects/demo", - "configurationFingerprint": "sha256:8e665a85f0c0a9fa5567cfc88d90cc1e794a34810b4617662c5610699ad22df6" + "configurationFingerprint": "sha256:0e84a43d287c8e889ff15740114e8de0d719cbf3ca6b6ce3530b97e164770f4a" } }, { @@ -49,7 +51,7 @@ "executable": { "toolchain": "project-maven" }, "arguments": ["-B", "-ntp", "spring-boot:run"], "workingDirectory": ".", - "configurationFingerprint": "sha256:225165d5264a20dba15802483efebc3a0d29abb16f36702d599324891e242511" + "configurationFingerprint": "sha256:65a5de6ae2a3135ad844ed30ba8558865f97a6696654a83d3d1d8544cc63380f" } }, { @@ -79,7 +81,7 @@ "-DforceStdout" ], "workingDirectory": ".", - "configurationFingerprint": "sha256:225165d5264a20dba15802483efebc3a0d29abb16f36702d599324891e242511" + "configurationFingerprint": "sha256:65a5de6ae2a3135ad844ed30ba8558865f97a6696654a83d3d1d8544cc63380f" } } ] diff --git a/windows/tauri/src-tauri/src/maven.rs b/windows/tauri/src-tauri/src/maven.rs index 4187845fd..6aef0c935 100644 --- a/windows/tauri/src-tauri/src/maven.rs +++ b/windows/tauri/src-tauri/src/maven.rs @@ -31,6 +31,8 @@ pub struct MavenLocalConfiguration { #[serde(default)] pub settings_path: Option, #[serde(default)] + pub local_repository_path: Option, + #[serde(default)] pub maven_executable_path: Option, #[serde(default)] pub java_home_path: Option, diff --git a/windows/tauri/src/features/maven/components/maven-pane.tsx b/windows/tauri/src/features/maven/components/maven-pane.tsx index 1c99786e3..e26016da7 100644 --- a/windows/tauri/src/features/maven/components/maven-pane.tsx +++ b/windows/tauri/src/features/maven/components/maven-pane.tsx @@ -137,6 +137,12 @@ function MavenSettingsDialog({ directory: boolean; }> = [ { id: "maven-settings-xml", field: "settingsPath", label: "settings.xml", directory: false }, + { + id: "maven-local-repository", + field: "localRepositoryPath", + label: t("maven.localRepository"), + directory: true, + }, { id: "maven-executable", field: "mavenExecutablePath", @@ -233,6 +239,7 @@ export default function MavenPane() { const customProfiles = useMavenStore((state) => state.customProfiles); const skipTests = useMavenStore((state) => state.skipTests); const settingsPath = useMavenStore((state) => state.settingsPath); + const localRepositoryPath = useMavenStore((state) => state.localRepositoryPath); const mavenExecutablePath = useMavenStore((state) => state.mavenExecutablePath); const javaHomePath = useMavenStore((state) => state.javaHomePath); const configurationSaveError = useMavenStore((state) => state.configurationSaveError); @@ -747,7 +754,7 @@ export default function MavenPane() { ) : null} {settingsDialogOpen ? ( setSettingsDialogOpen(false)} onSave={actions.updateLocalConfiguration} diff --git a/windows/tauri/src/features/maven/stores/maven.store.test.ts b/windows/tauri/src/features/maven/stores/maven.store.test.ts index acdb7a629..814328b62 100644 --- a/windows/tauri/src/features/maven/stores/maven.store.test.ts +++ b/windows/tauri/src/features/maven/stores/maven.store.test.ts @@ -157,6 +157,7 @@ describe("Maven workspace state", () => { local: { version: 1, settingsPath: "C:/Users/example/.m2/settings.xml", + localRepositoryPath: "D:/maven-repo", mavenExecutablePath: "D:/Tools/apache-maven", javaHomePath: "C:/Java/jdk-21", }, @@ -170,6 +171,7 @@ describe("Maven workspace state", () => { reactorPath: "reactor", profiles: ["dev", "qa"], settingsPath: "C:/Users/example/.m2/settings.xml", + localRepositoryPath: "D:/maven-repo", skipTests: true, mavenExecutablePath: "D:/Tools/apache-maven", javaHomePath: "C:/Java/jdk-21", @@ -186,6 +188,7 @@ describe("Maven workspace state", () => { store.getState().actions.updateLocalConfiguration({ settingsPath: "C:/Users/example/.m2/settings.xml", + localRepositoryPath: "D:/maven-repo", mavenExecutablePath: "D:/Tools/apache-maven", javaHomePath: "C:/Java/jdk-21", }); @@ -200,9 +203,11 @@ describe("Maven workspace state", () => { skipTests: false, }); expect(configuration?.portable).not.toHaveProperty("settingsPath"); + expect(configuration?.portable).not.toHaveProperty("localRepositoryPath"); expect(configuration?.local).toEqual({ version: 1, settingsPath: "C:/Users/example/.m2/settings.xml", + localRepositoryPath: "D:/maven-repo", mavenExecutablePath: "D:/Tools/apache-maven", javaHomePath: "C:/Java/jdk-21", }); diff --git a/windows/tauri/src/features/maven/stores/maven.store.ts b/windows/tauri/src/features/maven/stores/maven.store.ts index 1f4614012..73d76aec7 100644 --- a/windows/tauri/src/features/maven/stores/maven.store.ts +++ b/windows/tauri/src/features/maven/stores/maven.store.ts @@ -71,6 +71,7 @@ export interface MavenState { customProfiles: string[]; skipTests: boolean; settingsPath: string; + localRepositoryPath: string; mavenExecutablePath: string; javaHomePath: string; configurationSaveError: string | null; @@ -134,6 +135,7 @@ export function mavenLaunchContext(state: MavenState): MavenLaunchContext | null reactorPath: state.project.relativePath, profiles: normalizedProfiles(state.selectedProfiles), settingsPath: state.settingsPath || null, + localRepositoryPath: state.localRepositoryPath || null, skipTests: state.skipTests, mavenExecutablePath: state.mavenExecutablePath || null, javaHomePath: state.javaHomePath || null, @@ -150,6 +152,7 @@ function storedConfiguration(state: MavenState): MavenStoredConfiguration { const local: MavenLocalConfiguration = { version: 1, settingsPath: state.settingsPath || null, + localRepositoryPath: state.localRepositoryPath || null, mavenExecutablePath: state.mavenExecutablePath || null, javaHomePath: state.javaHomePath || null, }; @@ -158,9 +161,11 @@ function storedConfiguration(state: MavenState): MavenStoredConfiguration { function displayArguments(arguments_: readonly string[]): string { return arguments_ - .map((argument, index) => - index > 0 && arguments_[index - 1] === "-s" ? "" : argument, - ) + .map((argument, index) => { + if (index > 0 && arguments_[index - 1] === "-s") return ""; + if (argument.startsWith("-Dmaven.repo.local=")) return "-Dmaven.repo.local="; + return argument; + }) .join(" "); } @@ -225,6 +230,7 @@ export const createMavenStore = ( customProfiles: [], skipTests: false, settingsPath: "", + localRepositoryPath: "", mavenExecutablePath: "", javaHomePath: "", configurationSaveError: null, @@ -277,6 +283,7 @@ export const createMavenStore = ( customProfiles: [], skipTests: false, settingsPath: "", + localRepositoryPath: "", mavenExecutablePath: "", javaHomePath: "", reloadRequired: false, @@ -306,6 +313,7 @@ export const createMavenStore = ( customProfiles, skipTests: stored.portable?.skipTests ?? false, settingsPath: normalizedPath(stored.local?.settingsPath), + localRepositoryPath: normalizedPath(stored.local?.localRepositoryPath), mavenExecutablePath: normalizedPath(stored.local?.mavenExecutablePath), javaHomePath: normalizedPath(stored.local?.javaHomePath), reloadRequired: false, @@ -321,6 +329,7 @@ export const createMavenStore = ( customProfiles: [], skipTests: false, settingsPath: "", + localRepositoryPath: "", mavenExecutablePath: "", javaHomePath: "", }); @@ -369,12 +378,14 @@ export const createMavenStore = ( updateLocalConfiguration: (settings) => { const next = { settingsPath: normalizedPath(settings.settingsPath), + localRepositoryPath: normalizedPath(settings.localRepositoryPath), mavenExecutablePath: normalizedPath(settings.mavenExecutablePath), javaHomePath: normalizedPath(settings.javaHomePath), }; const state = get(); if ( next.settingsPath === state.settingsPath && + next.localRepositoryPath === state.localRepositoryPath && next.mavenExecutablePath === state.mavenExecutablePath && next.javaHomePath === state.javaHomePath ) { diff --git a/windows/tauri/src/features/maven/types/maven.types.ts b/windows/tauri/src/features/maven/types/maven.types.ts index 639780c7f..644a1a1a2 100644 --- a/windows/tauri/src/features/maven/types/maven.types.ts +++ b/windows/tauri/src/features/maven/types/maven.types.ts @@ -49,6 +49,7 @@ export interface MavenLaunchContext { reactorPath: string; profiles: string[]; settingsPath?: string | null; + localRepositoryPath?: string | null; skipTests: boolean; mavenExecutablePath?: string | null; javaHomePath?: string | null; @@ -80,6 +81,7 @@ export interface MavenPortableConfiguration { export interface MavenLocalConfiguration { version: 1; settingsPath?: string | null; + localRepositoryPath?: string | null; mavenExecutablePath?: string | null; javaHomePath?: string | null; } @@ -91,6 +93,7 @@ export interface MavenStoredConfiguration { export interface MavenSettings { settingsPath: string; + localRepositoryPath: string; mavenExecutablePath: string; javaHomePath: string; } diff --git a/windows/tauri/src/i18n/locale.ts b/windows/tauri/src/i18n/locale.ts index 705a01365..facbe534c 100644 --- a/windows/tauri/src/i18n/locale.ts +++ b/windows/tauri/src/i18n/locale.ts @@ -1442,6 +1442,7 @@ const catalogs = { "maven.profiles": "Profiles", "maven.settings": "Maven Settings", "maven.automatic": "Automatic", + "maven.localRepository": "Local repository", "maven.mavenExecutable": "Maven home or executable", "maven.javaHome": "Maven JDK Home", "maven.stop": "Stop Maven task", @@ -5424,6 +5425,7 @@ const catalogs = { "maven.profiles": "Profiles", "maven.settings": "Maven 设置", "maven.automatic": "自动检测", + "maven.localRepository": "本地仓库", "maven.mavenExecutable": "Maven 主目录 / 可执行文件", "maven.javaHome": "Maven JDK 主目录", "maven.stop": "停止 Maven 任务",