Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ PHP Support 已经验证了独立插件包、固定 Intelephense 版本和插件

插件包不编译、不链接任何 Swift 代码,也不做 codesign,因此与架构和宿主构建无关。

唯一的例外是自带运行时的 Java Support:它的包里有每个架构各一份的 JDK,因此与架构有关,并且只随应用发行、由应用的代码签名覆盖。它的组装步骤在插件自己的目录里,见 [Java 语言服务器是一个声明式插件](2026-10-13-java-language-server-as-declarative-plugin.md)。
唯一的例外是自带运行时的 Java Support:它的包里有每个架构各一份的 JDK,因此与架构有关,当前仍随应用发行,内置副本由应用的代码签名覆盖;独立安装副本要求完整包的发布者签名。双架构发布输入与迁移已准备,官方索引完整上架后构建会省去内置资源;自动上架仍暂停,见[按来源校验与独立发布基础](2026-10-25-java-plugin-distribution-foundation.md)。它的组装步骤在插件自己的目录里,见 [Java 语言服务器是一个声明式插件](2026-10-13-java-language-server-as-declarative-plugin.md)。

下载和组装逻辑放在插件专属的准备脚本里(PHP 迁出前是 `prepare-php-language-server.sh`,已随迁移删除)。后续插件可以复用相同的构建阶段和校验结构,但不能把某个语言的路径、版本或下载地址硬编码到宿主应用中。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## 先说结论

Java 的语言服务器 JDT LS(Eclipse 的 Java 语言服务器,提供补全、诊断和跳转)不再是 Lithe.app 里写死的一部分,而是官方插件 Java Support(`dev.lithe.plugin.java-support`)。插件包拥有 JDT LS、Lombok、调试和测试扩展,以及它运行所用的 JDK;`plugin.json` 声明这些文件在包内的位置和启动方式。这个插件随应用发行、默认启用,用户可以卸载和恢复。
Java 的语言服务器 JDT LS(Eclipse 的 Java 语言服务器,提供补全、诊断和跳转)不再是 Lithe.app 里写死的一部分,而是官方插件 Java Support(`dev.lithe.plugin.java-support`)。插件包拥有 JDT LS、Lombok、调试和测试扩展,以及它运行所用的 JDK;`plugin.json` 声明这些文件在包内的位置和启动方式。过渡期随应用发行、默认启用,用户可以卸载和恢复;完整双架构官方下载上架后默认应用省去资源,迁移与安装引导由[后续分发决策](2026-10-25-java-plugin-distribution-foundation.md)规定。

宿主不再知道 JDT LS 或 JDK 放在哪里。它只做两件事:按插件的声明启动语言服务器;为声明了 `jdt` 宿主集成(由宿主实现、插件按名字启用的一段专用逻辑)的服务器启用 Java 的工程模型、`jdt://` 虚拟文档、测试和调试支持。

Expand Down Expand Up @@ -104,9 +104,9 @@ dev.lithe.plugin.java-support/

`LitheOfficialPluginVerifier` 的规则随之调整:Mach-O 原生代码只允许出现在清单 `packagedRuntime` 声明的顶层目录内;包内任何位置都不允许符号链接。

### 5. 随应用发行、默认启用、可卸载
### 5. 过渡期内置与独立安装

- **随应用发行**:Java 是主场景,首次启动必须离线可用,所以包在 `Lithe.app/Contents/Resources/OfficialPlugins/` 下,信任方式是 `sameTeamAsHost`(由应用自身的代码签名封住)。
- **过渡期随应用发行**:原设计为 Java 保持首次启动离线可用,过渡包在 `Lithe.app/Contents/Resources/OfficialPlugins/` 下,清单主签名要求为 `publisherPackage`,内置副本通过 `bundledSignatureRequirement: sameTeamAsHost` 依靠应用自身的代码签名;只有真实位于宿主 bundle 内且来源为 bundled 的副本能用这个例外。按来源校验与独立发布基础见[后续决策](2026-10-25-java-plugin-distribution-foundation.md)。
- **默认启用**:随应用发行的包替代的是内置功能,它的模块默认开启;下载安装的插件,模块清单默认值仍是关闭,但用户在确认窗口里确认的首次安装(插件市场、社区源、磁盘包)会把它的模块记为启用,重启后直接生效,与 IntelliJ IDEA 安装即启用一致;更新和重装不改用户已有的启用选择。否则构建插件装好后 Build 工具窗口不出现,用户会以为界面没有注入。
- **卸载**:app bundle 运行时只读,包本身删不掉。卸载记录在用户目录的 `<app-support>/Lithe/Plugins/.removed-bundled-plugins.json`,宿主重启后不再注册这个插件,并在那次启动时删除它的语言服务器状态(卸载后到重启前,语言服务器仍在运行并写入该目录,所以不在点击时删除;重启前恢复则不会丢失索引)。插件管理里仍然列出它并提供“恢复”。
- **卸载时 JDT 状态的清理**:JDT LS 的工作区索引(`<cache>/Lithe/language-servers/jdtls/`)和 Equinox 配置区(`jdtls-configuration/`)在宿主缓存里,不在插件自己的 `PluginState/<plugin-id>/` 下,插件包管理器删不到。所以由 `jdt` 集成自己清理:启动时 `pluginsDidLoad` 拿到扫描结果,如果没有任何“未卸载”的已安装插件声明 `jdt` 集成,就在后台删除这两个目录(不碰同目录下其他语言服务器的缓存)。扫描本身失败(拿不到已安装列表)、或插件已安装但启动时被拒绝,都不算卸载,状态保留。这样宿主既不用写死 Java Support 的插件 ID,也不需要把缓存搬进插件状态目录。
Expand Down Expand Up @@ -170,7 +170,7 @@ dev.lithe.plugin.java-support/
- Rust 引擎里仍按提供方 ID `java` 启用 JDT 适配;Java 的运行、测试和调试仍是宿主内置实现,尚未成为扩展点,由插件通过 `hostIntegration.capabilities` 声明后启用。
- Windows 仍把 JDT LS 和 JDK 放在安装目录里,没有接入这份契约。

尚未完成、需要单独提案的部分:把 `jdt://` 虚拟文档等协议扩展改成清单里的声明式特性;Debug、Run/Test 扩展点;Java Support 改为从市场下载;Windows 接入。
尚未完成、需要继续推进的部分:把 `jdt://` 虚拟文档等协议扩展改成清单里的声明式特性;Debug、Run/Test 扩展点;Java Support 的正式市场下载与迁移;Windows 接入。独立分发已具备来源签名校验和双架构发布基础,但自动发布暂停,计划见[独立分发提案](../../proposed/architecture/2026-10-24-java-support-independent-distribution.md)。

## 验证

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Agent 笔记:Java 插件按来源校验与独立发布基础

状态:已实现

## 先说结论

Java Support 的同一份清单现在区分两种来源:随应用发行的副本可以依靠宿主签名,用户下载安装的副本必须有 Lithe 发布者签名。两种来源使用同一插件 ID,经过校验的用户安装版本替代内置版本,不重复登记 Java 语言服务。官方发布工具能记录 ARM64、Intel 两个归档及各自哈希,并从发布标签读取对应 API 和能力声明;迁移保留启用选择并提供按需安装入口;主包只在完整双架构官方下载进入索引后移除资源。自动上架仍暂停,真实发布包按[独立分发提案](../../proposed/architecture/2026-10-24-java-support-independent-distribution.md)继续验收。

## 问题

Java 已经是声明式插件,但 `sameTeamAsHost` 原来只接受应用签名封装内的文件。直接把目录放到用户插件目录会被拒绝;直接把策略改成 `publisherPackage` 又会要求普通应用构建持有插件发布私钥。

旧发布脚本把索引 API 写死为 1,且每版只有一个 ZIP。Java 使用 API 3,又携带不同架构的 JDK,必须保留真实 `requires`(宿主必须支持的能力版本)和两个归档。历史版本重试上架时,还不能拿当前清单描述旧归档。

## 决策

### 同一清单,来源决定是否使用宿主签名

新增通用能力 `packageTrust: 1`,只在清单声明 `vendor.bundledSignatureRequirement` 时使用。Rust Core 只允许官方命名空间、API 3、主签名为 `publisherPackage`,且内置策略只能是 `sameTeamAsHost`。

macOS 官方目录固定主签名和内置策略。包管理器先校验清单与官方目录一致,再根据解析后的实际路径和安装来源验证:只有来源为 `bundled` 且文件确实位于宿主 app 内的副本可以依靠宿主签名。外部目录伪造来源无效;安装记录、回滚版本和磁盘导入同样不能借用该例外。任何来源只要带了签名文档,就必须验证它,不能因副本在 app 内而忽略坏签名。

旧宿主没有 `packageTrust`,会在市场过滤和清单校验时拒绝这一版。Windows 本轮不声明该能力,后续实现相同来源边界之后才允许安装,不将 Core 能校验等同于宿主能执行。

正确做法:下载新版后校验完整文件树的发布者签名,再把完整版本放到平台应用数据目录;重启读取同一 ID 时只选择一个生效来源。

不要这样做:外部目录沿用 `sameTeamAsHost`,或声称 `bundled` 就省略签名。也不能把官方 ID 的策略改成同时接受任意一种签名。

### 构建显式选择内置副本

应用打包继续使用 `--bundled-only`。只有显式走这条路径且清单声明内置策略时,构建脚本才省略发布者签名,最终由应用签名封住资源。普通单插件 release 构建要求发布者私钥;debug 的 `--development-unsigned` 只用于本地开发,正式宿主拒绝它。

JDT LS、JDK、Lombok、Debug 和 Test 扩展保持当前锁定版本。资源先组装,原生签名等会改字节的步骤完成后才生成发布者签名,最终归档解压后再验证。运行时启动继续复用现有 `jdt` 集成;安装检查新增对运行时启动的计划校验,拒绝缺失目标架构的 JDK、包内入口和相关固定资源,不运行 JDK、不创建验证状态目录。

### 安装切换保留用户选择

内置清单默认启用,下载清单默认禁用。安装成功后,包管理器把原内置模块的默认值保存为显式选择,再在重启时选择下载版;显式禁用不变,安装失败不修改选择。迁移只读取旧包已经拥有的模块,新模块保持清单默认值,避免把新模块或其他所有者的状态误当作旧选择。同一 ID 的插件设置继续使用原键,项目 JDK、运行配置和调试的 JDK 选择链不变,不复制或改写用户配置。

旧 `.removed-bundled-plugins.json`(用户卸载内置包的记录)持续抑制内置来源。用户明确重新安装后,该记录不能清理已安装版本的状态;删除已安装更新时也记录内置移除,避免重启又启用被遮住的副本。跳过过渡宿主直接升级时,无包就保持未安装,显式禁用仍保留;只有用户确认安装才创建新版本,不自动下载。

打开 Java 文件或含 Java、Maven、Gradle 文件的项目时,无插件则显示每窗口会话一次的安装提示,支持前往市场和“从磁盘安装”的离线路径。新建项目页在 Java 插件缺失时提供模板安装入口,已有但禁用的插件继续走启用流程。离线或取消不重置设置,下一次会话可以重试。

### 精简应用以已发布索引为前提

发布登记中的 `bundledUntilPublishedVersion: 1.1.0` 表示安全退出内置分发的最低插件版本。构建读取本工作树的官方索引:至少一个达到版本门槛的版本,其仓库、API、能力要求与当前插件一致,两个架构的 URL、平台与 SHA-256 齐全且唯一,才不再组装内置 Java,也不预下载 JDT/JDK。索引缺项仍带过渡副本,索引读写格式损坏直接中止构建,不能当作“无需内置”。后续插件源码升版时,兼容的旧下载仍可满足门槛,不要求下载版本等于当前源码版本。

打包用同一份索引作判断并嵌入应用,运行时不联网决定是否删资源。`LITHE_OFFICIAL_PLUGIN_INDEX` 仅覆盖构建输入,传给插件构建的 `--marketplace-index`,默认读取仓库索引;测试用临时虚构下载项证明精简路径,不能将 fixture 发布为真实索引。预览的 `--bundled-only` 使用同一默认策略。公开下载前默认 app 仍内置 Java;公开包和索引独立验收合并后,下一次应用构建自然精简。

正确做法:先发布并验证签名双架构包,再合并真实索引;应用构建从该索引得出不再内置。不要这样做:因为发布配置写好了就先删资源,或运行时下载到 app bundle 中补回文件。

### 发布按标签固定全部输入

`official-plugins.json` 登记 Java 两种架构的归档和上游构建输入,`releaseEnabled: false` 保持暂停自动发布。先有包含新能力的宿主版本,再通过独立改动开放上架;本次不生成线上索引或发布标签。

发布计划携带清单的真实 API 和 `requires`,组装全部架构后再创建同一个 Release。索引同步从对应 Git tag 读取当时的发布配置和清单,下载所有归档并计算 SHA-256 后一次写入该版本;任一归档缺失或哈希无效都不写部分版本。已发布插件的目录或登记的上游输入发生变化,需要提高插件版本。

### 资源所有权

- `dist/packages/<arch>/` 是发布 job 的组装输出,`dist/verify/<arch>/` 是归档验证副本,ZIP 和校验文件属于同一次发布。job 结束回收,不跨工作树复制。
- `.artifacts/java-plugin-distribution/` 是本地验证输出,没有可转移的版本与工具链身份,验证者负责删除不用的临时副本;资源复用脚本明确拒绝。
- JDT/JDK 下载缓存仍只能经原有资源校验路由复用。每个工作树从固定上游输入重新组装插件,不共享已签名包或用户安装目录。
- 用户安装的插件版本是只读输入;索引、Equinox 配置、日志和锁仍由平台缓存和状态适配器持有。运行时不改 app bundle、插件安装资源或 Windows 发行目录,保持代码签名和 Sparkle 增量更新源基线。

## 考虑过的备选方案

- 对 Java 官方 ID 全局放宽签名:实现简单,但外部文件会获得内置包才有的信任,拒绝。
- 应用构建也要求插件发布私钥:统一使用包签名,但普通开发和应用 PR 构建不应该依赖发布私钥,拒绝。
- 生成两份不同来源清单:减少清单字段,但会造成同一插件版本有不同清单和能力要求,使用同一声明按来源验证。
- 为旧宿主新增最低应用版本字段:当前能力协商已能拒绝未知语义,不额外引入另一套版本比较。
- 索引重试使用最新清单:容易实现,但旧归档会被描述为具有后来新增的能力,改为从不可变标签读取。

## 后果

收益是应用打包仍能独立完成,外部安装具有完整签名检查,Java 资源可以按架构独立发布。代价是首次下载版要求新宿主能力,签名策略不能只看一个枚举,还要核对官方目录、来源和真实路径。

代码覆盖可信安装、启用/卸载选择迁移、缺包引导和以发布索引为门槛的主包精简。当前索引未上架,所以主包仍含 Java;正式签名发布、真实市场下载及完整 Java 产品工作流尚未验收,提案保持 `proposed`,不能把测试 fixture 当作上线证据。

## 验证

- `./scripts/verify-rust-core-comments.sh`、`./scripts/verify-shared-contracts.sh`:契约、注释和共享输入一致。
- Rust 的 `declarative_plugins` 与 `marketplace_plugins`:来源策略必须声明能力、非法组合拒绝、旧宿主在下载前拒绝。
- `PluginBundledPublisherTrustTests`:真实 Core、生产包管理器及注入测试公钥验证签名安装、单一来源、包篡改、伪造来源和缺失运行时,比较安装资源内容。
- `./scripts/verify-macos-package.sh --slim`:以临时完整索引构建精简 universal app,拒绝 JDT/JDK 下载输入,检查资源缺失、签名和 DMG;默认模式检查保留过渡包。
- `node --test scripts/test-official-plugin-distribution.mjs`:完整与部分上架、来源/能力不匹配和后续版本门槛。
- `node --test scripts/official-plugin-releases.test.mjs`:双架构索引、部分归档拒绝、历史标签元数据以及上游输入变更约束。
- `node scripts/test-reuse-worktree-resources.mjs`、`./scripts/verify-runtime-bundle-immutability.sh`:生成资源拒绝复用、发行目录只读边界。
- `./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh` 和 macOS 计时入口:相关测试必须有单项时长与 HTML/JUnit 结果。
- `.github/workflows/release-official-plugins.yml` 用 actionlint 校验;本地组装两种架构资源,验证 JDK 原生签名和发布归档的完整性。

上述验证不能替代正式发布者私钥签名、真实市场下载、Intel 真机及全部 Java 产品工作流。上线前继续按独立分发提案的退出条件执行,不能靠测试公钥通过就开放发布。

## 适用范围

- `Plugins/mac/Official/JavaSupport/`
- `Plugins/marketplace/official-plugins.json`
- `shared/contracts/plugins/v1/`
- `rust/lithe-core/src/plugins/`
- `macos/Sources/LitheModuleAPI/`
- `macos/Sources/LitheCoreContracts/Plugins/DeclarativePluginContracts.swift`
- `macos/Sources/Lithe/Platform/MacOS/Plugins/MacPluginPackageStore.swift`
- `macos/Tests/LitheTests/PluginBundledPublisherTrustTests.swift`
- `macos/Tests/LitheOfficialPluginVerifier/OfficialPluginVerifier.swift`
- `scripts/build-official-plugins.sh`
- `scripts/official-plugin-releases.mjs`
- `.github/workflows/release-official-plugins.yml`
- `scripts/worktree-resources.json`
- `scripts/reuse-worktree-resources.mjs`
- `docs/ci-builds.md`
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ Windows 插件包使用自己的原生运行时、入口和存储适配器,复

### 八、分期与退出条件

当前代码的实现边界见[Java 插件按来源校验与独立发布基础](../../implemented/architecture/2026-10-25-java-plugin-distribution-foundation.md)。该实现登记双架构发布输入但保留 `releaseEnabled: false`,尚未向用户公开下载。迁移选择、缺包提示及以完整索引为门槛的精简构建已在同一实现 PR 中补齐,过渡宿主发行、真实发布包和完整产品迁移仍按本提案验收。


| 阶段 | 交付内容 | 进入下一阶段前必须证明 |
| --- | --- | --- |
| 0:确认基线 | 盘点两种 macOS 架构的实际资源、压缩体积、JDK 消费者和离线行为;记录上游版本与许可证 | 明确本轮只改变分发;列出需保留的用户工作流和资源消费者 |
Expand All @@ -130,7 +133,7 @@ Windows 插件包使用自己的原生运行时、入口和存储适配器,复
| 后续:独立仓库 | 可选迁移插件源码、专属测试和发布来源,固定宿主测试资源输入 | 无第二份可编辑源码;用户无需重新安装或重置设置 |
| 后续:Windows 对齐 | 补齐必需宿主能力、平台包和真实 Windows 回归 | 同一版本能力声明可执行,Windows 安装目录哈希不变 |

首期按三个实现 PR 推进:可信下载安装、老用户切换、移除内置资源,阶段 0 的基线记录随第一项交付。完成阶段 3 即可验收 macOS 独立分发,不等待独立仓库或 Windows;Windows 另行验收前不声明双端完成。每项实现从最新 `preview` 创建独立分支,本地验证后先提交 Draft PR;不通过反复推送试探完整 CI。
按用户要求,可信下载安装、老用户切换与受发布索引保护的精简构建在同一个实现 PR 中交付,阶段 0 的基线记录一并保留。完成阶段 3 即可验收 macOS 独立分发,不等待独立仓库或 Windows;Windows 另行验收前不声明双端完成。实现分支从最新 `preview` 创建,本地验证后先提交 Draft PR;不通过反复推送试探完整 CI。

### 正确做法与反例

Expand Down
Loading
Loading