Skip to content

Commit 718074f

Browse files
authored
Merge pull request #999 from 1lck/fix/815-project-aware-auto-jdk
fix(runtime): 自动选择符合项目要求的 JDK,统一两端启动与设置 (#815)
1 parent 93cee7a commit 718074f

31 files changed

Lines changed: 929 additions & 52 deletions

‎.agents/notes/implemented/architecture/2026-09-21-maven-settings-reach-the-language-server.md‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,41 @@ Maven、版本、路径和来源,例如「自动 → JDK 21.0.4 · 路径 ·
233233
版本比较。运行面板保留一步进入「设置 → 运行配置」并定位当前服务的入口;两端设置窗口
234234
每次分类请求都会重新定位,重复请求同一分类同样生效。
235235

236-
「自动」本身的选择规则(是否按项目要求版本挑选、Windows 候选排序)属于 #815,不在此处
237-
改变。
236+
### 自动 JDK 选择(#815)
237+
238+
自动模式先满足项目已有的 Java 最低版本要求,再按来源选择。显式选择的项目 JDK、
239+
服务 JDK 或 Maven JDK 不会被自动替换。两端共用 Core `runConfig.selectJava`,版本
240+
比较复用现有需求诊断中的逻辑,兼容 Java 8 的 `1.8` 表达;不要在 Swift/TypeScript
241+
各写一套版本比较,也不要重新解析 POM 或建立另一个项目模型。JDT LS 仍拥有语言
242+
服务项目状态,本次只消费既有生成文档中的最低版本,选择机器上的启动工具。
243+
244+
来源顺序为 JAVA_HOME、PATH、普通安装、Windows 项目内安装。来源相同时先按数值
245+
版本降序,再按候选标识排序,避免目录枚举顺序左右结果。macOS 没有版本要求时
246+
保留原有 JAVA_HOME/首个发现候选回退;Windows 无要求时仍按来源选取,但修正原本
247+
覆盖 JAVA_HOME 优先级的字符串排序。没有任何兼容版本时继续保留回退选择,同时在
248+
生效值旁显示安装兼容 JDK 的提示。损坏或未来版本的需求文件明确报错,不按无要求处理。
249+
250+
例如项目要求 Java 17、JAVA_HOME 为 8、PATH 为 17、普通目录有 21 时,自动选择
251+
PATH 的 17;显式选择 8 仍保留 8,由既有启动诊断报告不兼容。不要仅让设置页显示
252+
17,而启动仍取候选列表第一个 8。Windows 的发现结果也把实际自动候选放在首位,
253+
保证 Run 诊断与宿主启动一致。Windows 设置与服务表单在项目发现结果更新后重新解析
254+
生效值,即使自动路径字段仍为空;旧请求不能覆盖刷新后的结果。macOS 的 Run、Maven、
255+
测试和设置经过同一个选择入口。
256+
257+
考虑过只改 Windows 排序,但这不能解决两端忽略项目要求的问题。也没有直接选最高
258+
版本:兼容的 JAVA_HOME/PATH 是用户已有环境意图,应优先保留。选择策略只读取现有
259+
工作区需求文档,不生成文件、探测进程或写入安装包。macOS 启动复用当前项目已经
260+
探测的候选;尚未完成完整发现时只探测 Java 并保留到项目切换或显式刷新,不能为了
261+
选择 Java 连带探测 Maven。无版本要求且 JAVA_HOME 可用时保留不探测的快速路径。
262+
PATH 仅用于识别已有候选的来源(平台负责解析符号链接),不额外扩大默认候选集合;代价是需求变化后需要重新解析
263+
选择,不能把上一次项目的结果缓存成全局默认值。
264+
265+
回归验证使用共享 `automatic-java-selection.json`,覆盖枚举反序、数值补丁版本、
266+
旧版 Java 格式、兼容候选筛选、无要求/无匹配回退、空候选及损坏文档。macOS 的
267+
`automaticJavaUsesProjectMinimumAndPreservesExplicitSelections` 验证设置选择、Maven
268+
继承、显式覆盖与项目切换;Windows 的
269+
`automatic_java_selection_reads_requirement_changes_without_probing` 验证同一工作区
270+
需求变化和显式路径旁路。原生实机验收仍需覆盖 Run、Maven 目标和测试运行。
238271

239272
### Maven 设置页的检测边界(#844)
240273

‎docs/development/platform-parity-matrix.csv‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ ai-commit-generation,AI,AI 提交信息,Provider 配置与提交信息生成,已
4242
ai-chat-session,AI,AI 对话,对话会话与流式响应,未实现,不适用,已实现,待验证,AI,Windows 验证新建会话、流式输出、取消和失败恢复;macOS 需要先定义产品范围。,,macos/Sources/Lithe/Platform/MacOS/AI,windows/tauri/src/features/ai/components/chat/ai-chat.tsx; windows/tauri/src/features/ai/services/ai-chat-service.ts
4343
ai-chat-history,AI,AI 对话,对话历史、置顶与归档,未实现,不适用,已实现,待验证,AI,Windows 验证历史加载、重命名、置顶、归档、删除和重启后持久化。,,macos/Sources/Lithe/Platform/MacOS/AI,windows/tauri/src/features/ai/services/ai-chat-history-service.ts; windows/tauri/src/features/layout/components/sidebar/sidebar-history.tsx
4444
ai-chat-providers,AI,AI 对话,多 Provider 与工作区范围,未实现,不适用,已实现,待验证,AI,Windows 验证 Provider 切换、凭据边界、工作区范围和模型错误提示。,,macos/Sources/Lithe/Platform/MacOS/AI,windows/tauri/src/features/ai/services/providers; windows/tauri/src/features/ai/lib/ai-workspace-scope.ts
45-
java-runtime-discovery,Java,Java/Maven,JDK 与 Maven 工具链发现,已实现,待验证,已实现,待验证,Java / Maven,配置多个 JDK/Maven 候选,验证发现、选择、版本不匹配和错误提示。,,macos/Sources/Lithe/Services/Java; macos/Sources/Lithe/Views/Run,windows/tauri/src/features/maven; windows/tauri/src/features/workspace
45+
java-runtime-discovery,Java,Java/Maven,JDK 与 Maven 工具链发现及按项目要求自动选择 JDK,已实现,待验证,已实现,待验证,Java / Maven,两端安装 JDK 8/17/21:项目要求 17 且 JAVA_HOME 为 8 时验证自动选择兼容版本;核对设置、Run、Maven 目标、测试使用同一 JDK。验证显式选择不变、没有兼容版本时回退并提示,以及无要求时的平台默认顺序。,,macos/Sources/Lithe/Services/Java; macos/Sources/Lithe/Views/Run; macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift; macos/Tests/LitheTests/MavenRuntimeTests.swift; rust/lithe-core/src/execution/java_selection.rs,windows/tauri/src/features/maven; windows/tauri/src/features/workspace; windows/tauri/src-tauri/src/run.rs; shared/fixtures/run-configuration/automatic-java-selection.json
4646
java-maven-project-settings,Java,Java/Maven,按项目配置 Maven 工具链、settings.xml 与本地仓库,已实现,待验证,已实现,待验证,Java / Maven,在 macOS 项目设置和 Windows Maven 设置页分别切换两个项目,配置 Maven、JDK、settings.xml 与本地仓库,验证保存重开、自动检测展示及项目间隔离;Windows 还需验证写盘失败后可直接重试。 Windows 自定义 settings.xml 后,仓库留空应显示该文件中的目录;验证 XML 实体和 CDATA、继承的项目 JDK/Maven、切换项目丢弃未保存草稿;冷启动 Wrapper 不应触发下载,版本探测卡住时设置窗口保持响应并在超时后清理探测进程;Run 配置显式选择的 Maven/JDK 优先于项目设置,未设置的字段分别继承 Maven 设置;确认主任务与前置任务使用相同工具链,Maven 设置上下文缺失时仍保留显式覆盖,均未配置时交由平台自动发现。,,macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Services/Java/ProjectRuntimeService.swift,windows/tauri/src/features/settings/components/tabs/maven-settings-panel.tsx; windows/tauri/src/features/maven/stores/maven.store.ts; windows/tauri/src-tauri/src/maven.rs; windows/tauri/src-tauri/src/maven/settings_xml.rs; windows/tauri/src/features/maven/components/maven-detected-value.test.tsx; windows/tauri/src/features/settings/components/tabs/maven-settings-panel.test.tsx; windows/tauri/src/features/run/stores/run.store.ts; windows/tauri/src/features/run/stores/run-maven-context.test.ts
4747
java-maven-project,Java,Java/Maven,项目根、模块与源码集识别,已实现,待验证,已实现,待验证,Java / Maven,使用单模块、多模块和非标准源码目录 fixture 对比项目模型。,,macos/Sources/Lithe/Services/Java; shared/contracts/application-boundary.md,windows/tauri/src/features/maven; windows/tauri/src/features/workspace
4848
java-maven-profiles-dependencies,Java,Java/Maven,Profile 与依赖树,已实现,待验证,已实现,待验证,Java / Maven,"切换 Maven profile 并刷新依赖树,确认模块归属、顺序、版本冲突/重复/依赖管理标注和错误边界;在依赖节点超过 6,000 个的项目中确认依赖树完整加载。 Windows 在 Maven 面板展开「依赖项」应直接加载依赖树,失败时显示宿主返回的真实原因,而不是通用的“无法加载 Maven 依赖”(#970)。",,macos/Sources/Lithe/Views/Run; macos/Sources/Lithe/Services/Java; macos/Sources/LitheExecutionModule/Services/MavenService.swift; macos/Sources/Lithe/Platform/MacOS/Persistence/MacMavenDependencyOutputStore.swift; rust/lithe-core/src/project/maven_dependency_tree.rs,windows/tauri/src/features/maven; windows/tauri/src-tauri/src/maven.rs; rust/lithe-core/src/project/maven_dependency_tree.rs

‎docs/development/platform-parity-matrix.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140

141141
| 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 |
142142
| --- | --- | --- | --- | --- | --- | --- |
143-
| Java/Maven | **JDK 与 Maven 工具链发现**<br><sub>java-runtime-discovery</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Services/Java`、`macos/Sources/Lithe/Views/Run`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src/features/maven`、`windows/tauri/src/features/workspace`</sub> | Java / Maven | 配置多个 JDK/Maven 候选,验证发现、选择、版本不匹配和错误提示。 | |
143+
| Java/Maven | **JDK 与 Maven 工具链发现及按项目要求自动选择 JDK**<br><sub>java-runtime-discovery</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Services/Java`、`macos/Sources/Lithe/Views/Run`、`macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift`、`macos/Tests/LitheTests/MavenRuntimeTests.swift`、`rust/lithe-core/src/execution/java_selection.rs`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src/features/maven`、`windows/tauri/src/features/workspace`、`windows/tauri/src-tauri/src/run.rs`、`shared/fixtures/run-configuration/automatic-java-selection.json`</sub> | Java / Maven | 两端安装 JDK 8/17/21:项目要求 17 且 JAVA_HOME 为 8 时验证自动选择兼容版本;核对设置、Run、Maven 目标、测试使用同一 JDK。验证显式选择不变、没有兼容版本时回退并提示,以及无要求时的平台默认顺序。 | |
144144
| Java/Maven | **按项目配置 Maven 工具链、settings.xml 与本地仓库**<br><sub>java-maven-project-settings</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Services/Java/ProjectRuntimeService.swift`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src/features/settings/components/tabs/maven-settings-panel.tsx`、`windows/tauri/src/features/maven/stores/maven.store.ts`、`windows/tauri/src-tauri/src/maven.rs`、`windows/tauri/src-tauri/src/maven/settings_xml.rs`、`windows/tauri/src/features/maven/components/maven-detected-value.test.tsx`、`windows/tauri/src/features/settings/components/tabs/maven-settings-panel.test.tsx`、`windows/tauri/src/features/run/stores/run.store.ts`、`windows/tauri/src/features/run/stores/run-maven-context.test.ts`</sub> | Java / Maven | 在 macOS 项目设置和 Windows Maven 设置页分别切换两个项目,配置 Maven、JDK、settings.xml 与本地仓库,验证保存重开、自动检测展示及项目间隔离;Windows 还需验证写盘失败后可直接重试。 Windows 自定义 settings.xml 后,仓库留空应显示该文件中的目录;验证 XML 实体和 CDATA、继承的项目 JDK/Maven、切换项目丢弃未保存草稿;冷启动 Wrapper 不应触发下载,版本探测卡住时设置窗口保持响应并在超时后清理探测进程;Run 配置显式选择的 Maven/JDK 优先于项目设置,未设置的字段分别继承 Maven 设置;确认主任务与前置任务使用相同工具链,Maven 设置上下文缺失时仍保留显式覆盖,均未配置时交由平台自动发现。 | |
145145
| Java/Maven | **项目根、模块与源码集识别**<br><sub>java-maven-project</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Services/Java`、`shared/contracts/application-boundary.md`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src/features/maven`、`windows/tauri/src/features/workspace`</sub> | Java / Maven | 使用单模块、多模块和非标准源码目录 fixture 对比项目模型。 | |
146146
| Java/Maven | **Profile 与依赖树**<br><sub>java-maven-profiles-dependencies</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Views/Run`、`macos/Sources/Lithe/Services/Java`、`macos/Sources/LitheExecutionModule/Services/MavenService.swift`、`macos/Sources/Lithe/Platform/MacOS/Persistence/MacMavenDependencyOutputStore.swift`、`rust/lithe-core/src/project/maven_dependency_tree.rs`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src/features/maven`、`windows/tauri/src-tauri/src/maven.rs`、`rust/lithe-core/src/project/maven_dependency_tree.rs`</sub> | Java / Maven | 切换 Maven profile 并刷新依赖树,确认模块归属、顺序、版本冲突/重复/依赖管理标注和错误边界;在依赖节点超过 6,000 个的项目中确认依赖树完整加载。 Windows 在 Maven 面板展开「依赖项」应直接加载依赖树,失败时显示宿主返回的真实原因,而不是通用的“无法加载 Maven 依赖”(#970)。 | |

‎macos/Sources/Lithe/Application/Features/IdeCapabilitiesFeatureModel.swift‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,9 @@ final class IdeCapabilitiesFeatureModel: ObservableObject {
293293
case .found(let url, let source): return ["status": "resolved", "path": url.path, "source": choiceSource(source)]
294294
case .invalid(let path): return ["status": "invalid", "message": "Invalid toolchain: " + path]
295295
case .fallback(let path, let next): var result = choice(next); result["invalidOverride"] = path; return result
296+
case .warning(let url, let source, let message):
297+
return ["status": "resolved", "path": url.path, "source": choiceSource(source), "warning": message]
298+
case .unavailable(let message): return ["status": "invalid", "message": message]
296299
case .notFound: return ["status": "notFound"]
297300
case nil: return ["status": "loading"]
298301
}

‎macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ struct DefaultRuntimeToolDiscovery: RuntimeToolDiscovery {
6868
protocol RuntimeLocator: Sendable {
6969
func environment() -> [String: String]
7070
func discover() -> RuntimeDiscoveryResult
71+
/// Java-only probing for launch paths; never runs Maven or other tools.
72+
func discoverJavaRuntimes() -> [JavaRuntimeCandidate]
7173
func validJavaHome(path: String) -> URL?
74+
/// Returns the candidate reached by PATH, resolving platform symlinks.
75+
func javaHomeOnPath(in candidates: [JavaRuntimeCandidate]) -> URL?
7276
func javaRuntime(at homeURL: URL) -> JavaRuntimeCandidate?
7377
func isExecutable(at url: URL) -> Bool
7478
func systemMavenExecutable() -> URL?
@@ -81,7 +85,26 @@ protocol RuntimeLocator: Sendable {
8185
}
8286

8387
extension RuntimeLocator {
88+
func discoverJavaRuntimes() -> [JavaRuntimeCandidate] { discover().javaRuntimes }
89+
func javaHomeOnPath(in candidates: [JavaRuntimeCandidate]) -> URL? { nil }
8490
// Default implementation for test stubs and non-macOS locators that do
8591
// not ship a bundled JDK.
8692
func bundledJdkHome() -> URL? { nil }
8793
}
94+
95+
/// Shared selection policy; platform locators still own executable validation
96+
/// and version probing. Requirements are read through Core's existing document.
97+
protocol JavaRuntimeSelecting: Sendable {
98+
func selectJavaRuntime(at root: URL?, candidates: [AutomaticJavaCandidate], fallbackID: String?) -> Result<AutomaticJavaSelection, RustCoreBridge.CoreCallError>
99+
}
100+
101+
struct AutomaticJavaCandidate: Codable, Sendable {
102+
let id: String
103+
let version: String
104+
let priority: UInt32
105+
}
106+
107+
struct AutomaticJavaSelection: Decodable, Sendable {
108+
let id: String?
109+
let warning: String?
110+
}

‎macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2615,6 +2615,19 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {
26152615
)
26162616
}
26172617

2618+
func selectJavaRuntime(
2619+
at root: URL?, candidates: [AutomaticJavaCandidate], fallbackID: String?
2620+
) -> Result<AutomaticJavaSelection, CoreCallError> {
2621+
struct Payload: Encodable {
2622+
let root: String?
2623+
let candidates: [AutomaticJavaCandidate]
2624+
let fallbackId: String?
2625+
}
2626+
return executeResult(command: "runConfig.selectJava", payload: Payload(
2627+
root: root?.standardizedFileURL.path, candidates: candidates, fallbackId: fallbackID
2628+
))
2629+
}
2630+
26182631
func inspectRunConfiguration(
26192632
at rootURL: URL,
26202633
checkFingerprint: Bool? = nil,
@@ -4104,3 +4117,5 @@ private extension RustCoreBridge.WorkspaceNodePayload {
41044117
)
41054118
}
41064119
}
4120+
4121+
extension RustCoreBridge: JavaRuntimeSelecting {}

‎macos/Sources/Lithe/Models/Runtime/ProjectRuntimeModels.swift‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ enum RuntimeChoiceSource: Equatable, Sendable {
230230
/// Outcome of one runtime selection chain in `ProjectRuntimeService`.
231231
indirect enum RuntimeChoice: Equatable, Sendable {
232232
case found(URL, RuntimeChoiceSource)
233+
case warning(URL, RuntimeChoiceSource, String)
234+
case unavailable(String)
233235
/// A configured path that is not a usable runtime; a launch fails on it.
234236
case invalid(String)
235237
/// A configured path that is not usable, replaced by `to`.
@@ -239,15 +241,16 @@ indirect enum RuntimeChoice: Equatable, Sendable {
239241

240242
var url: URL? {
241243
switch self {
242-
case .found(let url, _): url
244+
case .found(let url, _), .warning(let url, _, _): url
243245
case .fallback(_, let choice): choice.url
244-
case .invalid, .notFound: nil
246+
case .invalid, .notFound, .unavailable: nil
245247
}
246248
}
247249

248250
/// The same choice presented as inherited from the project JDK.
249251
var inheritedAsProjectJDK: RuntimeChoice {
250252
if case .found(let url, _) = self { return .found(url, .projectJDK) }
253+
if case .warning(let url, _, let message) = self { return .warning(url, .projectJDK, message) }
251254
return self
252255
}
253256
}

‎macos/Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeDiscovery.swift‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@ import Foundation
22

33
enum MacRuntimeDiscovery {
44
static func discover(environment: [String: String]) -> RuntimeDiscoveryResult {
5-
let javaRuntimes = discoverJavaHomes(environment: environment)
6-
.compactMap(probeJavaHome)
7-
.sorted { lhs, rhs in
8-
lhs.version.localizedStandardCompare(rhs.version) == .orderedDescending
9-
}
5+
let javaRuntimes = discoverJavaRuntimes(environment: environment)
106
let mavenRuntimes = discoverMavenExecutables(environment: environment)
117
.compactMap(probeMaven)
128
.sorted { lhs, rhs in
@@ -47,6 +43,25 @@ enum MacRuntimeDiscovery {
4743
: nil
4844
}
4945

46+
static func discoverJavaRuntimes(environment: [String: String]) -> [JavaRuntimeCandidate] {
47+
discoverJavaHomes(environment: environment).compactMap(probeJavaHome).sorted { lhs, rhs in
48+
lhs.version.localizedStandardCompare(rhs.version) == .orderedDescending
49+
}
50+
}
51+
52+
static func javaHomeOnPath(environment: [String: String]) -> URL? {
53+
for directory in (environment["PATH"] ?? "").split(separator: ":") {
54+
let executable = URL(fileURLWithPath: String(directory), isDirectory: true).appendingPathComponent("java")
55+
guard FileManager.default.isExecutableFile(atPath: executable.path) else { continue }
56+
let resolved = executable.resolvingSymlinksInPath()
57+
// Apple's system launcher selects a JDK itself; /usr is not a JDK
58+
// home and must never become JAVA_HOME for child processes.
59+
guard resolved.path != "/usr/bin/java" else { return nil }
60+
return validJavaHome(resolved.deletingLastPathComponent().deletingLastPathComponent().path)
61+
}
62+
return nil
63+
}
64+
5065
private static func discoverJavaHomes(environment: [String: String]) -> [URL] {
5166
discoverJavaHomes(
5267
environment: environment,

0 commit comments

Comments
 (0)