Repository navigation
feat(plugins): 声明式版本管理器 toolchainSources,自动发现 mise 等安装的运行时并按项目版本选择 - #1581
FxRayHughes wants to merge 1 commit into
Conversation
A plugin with pluginAPIVersion 3 and requires.toolchainSources 1 declares a version manager such as mise: where it installs runtimes, which executables each tool has, and which project and global files name a project's versions. Rust Core lists the install directories and reads the version files (it never runs the version manager) and returns the installed runtimes, the requested version and the selected installation. The macOS host adds those runtimes to JDK, Maven and tool discovery and prefers the version the project requests over the inherited JAVA_HOME and PATH; an explicit choice in Lithe still wins. A toolchain-only plugin applies without a restart.
技术方案评审:
|
| 贡献 | 回答什么 | 形式 | 谁提供 |
|---|---|---|---|
| 工具定义 | 工具是什么:怎么识别、探测版本、导出环境、启动 | 清单里的静态声明 | 任何插件 |
| 提供方 | 装了什么、项目要什么、全局要什么 | 插件通过 API 返回事实 | 版本管理器插件 |
| 使用方 | 谁需要这个工具 | 已有(通过 tool ID 引用) |
语言服务器、构建系统、运行配置 |
1. 工具定义(示意)
"toolDefinitions": [
{ "id": "java", "displayName": "JDK",
"executables": ["bin/java"], "commands": ["java"],
"version": { "arguments": ["-version"], "pattern": "(?:1\\.)?(\\d+(?:\\.\\d+)*)" },
"exports": [{"name": "JAVA_HOME", "value": "${home}"},
{"name": "LITHE_JAVA_PATH", "value": "${executable}"}],
"pathPrepend": ["${home}/bin"],
"launch": ["${executable}", "${runtimeArguments}", "-jar", "${entry}", "${arguments}"] },
{ "id": "maven", "displayName": "Maven",
"executables": ["bin/mvn"], "commands": ["mvn"],
"projectExecutables": ["mvnw"],
"version": { "arguments": ["-v"] },
"pathPrepend": ["${home}/bin"] }
]这张“工具 ID 到宿主用途”的映射表就是适配层里本来要写死的东西,改成插件贡献。它取代的是:java 当 JDK home 的特判、maven → mvn 的文件名巧合、契约里写死的版本探测参数和 LITHE_<TOOL>_PATH 导出规则、launch.kind: runtime 里 tool 只能是 java 或 node 的限制,以及选择链里专门给 mvnw、gradlew 写的分支。
- ID 归属。 插件自己的工具用
插件id:工具;不带前缀的短名(java、node、go、maven)是保留名,只有宿主内置或官方签名插件能定义。同一 ID 被重复定义时拒绝后来者,不允许覆盖。 - 宿主内置的那几个用同一种格式的数据文件表达,放在
shared/下,各平台随应用打包,作为第一个贡献者注册。适配层里不出现任何工具名,以后各插件可以逐个接管自己的定义。 - 安全校验由 Core 统一做,与具体工具无关。 环境变量值只能是
${home}、${executable}模板;禁止PATH、DYLD_*、LD_*;pathPrepend只能在${home}之内;探测参数必须是-x/--xx形状。安装审查里列出每个定义会运行的程序和参数。
2. 提供方 API
清单只声明“我能回答哪些工具”,不描述目录结构和文件格式:
"toolchainProviders": [{ "id": "mise", "displayName": "mise",
"tools": ["java", "node", "maven", "pnpm", "go"] }],
"permissions": ["toolchains.provide"]宿主向插件发 toolchains/query({operationID, workspace, tools},workspace 为空表示只问全局),插件返回:
{ "installations": [
{"tool": "java", "version": "21.0.2", "home": "/…/installs/java/21.0.2"} ],
"statements": [
{"tool": "java", "scope": "project", "source": "/proj/mise.toml",
"request": "21", "installation": "/…/java/21.0.2"},
{"tool": "java", "scope": "global", "source": "~/.config/mise/config.toml",
"request": "17", "installation": "/…/java/17.0.9"},
{"tool": "node", "scope": "project", "source": "/proj/.tool-versions",
"request": "lts", "installation": null} ],
"watch": ["/proj/mise.toml", "/proj/.tool-versions", "…/installs/java"] }- 插件只能回答自己在
tools里声明过的、且有已启用插件定义的 ID,其余丢弃。这样“任意同名文件被注入”的口子就堵上了。 - 版本语义(哪个文件优先、
lts对应哪个目录)归插件,宿主不解释request。 installation: null表示项目要求了但没装。watch沿用 PR 里“缓存到输入变化”的思路:宿主只做stat指纹比较,没变就不再调用插件。- 沿用现有的
operationID、取消、超时和过期结果规则。 - 返回的路径由 Core 统一校验:绝对路径、存在、可执行、不落在工作区内(保留 PR 现有的这条安全规则)、数量有上限。
.tool-versions、mise.toml这类通用解析函数放进 lithe-plugin-docs 的 Node/Java SDK,不放进宿主。
3. 统一的选择规则
所有工具共用一条规则,按层从上到下取第一个有值的:
| 层 | 内容 | 范围 |
|---|---|---|
| 1 固定 | 用户在 Lithe 里为这个项目选的 | 项目 |
| 2 项目要求 | 提供方上报的 scope: project,以及 mvnw、gradlew 等项目自带的包装脚本 |
项目 |
| 3 全局默认 | Lithe 里的全局默认 → 提供方上报的 scope: global → JAVA_HOME/PATH |
全局 |
| 4 自动检测 | 满足最低版本的已安装最新版(现有的 Core 规则) | — |
- 没装任何插件时,第 2 层为空,行为与现在完全一致。
- Lithe 里固定的值失效:报错,不回落(与调试适配器那篇 Note 的做法一致)。
- 项目要求了但没装:回落到下一层继续工作,但状态标成“未满足”,界面明确提示。
- 界面上每个工具一行,显示“JDK 21 · 来自项目要求(mise.toml)”,展开后列出每层的值,被盖住的划掉。
对这个 PR 的建议
可以保留: 工作区内路径拒绝、按输入缓存、只读原则、i18n 词条、功能矩阵条目、RuntimeChoiceSource 的“来自 %@”展示。
需要重做: Core 里的 toolchain_sources.rs(管理器专属解析与选择语义)、清单 schema 里的目录与格式描述、ProjectRuntimeService 里三条各自的链。
建议拆分:
- 契约(工具定义、提供方、校验)加 Agent Note,先定契约再写代码;
- 适配层的查询客户端(缓存、取消、过期);
- 统一选择规则加运行时设置界面;
- mise 插件加 SDK 帮手,再写一个 SDKMAN 插件当验收,并把
MacRuntimeDiscovery里写死的 SDKMAN 位置迁出去。
契约在 shared/contracts/plugins/v1/,合并后要在 lithe-plugin-docs 运行 node scripts/contract.mjs sync 同步。
待讨论
- API 方向。 上面是“宿主请求、插件返回事实”,不是“插件主动推送”。推送需要插件进程为每个打开的项目常驻;请求式有明确的超时和取消。需要时可以在同一协议上加
toolchains/changed通知。 - 规则细节。 “固定 > 项目要求 > 全局默认”,以及“要求没装就回落并警告”,是建议值,欢迎提出不同意见。
- 保留名。 只允许官方签名的插件或宿主内置定义不带前缀的短名;若允许社区插件定义,冲突处理会复杂很多。
- 自举和免重启。
- 提供运行时的插件,它自己的后端不能依赖它要发现的运行时,否则循环依赖。后端必须自带运行时或是单文件可执行程序(契约已有
packagedRuntime)。 - PR 的
canActivateWithoutRestart只放行“无后端”的插件,这个条件需要放宽。 - 这两点我们还没有原型验证过。
- 提供运行时的插件,它自己的后端不能依赖它要发现的运行时,否则循环依赖。后端必须自带运行时或是单文件可执行程序(契约已有
- Core 的一次性改动。 加定义的校验,以及把
checkRuntimeVersion里 Java 8 的1.x特例改成定义里的正则。之后新增工具或管理器都不再改 Core。
|
这个具体的先后顺序啥的我记得构建工具里面我之前就提过一个方案,按照优先级去维护一个顺序,你可以去看看,具体细节我记不清楚了哎 |
|
感谢这么细的评审。结论先说:同意按「工具定义 / 提供方 / 使用方」重做,本 PR 不再往下推。 我对照代码核对了几处问题,都成立:
你说的「按优先级维护顺序」,我找到的是你在 #1420 的评审(#1420 (comment)):
如果是这条,我打算把新方案的选择规则直接建在它上面,不再写第二套分层逻辑。下面是调整方案,请看方向对不对。 1. 选择规则:写成每个工具定义的默认候选数组四层规则不再写成
示意(Maven;Java 结构相同,把 { "id": "maven", "displayName": "Maven", "commands": ["mvn"],
"version": { "arguments": ["-v"] },
"executables": [
{ "id": "pinned", "kind": "setting", "settingID": "executable", "layer": "project" },
{ "id": "wrapper", "kind": "projectFile", "path": "mvnw" },
{ "id": "projectRequest", "kind": "toolchain", "scope": "project" },
{ "id": "default", "kind": "setting", "settingID": "executable", "layer": "global" },
{ "id": "globalRequest", "kind": "toolchain", "scope": "global" },
{ "id": "home", "kind": "environment", "variable": "MAVEN_HOME", "path": "bin/mvn" },
{ "id": "path", "kind": "command", "command": "mvn" },
{ "id": "detected", "kind": "toolchain", "scope": "installed" }
] }各层在 resolver 里的语义:
这样三条链变成一条。Provider、模型加载和实际执行用同一个结果,#1420 已经保证了这一点。没装版本管理器插件时,所有 2. 提供方:mise 的语义交还给 mise同意「宿主请求,插件返回事实」。mise 插件改成带后端,后端直接调用 mise 自己的机器可读输出,任何一方都不再重写 mise 的规则:
本机验证(mise 2026.9.8):用
自举:后端做成单文件原生程序(Rust,按 darwin-arm64 / x64 分包,Store 已支持按架构下载),不依赖它要发现的 node 或 java。mise 本身的位置用 #1420 的候选声明:设置 → SDKMAN 适合做第二个验收插件: 3. 对待讨论各点的意见
4. 拆分按你的建议,先契约后代码,拆成 4 个 PR:
你列的可以保留的部分(工作区内路径拒绝、按输入缓存、只读原则、i18n 词条、功能矩阵、「来自 %@」展示)会分别带进第 2、3 个 PR。第 1 个提案 PR 提交后关闭本 PR,分支保留作参考;配套的 Store 收录 PR(Lithe-IDEA/Lithe-store#32)同时关闭,等新契约落地后重新提交。 如果方向没问题,我先提第 1 个提案 PR。 |
|
剩下的没啥问题了,你大胆去做吧 |

关联 Issue
Fixes #1514
相关:#778(SDKMAN 的 JDK 检测)、#815(按项目要求的 Java 版本选 JDK)、#1455(Dock 启动时 PATH 里没有 node)。
问题
从 Dock/Finder 启动的 Lithe 不继承 shell 的
PATH/JAVA_HOME,mise(以及 asdf 等)的 shims 不可见;宿主的固定扫描位置也不包含~/.local/share/mise/installs。结果是:mise.toml/.tool-versions要求的版本(JDK、node、pnpm 等)不会被使用。把 mise 的目录硬编码进宿主只能解决一个版本管理器,还要宿主自己理解每种版本文件。本 PR 改为新增一个通用的声明式插件能力
toolchainSources:插件声明“版本管理器把运行时装在哪里、每个工具有哪些可执行文件、哪些文件写着项目要的版本”,共享 Core 只读地解析,宿主接入运行时选择。mise 支持作为社区插件单独发布(见下方“配套”)。改动
插件契约(
shared/contracts/plugins/v1/)toolchainSources(pluginAPIVersion: 3,requires.toolchainSources: 1,权限toolchains.read,不需要激活事件)。每个来源声明:installRoot:按顺序取第一个可用位置(环境变量、环境变量 + 子路径、主目录相对路径);tools:Lithe 的工具名(java的版本目录是 JDK home,其他工具按可执行文件名接入)、子目录、版本文件里的名字(如java、core:java)、home、可执行文件(单个路径分量内允许*);versionFiles(项目内,同一目录按声明顺序)与globalVersionFiles,格式为miseToml或toolVersions。toolchain-sources.plugin.json、invalid-manifests.json反例、rust-core-api.md同步更新。capabilities::check_used要求requires精确列出toolchainSources,多列或漏列都以plugin.manifest.invalid拒绝。Rust Core(
rust/lithe-core/src/plugins/toolchain_sources.rs)..和**、环境变量名格式、显示名长度等。plugins.declarative.toolchains({manifest, environment, homeDirectory, root?}):21 -> 21.0.2)和点文件,纯数字版本按新到旧排序;21匹配21.0.2,不匹配210)、latest、prefix:、mise 建立的别名链接(lts)、path:(绝对、~/或相对声明文件)。system、ref:等需要版本管理器运行时解析的写法不选择任何版本;path:规范化后落在项目内部时不选择任何版本——宿主会不经询问启动被选中的运行时,仓库不能借此指定自己携带的程序;inputs(读过和探测过的路径),供宿主判断缓存是否失效。macOS 宿主
PluginToolchainSources(端口)+MacPluginToolchainSources(实现):按项目缓存 Core 结果,键包含插件列表(id + 规范化清单)和环境;用inputs的 stat 指纹判断版本文件或安装目录是否变化,未变化时不重复调用 Core。MacRuntimeDiscovery/MacRuntimeLocator):插件提供的 JDK home 加在 SDKMAN 之后,Maven 加在/usr/bin/mvn之后。ProjectRuntimeService):JAVA_HOME→ 自动检测;mvnw→ 版本文件要求的 mvn → 系统 Maven → 插件提供的其他 mvn;LITHE_*_PATH等显式前缀 → 版本文件要求的可执行文件 → 其他已发现的候选 →PATH→ 插件提供的其他版本。.project来源仍不会被语言服务器自动选用。performRuntimeRefresh),主线程上的选择链只做缓存校验。RuntimeChoiceSource.toolchainSource(name)/RuntimeToolSource.toolchainSource,界面文案 “来自 %@” / “Version manager”。<id>.toolchain-sources,只承载启用状态;安全模式下不读取。canActivateWithoutRestart放行只含debugAdapters和/或toolchainSources模块、无后端、无其他贡献的插件;实时注册表扫描后AppModel调用pluginToolchainsDidChange()重新发现运行时。同时声明语言或其他贡献的插件仍等待重启。toolchains.read→“查找版本管理器安装的运行时,以及项目要求的版本”。文档、i18n、功能矩阵
.agents/notes/implemented/feature/2026-10-29-plugin-toolchain-sources.md;更新2026-10-26-plugin-live-activation.md的谓词和边界。shared/i18n新增 2 个词条(中英),已重新生成Localizable.strings。plugin-toolchain-sources:macOSimplemented / pending,Windowsmissing / not-applicable(Windows 宿主未接入;Core 校验通过不代表 Windows 可用)。配套
plugin.json,不含任何程序)。toolchainSources的宿主会在市场中隐藏该插件。node scripts/contract.mjs sync --revision <合并后的 preview commit>同步契约。验证
cargo test -p lithe-core --lib(GIT_CONFIG_GLOBAL指向init.defaultBranch=master)tests::toolchain_sources7 项、plugins::toolchain_sources单元测试 3 项,declarative_plugins覆盖新 fixture 与invalid-manifests.json反例)scripts/rust-core/verify-rust-core-crates.shinit.defaultBranch=main时 8 个既有git_line_history_*用例因checkout master失败,与本 PR 无关)test-git-execution.py、MacGitHubGitOperationsTests等 4 个 bridge suitecargo fmt --check -p lithe-core、scripts/rust-core/verify-rust-core-comments.shPluginToolchainSources、MacRuntimeDiscovery、ProjectRuntimeSettings、PluginLiveActivation、DeclarativePluginContract、ProjectRuntimeLoading、BuildToolProviderResolution、JavaLanguageServerRuntime、RunConfigurationIntegration、MavenRuntime、ExecutionModule、MacBuildProviderSessions、BuildSystemService等AgentEditRestorerTests、AgentSessionSelector*、LitheTreeListTests等在未修改的origin/preview上同样失败scripts/architecture/verify-shared-contracts.sh、verify-service-boundaries.shnode scripts/i18n/generate-i18n.mjs --check、verify-app-language-localization.mjsscripts/platform-feature-matrix/verify-platform-feature-matrix.sh、verify-platform-feature-matrix-change.sh origin/preview HEADscripts/macos/verify/verify-runtime-bundle-immutability.sh./scripts/agent-notes/verify-agent-notes.shplugin.json)mise current/mise where比较(全局配置、父目录mise.toml、子目录.tool-versions、mise.local.toml、temurin-21、lts、前缀3/3.13)未做:打包后的 app 实机 UI 验收(功能矩阵保持
pending)。已知限制
miseToml和toolVersions:不支持 mise 的conf.d/*.toml、环境专属配置(mise.<env>.toml、MISE_ENV)和[tools]之外的模板/任务写法。system、ref:等需要版本管理器运行时解析的写法不选择任何版本,回落到 Lithe 原有的选择。