Skip to content
Merged
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` 先按宿主内置 `OfficialPluginCatalog` 固定官方插件的签名策略,拒绝包内 manifest 将 `publisherPackage` 降级为 `sameTeamAsHost`,随后验证原生 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 包不会生成可安装的 PHP 包。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 @@ -4,7 +4,7 @@

## 先说结论

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

## 问题

Expand All @@ -31,6 +31,21 @@ 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`。它是与客户端内置公钥匹配的 base64 编码 32 字节
Ed25519 私钥;可用 `gh secret set LITHE_PLUGIN_PACKAGE_PRIVATE_KEY --repo 1lck/Lithe-IDEA < /secure/path/lithe-plugin-package-private-key.base64` 写入 GitHub,私钥只在构建 runner 的标准输入中使用,不能写入
仓库、release asset 或日志。公钥轮换需要先发布能识别新 key ID 的客户端,再更新
secret 并重新生成包,不能只替换 GitHub secret。

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

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

## 后果

Expand All @@ -135,6 +154,11 @@ bundle identifier、展示版本、可执行文件架构、渠道和最低系统
- 回滚路径的安全性依赖客户端内置公钥永不改变;如果需要轮换
`SPARKLE_PRIVATE_KEY`,替换 repository secret 本身不够,必须单独
规划迁移(重签或双签过渡期),否则旧客户端会拒绝新签名的更新。
- 独立插件包的安装安全性同样依赖客户端内置的 publisher 公钥;没有匹配 secret
的 release 构建会失败,普通 debug 构建会跳过需要 publisher 签名的 PHP 包,
指定单个 PHP 包构建时也会失败。这样构建目录中不会留下缺少
`lithe-plugin-signature.json` 的伪成功包;只有提供私钥的构建才会生成可交给
包级验签路径的产物。
- 需要重新评估的触发条件:如果差分更新的资产数量随架构或渠道增多
逼近 900 的清理阈值,或者需要支持两个以上的更新渠道,当前基于
"30 个 build + 3 个 zip 基线"的保留规则需要重新设计。
Expand All @@ -146,6 +170,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"
node scripts/test-download-sparkle-baseline.mjs
Expand Down Expand Up @@ -174,4 +199,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 @@ -117,32 +117,30 @@ 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
timeout-minutes: 35
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 @@ -171,9 +169,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 Down Expand Up @@ -201,8 +197,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 @@ -292,9 +287,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
36 changes: 16 additions & 20 deletions .github/workflows/release-preview-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,35 +106,33 @@ 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; the preview app will be ad-hoc signed and no externally installable PHP plugin archive will be published."
echo "Developer ID is not configured; the preview app will be ad-hoc signed. Official plugin packages use Lithe Ed25519 signatures."
fi

- name: Build and package the preview app
env:
LITHE_ARCH: ${{ matrix.architecture }}
LITHE_VERSION: ${{ env.PREVIEW_VERSION }}
LITHE_BUILD_NUMBER: ${{ needs.prepare.outputs.build }}
LITHE_PLUGIN_PACKAGE_PRIVATE_KEY: ${{ secrets.LITHE_PLUGIN_PACKAGE_PRIVATE_KEY }}
LITHE_BUILD_TIMESTAMP: ${{ needs.prepare.outputs.timestamp }}
LITHE_BUILD_GIT_BRANCH: ${{ github.event_name == 'workflow_dispatch' && inputs.source_branch || env.PREVIEW_BRANCH }}
LITHE_UPDATE_CHANNEL: preview
LITHE_PREVIEW_TAG: ${{ env.PREVIEW_TAG }}
LITHE_SPARKLE_PUBLIC_KEY: ${{ vars.SPARKLE_PUBLIC_KEY }}
run: |
test -n "$LITHE_SPARKLE_PUBLIC_KEY" || { echo "Configure SPARKLE_PUBLIC_KEY before publishing preview updates"; exit 1; }
test -n "$LITHE_PLUGIN_PACKAGE_PRIVATE_KEY" || { echo "Configure LITHE_PLUGIN_PACKAGE_PRIVATE_KEY before publishing preview updates"; 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 @@ -179,8 +177,7 @@ jobs:
compression-level: 0
retention-days: 14

- name: Upload optional PHP plugin archive
if: ${{ hashFiles(format('dist/Lithe-PHP-Support-{0}-{1}.zip', env.PREVIEW_VERSION, matrix.architecture)) != '' }}
- name: Upload PHP plugin archive
uses: actions/upload-artifact@v7
with:
name: macos-php-plugin-${{ matrix.architecture }}
Expand Down Expand Up @@ -282,10 +279,9 @@ jobs:

for architecture in arm64 x86_64; do
plugin_archive="dist/Lithe-PHP-Support-${LITHE_VERSION}-${architecture}.zip"
if [[ -e "$plugin_archive" ]]; then
gh release upload "$RELEASE_TAG" "$plugin_archive" \
--repo "$GITHUB_REPOSITORY" --clobber
fi
test -s "$plugin_archive"
gh release upload "$RELEASE_TAG" "$plugin_archive" \
--repo "$GITHUB_REPOSITORY" --clobber
done

# Publish every archive before replacing either feed. Retention protects
Expand Down
Loading
Loading