Skip to content
Closed
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 @@ -25,7 +25,7 @@ PHP 支持由用户选择安装和启用,主程序不携带 PHP 插件包、No
### 能力与生命周期

- PHP 的 LSP 使用现有 Rust Core 会话,以 `intelephense --stdio` 启动。符号、类型和诊断仍由上游服务拥有;主机不实现第二套 PHP 语义分析。
- macOS 插件管理页是 PHP 包和 Intelephense 的生命周期唯一入口:构建阶段按 `language-server.json` 下载并校验 npm tarball,把 launcher 和运行包放入插件 bundle;下载器再下载完整插件 zip,`MacPluginPackageStore` 同时验证插件 manifest、签名和语言服务器 launcher。安装、重装、回滚和卸载都针对同一个插件版本目录执行,因此不会留下脱离插件的 LSP。LSP 控制中心只显示当前项目的 PHP 语言服务器开关和运行状态,发现未安装、未启用或待重启时引导回插件管理页,不提供包操作按钮。
- macOS 插件管理页是 PHP 包和 Intelephense 的生命周期唯一入口:构建阶段按 `language-server.json` 下载并校验 npm tarball,把 launcher 和运行包放入插件 bundle;下载器再下载完整插件 zip,`MacPluginPackageStore` 先验证原生 bundle,再用内置 publisher Ed25519 公钥验证完整包清单、插件 ID、版本和文件 SHA-256,最后验证语言服务器 launcher。安装、重装、回滚和卸载都针对同一个插件版本目录执行,因此不会留下脱离插件的 LSP。LSP 控制中心只显示当前项目的 PHP 语言服务器开关和运行状态,发现未安装、未启用或待重启时引导回插件管理页,不提供包操作按钮。
- macOS 运行和测试使用插件模块持有的执行 session。相对文件名不做 trim,以 `-` 开头时加 `./`,避免把文件名当作命令选项。
- Windows 只有已安装且启用 PHP 扩展时才读取 Composer/PHPUnit 清单、展示运行入口;执行前再次检查开关。Composer 的字符串和字符串数组均交给 `composer run -- <name>` 执行,不在主机模拟脚本语义。
- Windows PHP 运行复用 Run 的输出面板和 native 进程启动能力,通用宿主服务在首个 await 之前按插件 ID 和工作区登记会话。禁用或关闭工作区时等待在途启动,再停止其拥有的 execution ID;自然结束释放所有权。不得通过普通终端事件绕过这个流程。
Expand Down Expand Up @@ -57,7 +57,7 @@ PHP 支持由用户选择安装和启用,主程序不携带 PHP 插件包、No

## 后果

不使用 PHP 的用户不承担语言服务器下载、索引和进程成本。代价是首次使用需要显式安装插件和 Node.js;macOS 在线包必须使用与宿主一致的签名,未配置 Developer ID 的调试或预览构建仍只能使用本地导入进行测试。Windows 目前提供 Composer 脚本及整套 PHPUnit,未声明支持 macOS 已有的单方法测试发现。Windows 本地包暂不提供在线分发、自动更新或签名身份验证,替换版本需先卸载再导入;包格式仅用于小型 Worker 语言插件。目标平台运行验证未完成前,功能矩阵保持 pending。
不使用 PHP 的用户不承担语言服务器下载、索引和进程成本。代价是首次使用需要显式安装插件和 Node.js;macOS 在线包需要 Lithe publisher Ed25519 签名,但不要求发布者持有 Developer ID;未配置 publisher secret 的 debug 包仍只能用于本地模块验证,不能通过正式安装器。Windows 目前提供 Composer 脚本及整套 PHPUnit,未声明支持 macOS 已有的单方法测试发现。Windows 本地包暂不提供在线分发、自动更新或签名身份验证,替换版本需先卸载再导入;包格式仅用于小型 Worker 语言插件。目标平台运行验证未完成前,功能矩阵保持 pending。

插件构建产物、PHPUnit 的 vendor 和应用语言工具缓存没有可靠的跨工作树身份标记,均在 `scripts/worktree-resources.json` 的 excludedResources 中排除。不得把它们共享为可变缓存。

Expand All @@ -73,7 +73,7 @@ PHP 支持由用户选择安装和启用,主程序不携带 PHP 插件包、No
- `MacRuntimeToolDiscoveryTests`:验证启用的 PHP 插件版本目录优先提供 Intelephense launcher。
- `PluginPackageStoreTests/reinstallCanReplaceTheActiveVersionOnlyAfterValidation`:验证重装不会绕过签名校验,并在校验完成后替换当前版本。
- `prepare-php-language-server.sh`:按 JSON 清单下载、校验并组装 Intelephense 运行包;插件版本目录删除时一并删除 launcher 和缓存文件。
- `.github/workflows/release-macos.yml`:Developer ID 构建额外发布架构对应的 PHP 插件 zip;未配置 Developer ID 时不发布可在线安装的独立包。
- `.github/workflows/release-macos.yml` 和 `.github/workflows/release-preview-macos.yml`:每个架构都发布 PHP 插件 zip;`LITHE_PLUGIN_PACKAGE_PRIVATE_KEY` 缺失或不匹配时工作流失败,避免发布无法被客户端识别的包。
- `./scripts/test-macos.sh --filter LithePhpSupportModuleTests`:模块、路径与禁用清理测试。
- `LITHE_RUN_PHP_INTEGRATION=1 ./scripts/test-macos.sh --filter RealPhpIntegrationTests`:真实工具测试;先在 `shared/fixtures/phpunit-project` 执行 `composer install`,并提供 Node.js 与插件组装出的 Intelephense launcher。
- Windows 前端测试包含禁用时不扫描、Composer 数组、下载取消、在途启动后禁用及跨工作区进程隔离。Windows native 测试与实际应用启动必须在 Windows 环境执行;Linux 交叉编译不等于运行验收。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

插件拥有语言服务器的文件、启动入口和生命周期;Lithe 只负责插件包验证、插件启停、运行时发现和现有 LSP 会话编排。运行时资源写入用户级 Application Support、Caches 或临时目录,插件卸载、重装和回滚必须能够连同自己的语言服务器一起清理或替换。

本阶段把这条边界进一步落实到 Rust Core:`plugin.validateManifest`、
`plugin.validateLanguageServer` 和 `plugin.lifecycle` 现在是跨平台可执行的
JSON 契约。macOS 包管理器在加载带语言服务器的插件前调用这些命令;Swift
只负责读取文件、检查插件包内的可执行入口和把结果投影到安装流程。PHP
Support 是第一份实际接入,Windows 仍待迁移到同一组命令。

## 问题

语言服务器通常包含可执行入口、第三方归档、许可证和平台相关资源。把这些文件直接放进主程序,或者让主程序在运行时自行下载,会带来几个问题:不使用该语言的用户承担下载和索引成本;插件签名边界不完整;语言服务器可能写入安装目录,破坏 app bundle 的发布基线;插件卸载后还可能留下脱离插件的运行时。
Expand All @@ -22,6 +28,11 @@ PHP Support 已经验证了独立插件包、固定 Intelephense 版本和插件

当前 PHP 插件的示例是 `Plugins/mac/Official/PhpSupport/language-server.json`。如果另一个语言服务器使用 zip、单文件可执行程序或不同的启动方式,应扩展构建脚本支持的格式,并保持“固定来源、固定校验、构建后签名”的原则;不能跳过校验直接把网络下载物复制进插件。

Rust Core 同时校验插件目录的 `plugin.json`:schema/API、严格三段版本、
宿主兼容区间、entrypoint、模块排序和语言能力到模块的归属。安装包可以使用
完整 `modules` 声明,Core 也兼容共享 fixture 的 `moduleIDs` 形式;两者都不能
让一个语言能力引用另一个插件的模块。平台适配器不应重新增加一套接受条件。

### 2. 构建阶段完成下载、解压和组装

官方 macOS 插件统一通过 `scripts/build-official-plugins.sh` 构建。该脚本负责:
Expand Down Expand Up @@ -49,6 +60,17 @@ macOS 当前的用户级目录是 `<app-support>/Lithe/Plugins/<plugin-id>/versi

LSP 控制中心和语言工具发现沿用现有 Rust Core LSP 会话。插件启用后,组合根把该插件版本目录中的启动器根路径传给平台运行时;插件未安装、被禁用、隔离或等待重启时,不得重新启用宿主内置的旧路径或通用安装器。

插件状态由 Rust Core 的生命周期 reducer 定义:`discovered`、`installing`、
`installed`、`enabling`、`enabled`、`disabling`、`disabled`、`uninstalling`、
`uninstalled` 和 `failed`。LSP session、语言服务器进程、测试运行和其他插件
资源使用不透明 ID 绑定到该状态;Core 在资源仍存在时拒绝 `disable`、
`uninstall`、失败和重置。当前 macOS PHP 切片已由 `ModuleRuntime` 将插件模块的
启用/禁用接到真实 Intelephense session;Rust reducer 仍是无状态 JSON 契约,尚未
替换 macOS/Windows 的完整 package store 和 session registry。下一阶段必须让
这些宿主状态提交和资源绑定统一消费该 reducer,而不是把 reducer 的存在误报为
全产品生命周期迁移完成。UI 的“保留语言服务器”只能保留经过版本/校验确认的
插件资源记录,不能把进程或下载物转移到宿主共享目录。

语言符号、诊断、补全和类型分析继续由上游语言服务器负责。Lithe 只拥有会话生命周期、取消、超时、旧结果保护、资源预算和稳定的跨平台适配契约,不在 Core、Swift 和 Windows 前端各自实现第二套语言语义。

### 5. 运行时资源必须离开安装目录
Expand Down Expand Up @@ -78,6 +100,8 @@ macOS 使用原生 bundle 和 Developer ID 签名;Windows 使用自己的 Taur

## 验证

- `cargo test -p lithe-core tests::plugins --no-fail-fast`:验证 PHP 清单、语言服务器清单、dispatcher 命令和资源阻塞的生命周期;生命周期 wire fixture 位于 `shared/fixtures/plugins/lifecycle-v1.json`。
- `scripts/test-macos.sh --filter 'PluginPackageStoreTests|RealPhpIntegrationTests'`:验证 macOS 包管理器通过 Rust Core 校验,并在有 Intelephense 时执行真实 PHP LSP 全流程。
- `./scripts/verify-official-plugins.sh`:检查官方插件构建、语言服务器清单、启动器和代码签名。
- `./scripts/verify-runtime-bundle-immutability.sh`:确认典型启动和插件工作流不会改写已安装 bundle。
- `node scripts/test-reuse-worktree-resources.mjs`:确认语言服务器下载和插件包不能跨工作树复用。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## 先说结论

更新包的真实性由 Sparkle 的 EdDSA 签名保证,Developer ID 签名是可选的分发能力,不是更新信任的唯一依据。stable 和 preview 完全隔离,回滚只接受内置公钥信任的更新,避免测试版本或伪造清单影响正式用户。
更新包和独立官方插件包的真实性分别由 Sparkle EdDSA 与 Lithe Ed25519 签名保证,Developer ID 签名是可选的分发能力,不是这两类内容信任的唯一依据。stable 和 preview 完全隔离,回滚和插件安装只接受内置公钥信任的内容,避免测试版本或伪造清单影响正式用户。

## 问题

Expand All @@ -31,6 +31,20 @@ runner keychain,用后即删。Developer ID 签名不等于公证,也不保
Gatekeeper 首次运行警告消失——这些是独立的发布关注点,更新器不自动
清除 quarantine。

### 独立官方插件包

PHP Support 作为独立 GitHub Release asset 发布,不再把“能否使用 Developer ID
证书”当成是否生成插件包的条件。构建阶段先完成原生 bundle 的代码签名,再由
`LithePluginPackageSigner` 使用 `LITHE_PLUGIN_PACKAGE_PRIVATE_KEY` 为包内每个文件
生成 SHA-256 清单和 Ed25519 签名;签名文档本身不进入被签名文件列表,避免验证时
出现自引用。Lithe 内置 `lithe-official-plugins-v1` 公钥,安装时先验证原生 bundle,
再验证完整包清单、插件 ID 和版本,任一文件被替换都会拒绝安装。

stable 和 preview 工作流都必须配置同一 repository secret
`LITHE_PLUGIN_PACKAGE_PRIVATE_KEY`;私钥只在构建 runner 的标准输入中使用,不能写入
仓库、release asset 或日志。公钥轮换需要先发布能识别新 key ID 的客户端,再更新
secret 并重新生成包,不能只替换 GitHub secret。

### 全量与差分双轨发布

stable 工作流继续发布 DMG + SHA-256 + `latest-macos.json` 供旧客户端
Expand Down Expand Up @@ -102,6 +116,10 @@ bundle identifier、展示版本、可执行文件架构、渠道和最低系统
少点一次 Gatekeeper 提示。但这本质上是绕过 macOS 的下载来源追踪
机制,属于安全职责之外的东西,因此明确不做,交由既有的可信来源
恢复步骤处理。
- **要求插件使用 Developer ID 或与宿主同 Team ID 签名**:能复用 Apple
的代码签名身份,但会让没有证书的发布环境无法生成独立包,也无法表达“包由
Lithe 发布者签发”的信任关系,因此改用包级 Ed25519 清单,同时保留 bundle
的原生代码签名校验。

## 后果

Expand All @@ -116,6 +134,9 @@ bundle identifier、展示版本、可执行文件架构、渠道和最低系统
- 回滚路径的安全性依赖客户端内置公钥永不改变;如果需要轮换
`SPARKLE_PRIVATE_KEY`,替换 repository secret 本身不够,必须单独
规划迁移(重签或双签过渡期),否则旧客户端会拒绝新签名的更新。
- 独立插件包的安装安全性同样依赖客户端内置的 publisher 公钥;没有匹配 secret
的构建会在 release workflow 中失败,而 debug 构建可以生成供模块验证器使用的
未发布包,但不能绕过正式安装器的包签名要求。
- 需要重新评估的触发条件:如果差分更新的资产数量随架构或渠道增多
逼近 900 的清理阈值,或者需要支持两个以上的更新渠道,当前基于
"30 个 build + 3 个 zip 基线"的保留规则需要重新设计。
Expand All @@ -127,6 +148,7 @@ bundle identifier、展示版本、可执行文件架构、渠道和最低系统
./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh --max-seconds 60 -- --filter StableRollbackDiskImageIntegrationTests
./scripts/test-macos.sh
./scripts/verify-macos-package.sh
./scripts/verify-official-plugins.sh
sparkle_tools=$(zsh scripts/prepare-sparkle-tools.sh)
ruby scripts/test-sparkle-update.rb "$sparkle_tools"
actionlint .github/workflows/release-macos.yml .github/workflows/release-preview-macos.yml .github/workflows/ci-macos.yml
Expand All @@ -150,4 +172,7 @@ Sparkle 版本间升级、缺失/损坏的 delta、下载中断、权限不足
- `scripts/create-macos-update-manifest.rb`
- `.github/workflows/release-macos.yml`
- `.github/workflows/release-preview-macos.yml`
- `scripts/build-official-plugins.sh`
- `macos/Sources/LithePluginPackageSigning/`
- `macos/Sources/Lithe/Platform/MacOS/Plugins/MacPluginPackageStore.swift`
- `docs/architecture/macos-updates.md`
38 changes: 16 additions & 22 deletions .github/workflows/release-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,31 +115,29 @@ jobs:
echo "Incomplete Developer ID configuration: provide the certificate or remove the optional identity and password."
exit 1
else
echo "Developer ID is not configured; using ad-hoc app signing and Sparkle EdDSA update signatures."
echo "Developer ID is not configured; using ad-hoc app signing. Official plugin packages use Lithe Ed25519 signatures."
fi

- name: Build and package the App
env:
LITHE_SPARKLE_PUBLIC_KEY: ${{ vars.SPARKLE_PUBLIC_KEY }}
LITHE_PLUGIN_PACKAGE_PRIVATE_KEY: ${{ secrets.LITHE_PLUGIN_PACKAGE_PRIVATE_KEY }}
LITHE_ARCH: ${{ matrix.architecture }}
LITHE_VERSION: ${{ needs.prepare.outputs.version }}
LITHE_BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
test -n "$LITHE_SPARKLE_PUBLIC_KEY" || { echo "Configure SPARKLE_PUBLIC_KEY before releasing"; exit 1; }
test -n "$LITHE_PLUGIN_PACKAGE_PRIVATE_KEY" || { echo "Configure LITHE_PLUGIN_PACKAGE_PRIVATE_KEY before releasing"; exit 1; }
./scripts/package-app.sh
if [[ -n "${LITHE_CODESIGN_IDENTITY:-}" ]]; then
plugin_root=$(LITHE_CODESIGN_IDENTITY="$LITHE_CODESIGN_IDENTITY" \
scripts/build-official-plugins.sh \
--configuration release \
--triple "${LITHE_ARCH}-apple-macosx" \
--plugin-id dev.lithe.plugin.php-support \
--output "dist/official-plugins/${LITHE_ARCH}")
ditto -c -k --sequesterRsrc --keepParent \
"$plugin_root/dev.lithe.plugin.php-support" \
"dist/Lithe-PHP-Support-${LITHE_VERSION}-${LITHE_ARCH}.zip"
else
echo "Developer ID is not configured; skipping externally installable PHP plugin archive."
fi
plugin_root=$(LITHE_CODESIGN_IDENTITY="${LITHE_CODESIGN_IDENTITY:--}" \
scripts/build-official-plugins.sh \
--configuration release \
--triple "${LITHE_ARCH}-apple-macosx" \
--plugin-id dev.lithe.plugin.php-support \
--output "dist/official-plugins/${LITHE_ARCH}")
ditto -c -k --sequesterRsrc --keepParent \
"$plugin_root/dev.lithe.plugin.php-support" \
"dist/Lithe-PHP-Support-${LITHE_VERSION}-${LITHE_ARCH}.zip"
./scripts/create-dmg.sh
dmg_name="Lithe-${LITHE_VERSION}-${LITHE_ARCH}.dmg"
(cd dist && shasum -a 256 "$dmg_name" > "$dmg_name.sha256")
Expand Down Expand Up @@ -168,9 +166,7 @@ jobs:
"$app_path/Contents/Info.plist"
done
plugin_archive="dist/Lithe-PHP-Support-${LITHE_VERSION}-${LITHE_ARCH}.zip"
if [[ -e "$plugin_archive" ]]; then
test -s "$plugin_archive"
fi
test -s "$plugin_archive"
lipo "$app_path/Contents/MacOS/Lithe" -verify_arch "$LITHE_ARCH"
hdiutil imageinfo "$dmg_path" > /dev/null
test -s "$dmg_path"
Expand All @@ -197,8 +193,7 @@ jobs:
if-no-files-found: error
retention-days: 7

- name: Upload optional PHP plugin archive
if: ${{ hashFiles(format('dist/Lithe-PHP-Support-{0}-{1}.zip', needs.prepare.outputs.version, matrix.architecture)) != '' }}
- name: Upload PHP plugin archive
uses: actions/upload-artifact@v7
with:
name: macos-php-plugin-${{ matrix.architecture }}
Expand Down Expand Up @@ -288,9 +283,8 @@ jobs:
for architecture in arm64 x86_64; do
upload_args+=(dist/sparkle-"$architecture"/*)
plugin_archive="dist/Lithe-PHP-Support-${LITHE_VERSION}-${architecture}.zip"
if [[ -e "$plugin_archive" ]]; then
upload_args+=("$plugin_archive")
fi
test -s "$plugin_archive"
upload_args+=("$plugin_archive")
done

create_args+=(--notes-file "$notes_file")
Expand Down
Loading
Loading