diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2162962..48850f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,11 @@ -# 契约 SDK 的日常验证。发布不走这里(见 release.yml),但**发布会做的事这里基本都做一遍**。 +# 契约 SDK 与插件构建支持包的日常验证。发布不走这里(见 release.yml), +# 但**发布会做的事这里基本都做一遍**。 +# +# 本仓库产出三个包,共用一个版本号: +# VelaShell.PluginSdk 契约程序集 +# VelaShell.PluginSdk.Testing 测试替身 +# VelaShell.PluginSdk.Build 插件工程只需引用的那一个包 +# (2026-09-11 从 velashell-plugin-cli 搬来) # # 与 release.yml 的差别只有三处:不推 nuget.org、不上传产物到 Release、 # 以及 fork PR 拿不到强名称密钥时关掉签名继续跑。 @@ -91,9 +98,15 @@ jobs: run: | $version = '${{ steps.version.outputs.version }}' New-Item -ItemType Directory -Force artifacts/nuget | Out-Null + # VelaShell.PluginSdk.Build 也在这一批里:它与契约包同仓库、同版本、同一次发布 + # (2026-09-11 从 velashell-plugin-cli 搬过来)。顺序无所谓 —— 它对契约包和打包器 + # 都是 ProjectReference,dotnet pack 会自己先把那两个工程建出来。 + # (VelaShell.PluginSdk.Packer 不在这个列表里:它 IsPackable=false,只以构建产物的 + # 形式躺在 .Build 包的 tools/ 下。) $projects = @( 'src/VelaShell.PluginSdk/VelaShell.PluginSdk.csproj', - 'src/VelaShell.PluginSdk.Testing/VelaShell.PluginSdk.Testing.csproj' + 'src/VelaShell.PluginSdk.Testing/VelaShell.PluginSdk.Testing.csproj', + 'src/VelaShell.PluginSdk.Build/VelaShell.PluginSdk.Build.csproj' ) foreach ($project in $projects) { dotnet pack $project -c Release -o artifacts/nuget -p:VelaSdkVersion=$version --nologo $env:VELA_SIGN_ARGS @@ -101,11 +114,11 @@ jobs: } Get-ChildItem artifacts/nuget | Select-Object -ExpandProperty Name - # ── 拆库之后新增的一步 ──────────────────────────────────────────────── - # 本仓库是 Avalonia 版本锁的**权威**,而这个事实只通过包里那一个 buildTransitive - # props 文件传达给下游(宿主的 VerifyAvaloniaMatchesSdk、cli 仓库的 - # VerifyAvaloniaVersionPin 都读它)。单仓库时代那两处核对是直接读文件的, - # 漏打进包也没人发现;现在包就是唯一通道,所以拆完包核对一遍它真的在里面。 + # 本仓库是 Avalonia 版本锁的**权威**,而这个事实传给**宿主**的唯一通道就是包里那一个 + # buildTransitive props 文件(宿主的 VerifyAvaloniaMatchesSdk 读它)。它没打进包也不会 + # 让 pack 失败,所以拆开核对一遍。 + # (.Build 现在同仓库,直接读 $(VelaAvaloniaVersion),不走这条通道 —— 见其 + # VerifyAvaloniaVersionPin。所以这一步守的是宿主那一侧。) - name: Verify the pin props shipped in the package shell: pwsh run: | @@ -121,7 +134,7 @@ jobs: Expand-Archive -Path $zip -DestinationPath (Join-Path $work 'x') $props = Join-Path $work 'x/buildTransitive/VelaShell.PluginSdk.props' if (-not (Test-Path $props)) { - throw "buildTransitive/VelaShell.PluginSdk.props is missing from the package. Downstream repos read the pinned Avalonia version from it; without it the host and VelaShell.PluginSdk.Build lose their only cross-repo consistency check." + throw "buildTransitive/VelaShell.PluginSdk.props is missing from the package. The VelaShell host reads the pinned Avalonia version from it; without it the host loses its only consistency check against this SDK." } $text = Get-Content -Raw $props $expected = [regex]::Match((Get-Content -Raw Directory.Build.props), '([^<]+)').Groups[1].Value @@ -132,6 +145,24 @@ jobs: Write-Host "Pinned Avalonia = $shipped, SDK package version = $version" Write-Host $text + # 端到端冒烟:拿刚打出的包,站在插件作者的位置上走一遍 + # (最小插件工程 → 还原 → 构建 → 出 .vpx → 读回容器 → 查共享程序集有没有漏)。 + # 夹具在 tests/smoke/,细节见 scripts/Invoke-Smoke.ps1 的注释。 + # + # 这一步不是形式,它挡两类东西: + # ① 插件工程是**仓库外环境**,仓库内的 Directory.Build.props 一条也吃不到 —— + # 历史上两个最难查的坑(CA2252 全线报错、AXAML 编译器到不了插件工程) + # 都只有在这个前提下才会显形; + # ② 打包器(VelaShell.PluginSdk.Packer)真的进了包、targets 真的调得动它 —— + # "随包分发、无需全局工具"是本包对插件作者的承诺,而漏打或调错了只有真跑一遍 + # 才看得见(插件作者那边的现象是一句 VELA1003 或构建到一半 Exec 失败)。 + # 改 targets 或改打包器命令面之后必跑。 + - name: Smoke test (plugin project -> build -> .vpx) + shell: pwsh + run: | + & ./scripts/Invoke-Smoke.ps1 -Feed ./artifacts/nuget -Version '${{ steps.version.outputs.version }}' + if ($LASTEXITCODE) { exit $LASTEXITCODE } + - uses: actions/upload-artifact@v7 with: name: nuget-packages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b33c13..2bbf380 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,21 @@ -# 契约 SDK 的发布流水线:在 GitHub 页面**发布 Release** 时自动触发。 +# 插件契约 SDK 与构建支持包的发布流水线:在 GitHub 页面**发布 Release** 时自动触发。 # -# 一次发布产出两个 NuGet 包,共用 Release 标签里的那个版本号: +# 一次发布产出三个 NuGet 包,共用 Release 标签里的那个版本号: # # VelaShell.PluginSdk 契约程序集(插件与宿主唯一共享的类型) # VelaShell.PluginSdk.Testing 测试替身(TestPluginContext 与各能力内存实现) +# VelaShell.PluginSdk.Build 插件工程**只需引用的那一个包**(targets + 打包器 + 依赖锁) # -# **其余三个包不在这里发**(2026-08-27 拆库起): -# VelaShell.Plugin.Cli / VelaShell.PluginSdk.Build … VelaShellLabs/velashell-plugin-cli -# VelaShell.Plugin.Templates ………………………………… VelaShellLabs/velashell-plugin-templates -# 那两个仓库有自己的版本号与自己的 Release 流水线。本仓库发 1.6.0 **不要求**它们跟着发; -# 它们只在想吃到新契约时才把自己引用的 VelaShell.PluginSdk 版本抬上来。 +# .Build 于 2026-09-11 从 velashell-plugin-cli 搬来。它对契约包是 ProjectReference, +# 因此传给插件工程的那份契约永远就是本仓库这一版 —— 原先"引用哪一版契约"那个跨仓库 +# 旋钮就此消失,连带它漂移的可能。插件工程出包用的打包器同样在本仓库 +# (src/VelaShell.PluginSdk.Packer,IsPackable=false,只随 .Build 的 tools/ 分发)。 +# +# **另外两个包不在这里发**: +# VelaShell.Plugin.Cli ……………… VelaShellLabs/velashell-plugin-cli +# VelaShell.Plugin.Templates …… VelaShellLabs/velashell-plugin-templates +# 那两个仓库有自己的版本号与自己的 Release 流水线,本仓库发版**不要求**它们跟着发, +# 本仓库也**不引用**它们的任何东西 —— 依赖方向是单向的,没有环。 # # 📌 版本号:**发版前在本地落好、随功能改动一起合进 main**。 # 跑一次 `pwsh scripts/Set-Version.ps1 <版本>`,它会把该版本写进 Directory.Build.props、 @@ -22,13 +28,16 @@ # Release 标签,与仓库里当时提交了什么无关。忘了第 ② 步的兜底是 CI 的版本同步体检。 # # ⚠️ Avalonia 版本锁:本仓库是整个插件生态里这个版本号的**权威**。改 VelaAvaloniaVersion -# 等于改插件生态的 Avalonia 版本 —— 必须与宿主同一波发布,并且发完之后让 -# velashell-plugin-cli 抬一次它引用的 SDK 版本,否则它那道 VerifyAvaloniaVersionPin 会红。 +# 等于改插件生态的 Avalonia 版本 —— 必须与宿主同一波发布,并且**同一个提交里**把 +# src/VelaShell.PluginSdk.Build 的两处副本(csproj 上那条精确区间、build/*.props 里的 +# 同名默认值)一起改掉,否则构建期的 VELA1000 / VELA1006 会红。 # # 推送用 **NuGet Trusted Publishing(OIDC)**,不存 API Key: # NuGet/login 拿本次运行的 GitHub OIDC 令牌去 nuget.org 换一把**短时效**的推送密钥, # 用完即弃。密钥不落仓库机密,也就没有"泄漏了要轮换"这件事。 -# ⚠️ 首次发版前必须在 nuget.org 上为**这两个包**各建一条可信发布策略,三项都填新值: +# ⚠️ 首次发版前必须在 nuget.org 上为**这三个包**各建一条可信发布策略,三项都填新值: +# (VelaShell.PluginSdk.Build 原先那条指向 velashell-plugin-cli,搬库之后**必须重建**, +# 否则推它的时候会被 nuget.org 拒掉 —— 而那时前两个包已经推上去了。) # Repository owner VelaShellLabs # Repository velashell-plugin-sdk # Workflow file release.yml @@ -126,9 +135,15 @@ jobs: run: | $version = '${{ steps.version.outputs.version }}' New-Item -ItemType Directory -Force artifacts/nuget | Out-Null + # VelaShell.PluginSdk.Build 也在这一批里:它与契约包同仓库、同版本、同一次发布 + # (2026-09-11 从 velashell-plugin-cli 搬过来)。顺序无所谓 —— 它对契约包和打包器 + # 都是 ProjectReference,dotnet pack 会自己先把那两个工程建出来。 + # (VelaShell.PluginSdk.Packer 不在这个列表里:它 IsPackable=false,只以构建产物的 + # 形式躺在 .Build 包的 tools/ 下。) $projects = @( 'src/VelaShell.PluginSdk/VelaShell.PluginSdk.csproj', - 'src/VelaShell.PluginSdk.Testing/VelaShell.PluginSdk.Testing.csproj' + 'src/VelaShell.PluginSdk.Testing/VelaShell.PluginSdk.Testing.csproj', + 'src/VelaShell.PluginSdk.Build/VelaShell.PluginSdk.Build.csproj' ) foreach ($project in $projects) { dotnet pack $project -c Release -o artifacts/nuget -p:VelaSdkVersion=$version --nologo @@ -136,9 +151,11 @@ jobs: } Get-ChildItem artifacts/nuget | Select-Object -ExpandProperty Name - # 拆库之后,包里的 buildTransitive props 是 Avalonia 版本锁传给下游的**唯一通道** - # (宿主的 VerifyAvaloniaMatchesSdk、cli 仓库的 VerifyAvaloniaVersionPin 都读它)。 - # 它没打进包也不会让 pack 失败,所以在推之前拆开核对一遍。 + # 本仓库是 Avalonia 版本锁的**权威**,而这个事实传给**宿主**的唯一通道就是包里那一个 + # buildTransitive props 文件(宿主的 VerifyAvaloniaMatchesSdk 读它)。它没打进包也不会 + # 让 pack 失败,所以拆开核对一遍。 + # (.Build 现在同仓库,直接读 $(VelaAvaloniaVersion),不走这条通道 —— 见其 + # VerifyAvaloniaVersionPin。所以这一步守的是宿主那一侧。) - name: Verify the pin props shipped in the package shell: pwsh run: | @@ -154,7 +171,7 @@ jobs: Expand-Archive -Path $zip -DestinationPath (Join-Path $work 'x') $props = Join-Path $work 'x/buildTransitive/VelaShell.PluginSdk.props' if (-not (Test-Path $props)) { - throw "buildTransitive/VelaShell.PluginSdk.props is missing from the package. Downstream repos read the pinned Avalonia version from it; without it the host and VelaShell.PluginSdk.Build lose their only cross-repo consistency check." + throw "buildTransitive/VelaShell.PluginSdk.props is missing from the package. The VelaShell host reads the pinned Avalonia version from it; without it the host loses its only consistency check against this SDK." } $text = Get-Content -Raw $props $expected = [regex]::Match((Get-Content -Raw Directory.Build.props), '([^<]+)').Groups[1].Value @@ -165,6 +182,24 @@ jobs: Write-Host "Pinned Avalonia = $shipped, SDK package version = $version" Write-Host $text + # 端到端冒烟:拿刚打出的包,站在插件作者的位置上走一遍 + # (最小插件工程 → 还原 → 构建 → 出 .vpx → 读回容器 → 查共享程序集有没有漏)。 + # 夹具在 tests/smoke/,细节见 scripts/Invoke-Smoke.ps1 的注释。 + # + # 这一步不是形式,它挡两类东西: + # ① 插件工程是**仓库外环境**,仓库内的 Directory.Build.props 一条也吃不到 —— + # 历史上两个最难查的坑(CA2252 全线报错、AXAML 编译器到不了插件工程) + # 都只有在这个前提下才会显形; + # ② 打包器(VelaShell.PluginSdk.Packer)真的进了包、targets 真的调得动它 —— + # "随包分发、无需全局工具"是本包对插件作者的承诺,而漏打或调错了只有真跑一遍 + # 才看得见(插件作者那边的现象是一句 VELA1003 或构建到一半 Exec 失败)。 + # 改 targets 或改打包器命令面之后必跑。 + - name: Smoke test (plugin project -> build -> .vpx) + shell: pwsh + run: | + & ./scripts/Invoke-Smoke.ps1 -Feed ./artifacts/nuget -Version '${{ steps.version.outputs.version }}' + if ($LASTEXITCODE) { exit $LASTEXITCODE } + - uses: actions/upload-artifact@v7 with: name: nuget-packages diff --git a/AGENTS.md b/AGENTS.md index 98180fd..24289f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,20 +37,42 @@ VelaShell 生态的**全部文档**集中在一个仓库: - **例外**:留在代码仓库里的少数几份文件不适用上述规则,因为它们服务的是「在这个仓库里写代码」 这件事,搬走只会离使用场景更远。各仓库的例外清单见下面第三节。 -## 三、本仓库:velashell-plugin-sdk(插件契约 SDK) +## 三、本仓库:velashell-plugin-sdk(插件契约 SDK + 构建支持包) -产出 `VelaShell.PluginSdk`(契约程序集)与 `VelaShell.PluginSdk.Testing`(测试替身)两个 NuGet 包。 -这是插件与宿主**唯一共享的那批类型**。 +产出三个**同版本、同一次发布**的 NuGet 包: + +| 包 | 是什么 | +| --- | --- | +| `VelaShell.PluginSdk` | 契约程序集 —— 插件与宿主**唯一共享的那批类型** | +| `VelaShell.PluginSdk.Testing` | 测试替身 | +| `VelaShell.PluginSdk.Build` | 插件工程**只需引用的那一个包**:targets + 随包分发的打包器 + 依赖锁 | + +外加一个**不发包**的工程:`src/VelaShell.PluginSdk.Packer`(`IsPackable=false`)—— +`.vpx` 打包器,只以构建产物的形式躺在 `.Build` 包的 `tools/` 下。 + +`.Build` 于 2026-09-11 从 `velashell-plugin-cli` 搬来,打包器随后也做进了本仓库。 +两者都走 `ProjectReference`,所以发给插件作者的契约与打包器永远就是本仓库这一版 —— +这正是搬过来要的效果:没有任何跨仓库的版本需要对齐。 ### 构建与测试 ```bash dotnet build VelaShell.PluginSdk.slnx dotnet test VelaShell.PluginSdk.slnx -c Debug + +# 端到端冒烟:拿刚打出的包当插件作者走一遍 +dotnet pack src/VelaShell.PluginSdk/VelaShell.PluginSdk.csproj -c Release -o artifacts/nuget +dotnet pack src/VelaShell.PluginSdk.Build/VelaShell.PluginSdk.Build.csproj -c Release -o artifacts/nuget +pwsh scripts/Invoke-Smoke.ps1 -Feed ./artifacts/nuget -Version <版本> ``` `-c Debug` 不是随口一说:Release 会打开强名称签名,而测试程序集不是签名友元, -Release 下 `dotnet test` 编不过。签名密钥不入库,CI 从 `STRONG_NAME_KEY` 机密还原。 +Release 下 `dotnet test` 编不过。签名密钥不入库,CI 从 `STRONG_NAME_KEY` 机密还原; +本地想打 Release 包又没有密钥时加 `-p:SignAssembly=false`。 + +冒烟夹具在 `tests/smoke/`:一个手写的最小插件工程,刻意带两个空的 +`Directory.Build.props`/`.targets` 来切断向上查找 —— **插件工程是仓库外环境,仓库内的 +构建约定一条也吃不到**。**改了 `.Build` 的 targets、或抬了它引用的 vela-plugin 版本,必须跑它。** ### 只有本仓库能动的两件事 @@ -58,8 +80,24 @@ Release 下 `dotnet test` 编不过。签名密钥不入库,CI 从 `STRONG_NAME_ 纪律是「SDK 主版本 == apiLevel」,由 `scripts/Set-Version.ps1` 在发版前硬核对。 **破坏性变更要先手工把 `Level` +1**:脚本会核对但不代改,因为「契约破没破」是人的判断。 2. **Avalonia 版本锁的权威** —— 值写在 `Directory.Build.props` 的 `VelaAvaloniaVersion`, - 由本包经 `buildTransitive` 导出成 `$(VelaSdkPinnedAvaloniaVersion)`,宿主与 CLI 仓库在 - 各自构建期核对。改它 = 改整个插件生态的 Avalonia 版本,必须与宿主同一波发布。 + 由 `VelaShell.PluginSdk` 包经 `buildTransitive` 导出成 `$(VelaSdkPinnedAvaloniaVersion)` + 供宿主核对;`.Build` 现在同仓库,直接读这个属性(`VerifyAvaloniaVersionPin`)。 + 改它 = 改整个插件生态的 Avalonia 版本,必须与宿主同一波发布,并且在**同一个提交里** + 把 `.Build` 的两处副本(csproj 上那条精确区间、`build/*.props` 里的同名默认值)一起改掉。 + +### 打包器为什么在本仓库、为什么是独立进程 + +`.vpx` 的定义(`VpxContainer`)与清单规则(`PluginManifestReader`)本来就在 +`VelaShell.PluginSdk` 里 —— `vela-plugin` 只是它的另一个调用方。让 `.Build` 绕一趟 cli 仓库 +去取打包器,换来的只是一个要人盯着的跨仓库版本旋钮(2026-09-11 短暂存在过,已去掉)。 + +**刻意不做成 MSBuild 任务**:VS 的 MSBuild 跑在 .NET Framework 上,而本仓库是 net11.0。 +做成 `` 的话,插件作者在 VS 里一按生成就会因为加载不了任务程序集而失败 —— +而清单校验是 `AfterTargets="Build"` 的,每次生成都跑。`dotnet exec` 一个独立进程则与调用方 +的 MSBuild 是哪种运行时完全无关。 + +改了打包器的命令面(`validate` / `pack` 的参数或输出),`.Build` 的 `build/*.targets` +要跟着改,**并跑一次 `scripts/Invoke-Smoke.ps1`** —— 那个冒烟是这条链唯一的守门人。 ### 版本号不归你定 —— 也不要为了自证能编译去造本地包 @@ -68,8 +106,10 @@ Release 下 `dotnet test` 编不过。签名密钥不入库,CI 从 `STRONG_NAME_ - **不要**跑 `scripts/Set-Version.ps1`、不要动 `Directory.Build.props` 的 `VelaSdkVersion`、 不要动 `VelaPluginApi.SdkVersion`。下一版是 1.6.0 还是 1.5.2、还是先发个 preview, 取决于当时排了什么、要不要跟宿主同波发 —— 这些你不知道。 -- **不要** `dotnet pack` 出本地包、不要在任何 `nuget.config` 里加本地源、 +- **不要** `dotnet pack` 出本地包给**别的仓库**用、不要在任何 `nuget.config` 里加本地源、 不要把下游仓库的 `Directory.Packages.props` 指到一个还不存在的版本。 + (例外只有一个:`scripts/Invoke-Smoke.ps1` 会把包打进本仓库的临时源自己验自己, + 那是 CI 的常规步骤,产物不出本仓库、也不进任何人的 `nuget.config`。) 自己造一个包来让编译通过,等于把「这套东西还没发布」这个事实从构建结果里抹掉; 而且本地包必然**未签名**(`VelaShell.snk` 不在仓库里),下游一编译就是一片 `CS0012` 强名称不匹配 —— 那是自己制造的噪声,不是真问题。 diff --git a/Directory.Build.props b/Directory.Build.props index dad8cd7..744a795 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,14 +14,23 @@ 12.1.2 + + + + + + diff --git a/scripts/Invoke-Smoke.ps1 b/scripts/Invoke-Smoke.ps1 new file mode 100644 index 0000000..1996652 --- /dev/null +++ b/scripts/Invoke-Smoke.ps1 @@ -0,0 +1,158 @@ +#Requires -Version 7.0 +<# +.SYNOPSIS + 端到端冒烟:拿刚打出来的包,站在**插件作者**的位置上走一遍。 + +.DESCRIPTION + 验的是 VelaShell.PluginSdk.Build:插件作者**只引这一个包**,它到不到位,这里说了算。 + (本脚本随该包一起从 velashell-plugin-cli 搬到本仓库,2026-09-11。) + + 拆库(2026-08-27)之前这一步是"装模板 → dotnet new → 构建 → 出 .vpx"。模板搬去 + VelaShellLabs/velashell-plugin-templates 之后,不能再依赖那个包来验自己的包 + —— 否则模板仓库出问题会让这里的 CI 无端变红,而且发包时还得先有模板包。 + + 所以夹具改成自带:tests/smoke/ 下是一个**手写的最小插件工程**,与 velaplugin-ui + 模板同形(命令 + 面板 + 编译期 AXAML)。本脚本把它复制到临时目录、把包版本填进去、 + 从给定的本地源还原,然后: + + 1. `dotnet build -t:PackVpx` —— 覆盖 targets 的全部四件事:共享程序集不落地、 + Avalonia 版本核对、清单编译期校验、一步出包; + 2. 确认 bin/vpx/ 下真的有 .vpx; + 3. 用打包器的 info 把容器读回来(魔数、摘要、清单); + 4. 确认插件输出目录里**没有**共享程序集 —— 出现了就说明 exclude=Runtime 的链路断了。 + + 夹具刻意放在临时目录而不是原地构建:tests/smoke/ 里那两个空的 + Directory.Build.props/.targets 已经切断了向上查找,但复制出去更贴近真实 + (插件作者的工程不在本仓库里)。 + +.PARAMETER Feed + 本地 NuGet 源目录,里面应有刚打出的 VelaShell.PluginSdk.Build.<版本>.nupkg + 与 VelaShell.PluginSdk.<版本>.nupkg(.Build 依赖同版本的契约包,而那一版还没上 nuget.org)。 + 本仓库三个包之外不需要任何别的东西 —— 打包器就在 .Build 包的 tools/ 里。 + +.PARAMETER Version + 要验的 VelaShell.PluginSdk.Build 版本号。 + +.PARAMETER PackerDll + 用来做第 3 步读回校验的打包器。默认**从刚打出的 .Build 包里取** + (tools/net11.0/VelaShell.PluginSdk.Packer.dll)。 + + 刻意不用 src/VelaShell.PluginSdk.Packer 的 bin 产物:从包里取顺带核对了**打包器真的 + 进了包**。漏了的话,插件作者 `-t:PackVpx` 时才会拿到 VELA1003,而那时已经发出去了。 + +.PARAMETER WorkDirectory + 工作目录。默认取 RUNNER_TEMP(CI)或系统临时目录。 + +.EXAMPLE + pwsh scripts/Invoke-Smoke.ps1 -Feed ./artifacts/nuget -Version 1.5.0 +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] [string] $Feed, + [Parameter(Mandatory)] [string] $Version, + [string] $PackerDll, + [string] $WorkDirectory +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$root = Split-Path -Parent $PSScriptRoot +$feedPath = (Resolve-Path $Feed).Path +$fixture = Join-Path $root 'tests/smoke' +if (-not (Test-Path $fixture)) { throw "冒烟夹具不存在:$fixture" } + +if (-not $WorkDirectory) { + $WorkDirectory = Join-Path ($env:RUNNER_TEMP ?? [IO.Path]::GetTempPath()) 'vela-plugin-smoke' +} +# 打包器解到**工作区之外**的兄弟目录:解进去的话,那些文件会成为插件工程的默认 None 项, +# 平白给这个"模拟插件作者"的工程加一堆它本来不会有的东西。 +$packerDirectory = "$WorkDirectory.packer" +foreach ($stale in @($WorkDirectory, $packerDirectory)) { + if (Test-Path $stale) { Remove-Item -Recurse -Force $stale } +} +New-Item -ItemType Directory -Force $WorkDirectory | Out-Null + +# ── 打包器从哪来 ──────────────────────────────────────────────────────────── +# 从**刚打出的那个包**里拆一份出来,而不是直接用 src/VelaShell.PluginSdk.Packer 的 bin —— +# 顺带核对打包器真的进了包(它随包分发是本包对插件作者的承诺)。 +if (-not $PackerDll) { + $nupkg = Join-Path $feedPath "VelaShell.PluginSdk.Build.$Version.nupkg" + if (-not (Test-Path $nupkg)) { throw "本地源里没有 $nupkg;先 dotnet pack 出包再跑冒烟。" } + New-Item -ItemType Directory -Force $packerDirectory | Out-Null + # Expand-Archive 只认 .zip 扩展名,.nupkg 直接喂给它会被拒;先复制一份改名。 + $zip = Join-Path $packerDirectory 'pkg.zip' + Copy-Item $nupkg $zip + Expand-Archive -Path $zip -DestinationPath (Join-Path $packerDirectory 'x') + $PackerDll = Join-Path $packerDirectory 'x/tools/net11.0/VelaShell.PluginSdk.Packer.dll' + if (-not (Test-Path $PackerDll)) { + throw @" +VelaShell.PluginSdk.Build.$Version.nupkg 里没有 tools/net11.0/VelaShell.PluginSdk.Packer.dll。 +打包器随包分发是这个包对插件作者的承诺(`dotnet build -t:PackVpx` 不必装任何全局工具), +漏了的话插件作者只会拿到一句 VELA1003。看 VelaShell.PluginSdk.Build.csproj 的 +AddVelaPackerToPackage。 +"@ + } +} +if (-not (Test-Path $PackerDll)) { throw "找不到打包器:$PackerDll。" } + +Write-Host "== 冒烟:VelaShell.PluginSdk.Build $Version ==" +Write-Host " 源 $feedPath" +Write-Host " 打包器 $PackerDll" +Write-Host " 工作区 $WorkDirectory" + +# ── 铺夹具 ────────────────────────────────────────────────────────────────── +Copy-Item -Recurse -Force (Join-Path $fixture '*') $WorkDirectory + +$csproj = Join-Path $WorkDirectory 'Smoke.csproj' +$text = [IO.File]::ReadAllText($csproj) +if ($text -notmatch 'VELA_BUILD_VERSION') { + throw "tests/smoke/Smoke.csproj 里找不到 VELA_BUILD_VERSION 占位符;夹具改过了?" +} +[IO.File]::WriteAllText($csproj, $text.Replace('VELA_BUILD_VERSION', $Version)) + +# 很关键:不清掉的话机器上已有的 nuget.org 缓存或别的源可能把**上一版** +# 同名包喂进来,冒烟就验不到刚打出的这一版了。nuget.org 仍要留着 —— Avalonia 从那来。 +@" + + + + + + + + +"@ | Set-Content (Join-Path $WorkDirectory 'nuget.config') + +Push-Location $WorkDirectory +try { + # ── 1. 构建 + 出包 ────────────────────────────────────────────────────── + dotnet build -c Release -t:PackVpx --nologo + if ($LASTEXITCODE -ne 0) { throw "插件工程构建失败(见上方输出)。" } + + # ── 2. 产物在不在 ─────────────────────────────────────────────────────── + $vpx = Get-ChildItem 'bin/vpx/*.vpx' -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $vpx) { throw "PackVpx 没产出 .vpx。" } + Write-Host " 产物 $($vpx.Name)" + + # ── 3. 容器读得回来吗 ─────────────────────────────────────────────────── + dotnet $PackerDll info $vpx.FullName + if ($LASTEXITCODE -ne 0) { throw "打包器 info 读不回刚打出的 .vpx。" } + + # ── 4. 共享程序集有没有漏进插件输出目录 ───────────────────────────────── + # 漏了就说明 exclude=Runtime 的链路断了。这不是"包大了一点"的问题: + # 装载器一律让这些程序集回落到宿主那份,插件目录里那些副本只会误导人, + # 让人以为版本是自己带的那份说了算。 + $leaked = Get-ChildItem 'bin/Release/net11.0' -Filter '*.dll' | + Where-Object { $_.Name -like 'Avalonia*' -or $_.Name -eq 'VelaShell.PluginSdk.dll' } + if ($leaked) { + throw "共享程序集漏进了插件输出目录:$($leaked.Name -join ', ')" + } + + Write-Host "== 冒烟通过 ==" +} +finally { + Pop-Location +} + +exit 0 diff --git a/scripts/Set-Version.ps1 b/scripts/Set-Version.ps1 index fa143af..7fd1521 100644 --- a/scripts/Set-Version.ps1 +++ b/scripts/Set-Version.ps1 @@ -4,7 +4,9 @@ 把 SDK 版本号写进本仓库里所有需要它的地方。 .DESCRIPTION - 拆库之后本仓库只管**契约包**的版本号(VelaShell.PluginSdk / .Testing),落点缩到四处: + 本仓库这一个版本号管三个同时发布的包(VelaShell.PluginSdk / .Testing / .Build —— + .Build 于 2026-09-11 从 velashell-plugin-cli 搬来,见 Directory.Build.props 的注释)。 + 落点四处: Directory.Build.props —— 包版本的默认值 src/…/VelaPluginApi.cs SdkVersion 常量 —— 宿主写进 host.json 的值, @@ -18,12 +20,17 @@ docs 那两处不影响功能,但它们是给人照抄的 —— 2026-08-30 全部文档搬到 VelaShellLabs/velashell-docs 之后,它们不在本仓库的 checkout 里,所以找不到就跳过。 - **不在本仓库的落点**(2026-08-27 拆库起,各自由所在仓库的同名脚本管): + **不在本仓库的落点**(各自由所在仓库的同名脚本管): · dotnet new 模板的 sdkVersion 默认值,以及 velashell-docs 里 zh|en/templates/dev-guide.md 的 PackageReference 片段 …… velashell-plugin-templates · velashell-docs 里 zh|en/cli/cli.md 的版本横幅 ……… velashell-plugin-cli - 那几处跟的是 VelaShell.PluginSdk.Build / VelaShell.Plugin.Cli 的版本,与本仓库无关 —— - 这正是拆库要的效果:SDK 发 1.6.0 不必惊动模板和 CLI。 + 前者跟的是 VelaShell.PluginSdk.Build 的版本 —— 那**现在就是本仓库的版本号**, + 所以本仓库发完版之后,想让 `dotnet new velaplugin` 生成的工程指过来,要去模板仓库 + 抬一次 VelaBuildPackageVersion 再发一版模板(不做也不会坏:新建的工程继续引用上一版 + .Build 包,那是完全可用的)。后者跟的是 vela-plugin 的版本,与本仓库无关。 + + 打包器(src/VelaShell.PluginSdk.Packer)没有自己的版本号:它 IsPackable=false, + 只以构建产物的形式随 .Build 的 tools/ 分发,跟着本仓库这一个版本号走。 发版流水线在解析出 Release 标签之后**第一件事**也会跑本脚本 (见 .github/workflows/release.yml),因此产物永远与标签一致,与仓库里当时提交了什么 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8c3460c..9549ba5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -34,7 +34,11 @@ + STRONG_NAME_KEY 机密还原。本地 Debug 构建不签名,不需要密钥。 + ⚠️ VelaShell.PluginSdk.Build 是例外:它没有对外的程序集面(IncludeBuildOutput=false), + 在自己的 csproj 里把 SignAssembly 与 IncludeSymbols 都显式关掉了 —— 不关的话, + 本地 `dotnet pack -c Release` 会因为找不到 .snk 在 CSC 那一步失败, + 而 IncludeSymbols 会生成一个空 .snupkg 让 nuget.org 直接 400。理由见那边的注释。 --> true $(VelaStrongNameKey) diff --git a/src/VelaShell.PluginSdk.Build/README.md b/src/VelaShell.PluginSdk.Build/README.md new file mode 100644 index 0000000..4b956c0 --- /dev/null +++ b/src/VelaShell.PluginSdk.Build/README.md @@ -0,0 +1,39 @@ +# VelaShell.PluginSdk.Build + +VelaShell 插件工程唯一需要引用的包。引一个,下面这些全都到位: + +```xml + +``` + +| 它做了什么 | 为什么 | +| --- | --- | +| 传递引入 `VelaShell.PluginSdk`(契约)与 **与宿主版本一致的 Avalonia**,均为编译期引用 | 装载器强制插件与宿主共享这两类程序集;版本不一致会在用户机器上才炸 | +| `EnableDynamicLoading=true`、`plugin.json` 进输出目录 | 插件是被动态装载的组件,ALC 靠 `deps.json` 解析自带依赖 | +| 共享程序集的运行时资产不落插件目录 | 放进去也不会被加载,只是撑大包体 | +| Avalonia 版本冲突从警告升为错误(NU1608/NU1605) | 版本漂移必须在构建期就红,而不是装机后才现形 | +| 构建后按宿主同一套规则校验 `plugin.json` | 杜绝"本机构建过、宿主装不上" | +| `dotnet build -t:PackVpx` 一步出 `.vpx` | 打包器随包分发,不必安装任何全局工具 | + +## 出包 + +```bash +dotnet build -c Release -t:PackVpx +# → bin/vpx/<插件id>-<版本>.vpx + +# 带签名(密钥用 `vela-plugin keygen` 生成,不要提交进仓库) +dotnet build -c Release -t:PackVpx -p:VelaSigningKey=/path/to/key.pem +``` + +## 可调属性 + +| 属性 | 默认 | 说明 | +| --- | --- | --- | +| `VelaPluginManifest` | `$(MSBuildProjectDirectory)\plugin.json` | 清单路径 | +| `VelaVpxOutputDirectory` | `bin\vpx\` | `.vpx` 产物目录 | +| `VelaSigningKey` | 空 | 打包时用的 PEM 私钥 | +| `VelaPackMask` | `true` | 是否对载荷做掩码变换 | +| `VelaValidateManifestOnBuild` | `true` | 构建后是否校验清单 | +| `VelaSkipAvaloniaVersionCheck` | `false` | 跳过 Avalonia 版本一致性检查 | + +完整开发指南: diff --git a/src/VelaShell.PluginSdk.Build/VelaShell.PluginSdk.Build.csproj b/src/VelaShell.PluginSdk.Build/VelaShell.PluginSdk.Build.csproj new file mode 100644 index 0000000..dd58602 --- /dev/null +++ b/src/VelaShell.PluginSdk.Build/VelaShell.PluginSdk.Build.csproj @@ -0,0 +1,126 @@ + + + + VelaShell.PluginSdk.Build + VelaShell 插件工程的构建支持包:一次引用即得到契约程序集、与宿主版本一致的 Avalonia、共享程序集不落地的处理、清单编译期校验,以及 `dotnet build -t:PackVpx` 一步出 .vpx 包。插件工程只需引用本包。 + velashell;plugin;msbuild;vpx;sdk + + + false + + false + + false + false + false + + $(NoWarn);NU5128 + $(TargetsForTfmSpecificContentInPackage);AddVelaPackerToPackage + + + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + <_VelaAvaloniaPin>@(PackageReference->WithMetadataValue('Identity','Avalonia')->'%(Version)') + <_VelaAvaloniaPin>$(_VelaAvaloniaPin.Replace('[','').Replace(']','')) + + + + + + + + + + + + + + + + + <_VelaPackerOutputDir>$(MSBuildThisFileDirectory)..\VelaShell.PluginSdk.Packer\bin\$(Configuration)\$(TargetFramework)\ + + + <_VelaPackerFile Include="$(_VelaPackerOutputDir)*.dll;$(_VelaPackerOutputDir)*.json" /> + + + + + + diff --git a/src/VelaShell.PluginSdk.Build/build/VelaShell.PluginSdk.Build.props b/src/VelaShell.PluginSdk.Build/build/VelaShell.PluginSdk.Build.props new file mode 100644 index 0000000..4ac47ec --- /dev/null +++ b/src/VelaShell.PluginSdk.Build/build/VelaShell.PluginSdk.Build.props @@ -0,0 +1,42 @@ + + + + + + + true + + + true + + + $(MSBuildProjectDirectory)\plugin.json + + + 12.1.2 + + + $(WarningsAsErrors);NU1608;NU1605 + + + true + + + true + + + + + + + + diff --git a/src/VelaShell.PluginSdk.Build/build/VelaShell.PluginSdk.Build.targets b/src/VelaShell.PluginSdk.Build/build/VelaShell.PluginSdk.Build.targets new file mode 100644 index 0000000..fda9e00 --- /dev/null +++ b/src/VelaShell.PluginSdk.Build/build/VelaShell.PluginSdk.Build.targets @@ -0,0 +1,107 @@ + + + + + + + $(VelaPluginCli) + $(MSBuildThisFileDirectory)..\tools\net11.0\VelaShell.PluginSdk.Packer.dll + $(MSBuildProjectDirectory)\bin\vpx\ + + + + + + + + runtime + + + + + + + + + + + + + + + <_VelaAvaloniaRef Include="@(ReferencePath)" Condition="'%(ReferencePath.NuGetPackageId)' == 'Avalonia'" /> + + + + + + + + + + + + + + + + <_VelaPackArgs>pack "$(TargetDir.TrimEnd('\'))" --output "$(VelaVpxOutputDirectory.TrimEnd('\'))" + <_VelaPackArgs Condition="'$(VelaPackMask)' != 'true'">$(_VelaPackArgs) --no-mask + <_VelaPackArgs Condition="'$(VelaSigningKey)' != ''">$(_VelaPackArgs) --key "$(VelaSigningKey)" + + + + + diff --git a/src/VelaShell.PluginSdk.Packer/Program.cs b/src/VelaShell.PluginSdk.Packer/Program.cs new file mode 100644 index 0000000..fa2f6e2 --- /dev/null +++ b/src/VelaShell.PluginSdk.Packer/Program.cs @@ -0,0 +1,265 @@ +using System.IO.Compression; +using System.Security.Cryptography; +using VelaShell.PluginSdk.Manifest; +using VelaShell.PluginSdk.Packaging; + +namespace VelaShell.PluginSdk.Packer; + +/// +/// .vpx 打包器。VelaShell.PluginSdk.Build 把本工程的构建产物收进包的 tools/, +/// 它的 targets 用 dotnet exec 调这里的 validate 与 pack。 +/// +/// +/// 命令面**刻意只有三条**,而且只服务于一个调用方(那套 targets)—— +/// 面向人的完整工具是 vela-plugin(VelaShellLabs/velashell-plugin-cli), +/// 它有商店、开发内环、签名与体检。这里多加一条命令,就多一份要与那边对齐的东西。 +/// +/// 真正的逻辑一行都不在这里:容器格式在 ,清单规则在 +/// ,两者都在 VelaShell.PluginSdk —— 也就是**同一个包** +/// 发给插件工程编译用的那份契约。"打包器与契约同版本"因此是构建出来的事实,不是约定。 +/// +internal static class Program +{ + private static int Main(string[] args) + { + try + { + if (args.Length == 0) + { + return Usage(); + } + string[] rest = args[1..]; + return args[0] switch + { + "validate" => Validate(rest), + "pack" => Pack(rest), + "info" => Info(rest), + "help" or "--help" or "-h" => Usage(), + _ => throw new PackerException($"Unknown command '{args[0]}'. Try validate | pack | info.") + }; + } + catch (PackerException ex) + { + Console.Error.WriteLine($"error: {ex.Message}"); + return 1; + } + // 清单/容器两类异常带的是"哪一条规则没过",原样透出比包一层更有用。 + catch (Exception ex) when (ex is PluginManifestException or VpxFormatException + or IOException or UnauthorizedAccessException) + { + Console.Error.WriteLine($"error: {ex.Message}"); + return 1; + } + } + + private static int Usage() + { + Console.WriteLine(""" + vela .vpx packer - invoked by the VelaShell.PluginSdk.Build MSBuild targets. + + validate check plugin.json and the entry assembly + pack [options] pack a plugin output directory into .vpx + -o, --output destination (default: next to ) + --no-mask do not mask the payload + -k, --key sign with this ECDSA P-256 private key + info print the container header and manifest + + For everyday use install the full tool: dotnet tool install -g VelaShell.Plugin.Cli + """); + return 0; + } + + /// 校验清单与入口程序集(目录或 plugin.json 路径均可)。 + private static int Validate(string[] args) + { + Options options = Options.Parse(args); + string target = Path.GetFullPath(options.Positional.FirstOrDefault() ?? "."); + string directory = Directory.Exists(target) ? target : Path.GetDirectoryName(target)!; + PluginManifest manifest = LoadManifest(directory); + RequireEntry(directory, manifest); + + Console.WriteLine($"OK {manifest.Id} v{manifest.Version} ({manifest.DisplayName})"); + Console.WriteLine($" entry {manifest.Entry}"); + Console.WriteLine($" hostMode {manifest.HostMode}"); + Console.WriteLine($" apiLevel {manifest.ApiLevel} (this SDK: {VelaPluginApi.Level})"); + Console.WriteLine($" author {manifest.Author ?? manifest.Publisher ?? "(not set)"}"); + if (manifest.ApiLevel > VelaPluginApi.Level) + { + Warn($"apiLevel {manifest.ApiLevel} is newer than this SDK ({VelaPluginApi.Level}); " + + "hosts built on this SDK will refuse to load the plugin."); + } + if (manifest.Author is null && manifest.Publisher is null) + { + Warn("neither \"author\" nor \"publisher\" is set - the plugin manager page will show no author."); + } + return 0; + } + + /// 把插件产物目录打成 .vpx。 + private static int Pack(string[] args) + { + Options options = Options.Parse(args); + string source = Path.GetFullPath(options.Positional.FirstOrDefault() ?? "."); + PluginManifest manifest = LoadManifest(source); + RequireEntry(source, manifest); + + string fileName = $"{manifest.Id}-{manifest.Version}{VpxContainer.FileExtension}"; + string output = options.Get("--output") is { } requested + // 目录 → 用约定文件名;否则当成完整路径。MSBuild 的 PackVpx 传的就是目录。 + ? Path.GetFullPath(Directory.Exists(requested) + || !requested.EndsWith(VpxContainer.FileExtension, StringComparison.OrdinalIgnoreCase) + ? Path.Combine(requested, fileName) + : requested) + : Path.GetFullPath(Path.Combine(source, "..", fileName)); + + using ECDsa? key = LoadPrivateKey(options.Get("--key")); + VpxContainer.Pack(source, output, new() + { + Mask = !options.Has("--no-mask"), + SigningKey = key + }); + + VpxPackageInfo info = VpxContainer.ReadInfo(output); + Console.WriteLine($"Packed {manifest.Id} v{manifest.Version}"); + Console.WriteLine($" -> {output}"); + Console.WriteLine($" payload {info.PayloadLength} bytes, sha256 {info.PayloadSha256}"); + Console.WriteLine($" {(info.Signature is null ? "unsigned" : "signed by " + VpxContainer.PublicKeyFingerprint(info.Signature.PublicKey))}"); + return 0; + } + + /// + /// 打印容器头、签名状态与包里的清单。targets 用不到它 —— 它在这里是给 CI 的端到端冒烟 + /// 做"把刚打出的包读回来"那一步用的(见 scripts/Invoke-Smoke.ps1),顺带也方便排障。 + /// + private static int Info(string[] args) + { + Options options = Options.Parse(args); + string package = Path.GetFullPath(options.Positional.FirstOrDefault() + ?? throw new PackerException("Missing package path. Usage: info ")); + if (!File.Exists(package)) + { + throw new PackerException($"Package not found: {package}"); + } + + VpxPackageInfo info = VpxContainer.ReadInfo(package); + Console.WriteLine(Path.GetFileName(package)); + Console.WriteLine($" format v{info.FormatVersion}"); + Console.WriteLine($" flags {info.Flags}"); + Console.WriteLine($" payload {info.PayloadLength} bytes"); + Console.WriteLine($" sha256 {info.PayloadSha256}"); + VpxSignatureState signature = VpxContainer.VerifySignature(info); + Console.WriteLine($" signature {(signature == VpxSignatureState.Trusted ? "Valid" : signature.ToString())}" + + (info.Signature is { } block ? $" ({VpxContainer.PublicKeyFingerprint(block.PublicKey)})" : "")); + + // 光有摘要看不出这是哪个插件,把清单从载荷里读出来。 + using Stream payload = VpxContainer.OpenPayload(package); + using ZipArchive archive = new(payload, ZipArchiveMode.Read); + if (archive.GetEntry(PluginManifestReader.FileName) is { } entry) + { + using StreamReader reader = new(entry.Open()); + PluginManifest manifest = PluginManifestReader.Parse(reader.ReadToEnd()); + Console.WriteLine($" plugin {manifest.Id} v{manifest.Version} ({manifest.DisplayName})"); + Console.WriteLine($" author {manifest.Author ?? manifest.Publisher ?? "(not set)"}"); + } + return 0; + } + + // ---- helpers ---------------------------------------------------------- + + private static PluginManifest LoadManifest(string directory) + { + string manifestPath = Path.Combine(directory, PluginManifestReader.FileName); + if (!File.Exists(manifestPath)) + { + throw new PackerException($"No {PluginManifestReader.FileName} in '{directory}'. " + + "Point the command at the plugin's build output directory."); + } + return PluginManifestReader.Load(manifestPath); + } + + private static void RequireEntry(string directory, PluginManifest manifest) + { + if (!File.Exists(Path.Combine(directory, manifest.Entry))) + { + throw new PackerException($"Entry assembly '{manifest.Entry}' is missing from '{directory}'. " + + "Build the plugin project first."); + } + } + + private static ECDsa? LoadPrivateKey(string? path) + { + if (path is null) + { + return null; + } + string full = Path.GetFullPath(path); + if (!File.Exists(full)) + { + throw new PackerException($"Key file not found: {full}"); + } + ECDsa key = ECDsa.Create(); + try + { + key.ImportFromPem(File.ReadAllText(full)); + } + catch (ArgumentException ex) + { + key.Dispose(); + throw new PackerException($"'{full}' is not a PEM private key: {ex.Message}"); + } + return key; + } + + private static void Warn(string message) => Console.WriteLine($"warning: {message}"); +} + +/// 参数已经用尽了可诊断的信息,剩下的只是把话说给人听。 +internal sealed class PackerException(string message) : Exception(message); + +/// +/// 极简参数解析。调用方只有那套 targets,形状是固定的:一个位置参数 + 三个选项。 +/// 不引第三方命令行库 —— 本工程要被原样复制进别人的 tools/,依赖越少越好。 +/// +internal sealed class Options +{ + private readonly Dictionary _options = [with(StringComparer.Ordinal)]; + + public List Positional { get; } = []; + + public static Options Parse(string[] args) + { + Options parsed = new(); + for (int i = 0; i < args.Length; i++) + { + string token = args[i]; + if (token.Length == 0 || token[0] != '-') + { + parsed.Positional.Add(token); + continue; + } + string? name = token switch + { + "--output" or "-o" => "--output", + "--key" or "-k" => "--key", + _ => null + }; + if (name is null) + { + // 开关(目前只有 --no-mask)。不认识的也收下:多一个未知开关不该让构建失败, + // 而漏掉一个带值选项会让下一个 token 被当成位置参数,那才是会出错的方向。 + parsed._options[token] = null; + continue; + } + if (i + 1 >= args.Length) + { + throw new PackerException($"'{token}' needs a value."); + } + parsed._options[name] = args[++i]; + } + return parsed; + } + + public bool Has(string name) => _options.ContainsKey(name); + + public string? Get(string name) => _options.TryGetValue(name, out string? value) ? value : null; +} diff --git a/src/VelaShell.PluginSdk.Packer/README.md b/src/VelaShell.PluginSdk.Packer/README.md new file mode 100644 index 0000000..3cf315c --- /dev/null +++ b/src/VelaShell.PluginSdk.Packer/README.md @@ -0,0 +1,29 @@ +# VelaShell.PluginSdk.Packer + +`.vpx` 打包器。**不发 NuGet 包** —— 它以构建产物的形式被 +[`VelaShell.PluginSdk.Build`](../VelaShell.PluginSdk.Build/README.md) 收进那个包的 +`tools/net11.0/`,插件工程 `dotnet build -t:PackVpx` 调的就是它。 + +``` +validate 校验 plugin.json 与入口程序集 +pack [-o ] [--no-mask] [-k ] +info 打印容器头、签名状态与清单 +``` + +## 三个设计决定 + +**为什么是独立进程,不是 MSBuild 任务。** VS 的 MSBuild 跑在 .NET Framework 上,而本仓库是 +net11.0。做成 `` 的话,插件作者在 VS 里一按生成就会因为加载不了任务程序集而失败 +—— 而清单校验是 `AfterTargets="Build"` 的,每次生成都跑。`dotnet exec` 一个独立进程与调用方 +的 MSBuild 是哪种运行时完全无关。 + +**为什么在本仓库,不在 velashell-plugin-cli。** 打包器需要的东西——`VpxContainer`(容器格式) +与 `PluginManifestReader`(清单规则)——本来就在 `VelaShell.PluginSdk` 里,它们才是 `.vpx` 的 +定义。放在这里,`.Build` 发出去的打包器与它发出去的契约天然同版本,两个仓库之间不需要任何 +版本对齐。 + +**为什么命令面只有三条。** 面向人的完整工具是 +[`vela-plugin`](https://github.com/VelaShellLabs/velashell-plugin-cli)(商店、开发内环、 +签名、体检)。这里只留 targets 真正会调的那两条,外加冒烟用来把包读回来的 `info`。 +多一条命令就多一份要与那边对齐的东西 —— 而两边一致性的根据是它们走同一个 `VpxContainer`, +不是命令面长得像。 diff --git a/src/VelaShell.PluginSdk.Packer/VelaShell.PluginSdk.Packer.csproj b/src/VelaShell.PluginSdk.Packer/VelaShell.PluginSdk.Packer.csproj new file mode 100644 index 0000000..84c078e --- /dev/null +++ b/src/VelaShell.PluginSdk.Packer/VelaShell.PluginSdk.Packer.csproj @@ -0,0 +1,38 @@ + + + + Exe + VelaShell.PluginSdk.Packer + VelaShell.PluginSdk.Packer + + + false + false + true + + + false + false + + + + + + + diff --git a/tests/smoke/DemoPanel.axaml b/tests/smoke/DemoPanel.axaml new file mode 100644 index 0000000..9f1ac00 --- /dev/null +++ b/tests/smoke/DemoPanel.axaml @@ -0,0 +1,24 @@ + + + + + + +