diff --git a/.github/workflows/compile-msix.yml b/.github/workflows/compile-msix.yml new file mode 100644 index 00000000..25475084 --- /dev/null +++ b/.github/workflows/compile-msix.yml @@ -0,0 +1,58 @@ +name: compile-msix + +on: + release: + types: [released] + +permissions: + contents: write + id-token: write + packages: write + +jobs: + compile-msix: + name: Build MSIX bundle and publish + runs-on: windows-latest + defaults: + run: + working-directory: packaging/msix + steps: + - name: Checkout version-fox + uses: actions/checkout@v7 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache: true + - name: Decode signing certificate + env: + MSIX_PFX_BASE64: ${{ secrets.MSIX_PFX_BASE64 }} + shell: pwsh + run: | + if ([string]::IsNullOrEmpty($env:MSIX_PFX_BASE64)) { + Write-Host "MSIX_PFX_BASE64 not configured; the bundle will be published unsigned." + exit 0 + } + [IO.File]::WriteAllBytes((Join-Path (Get-Location) "signing.pfx"), [Convert]::FromBase64String($env:MSIX_PFX_BASE64)) + # No inputs: make-msix.ps1 reads the version from internal/version.go + # and builds every architecture from source itself. + - name: Build MSIX bundle + env: + MSIX_SIGN_PFX_PASSWORD: ${{ secrets.MSIX_PFX_PASSWORD }} + MSIX_TIMESTAMP_URL: ${{ vars.MSIX_TIMESTAMP_URL }} + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $tagVersion = "${{ github.event.release.tag_name }}" -replace '^v', '' + $sourceVersion = (Select-String -Path "../../internal/version.go" -Pattern 'RuntimeVersion\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value + if ($tagVersion -ne $sourceVersion) { + throw "Version mismatch: release tag is '$tagVersion' but internal/version.go says '$sourceVersion'." + } + ./make-msix.ps1 + - name: Upload MSIX Assets + uses: version-fox/vfox-release-assets@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + release_id: ${{ github.event.release.id }} + assets_path: packaging/msix/Output/*.msixbundle diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 602ad70b..93f4f2be 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1,6 +1,17 @@ name: vfox E2E Test -on: [push] +on: + push: + pull_request: + paths: + - 'packaging/msix/**' + - 'scripts/e2e-msix-test.ps1' + - 'scripts/testdata/msix-probe/**' + - 'scripts/testdata/msix-runner/**' + - 'cmd/commands/upgrade*' + - '.github/workflows/e2e-tests.yml' + - '.github/workflows/compile-msix.yml' + workflow_dispatch: jobs: test-windows: @@ -47,6 +58,37 @@ jobs: name: test-results-windows-${{ matrix.go-version }} path: ${{ runner.temp }}\.vfox + test-msix-windows: + name: Test MSIX packaging on Windows + runs-on: windows-latest + timeout-minutes: 20 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v7 + - name: Setup Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + - name: Check Go installation + run: go version + # Build, pack, sign, install and verify the .msixbundle end-to-end + - name: Run MSIX packaging integration tests + shell: pwsh + run: | + cd ${{ github.workspace }} + $ErrorActionPreference = "Stop" + & .\scripts\e2e-msix-test.ps1 + env: + VFOX_HOME: ${{ runner.temp }}\.vfox + - name: Upload test results + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-results-msix-windows + path: msix-e2e-out + test-unix: name: Test on Unix runs-on: ${{ matrix.os }} @@ -89,7 +131,7 @@ jobs: test-summary: name: Test Summary runs-on: ubuntu-latest - needs: [test-windows, test-unix] + needs: [test-windows, test-unix, test-msix-windows] if: always() steps: @@ -101,14 +143,13 @@ jobs: echo "E2E Tests Summary" echo "================" echo "Windows tests: $(ls -d test-results-windows-* 2>/dev/null | wc -l) platform(s)" + echo "MSIX packaging tests: $(ls -d test-results-msix-windows-* 2>/dev/null | wc -l) platform(s)" echo "Unix tests: $(ls -d test-results-* 2>/dev/null | grep -v windows | wc -l) platform(s)" echo "" echo "All test artifacts have been uploaded." - name: Check test results - if: failure() + if: needs.test-windows.result != 'success' || needs.test-unix.result != 'success' || needs.test-msix-windows.result != 'success' run: | echo "Some tests failed. Please check the artifacts above." exit 1 - - diff --git a/.gitignore b/.gitignore index 9c9ca916..d8f7e415 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,11 @@ coverage.out .vfox .gocache + +# MSIX packaging artifacts +packaging/msix/Output/ +packaging/msix/staging/ +packaging/msix/assets/ +packaging/msix/build/ +packaging/msix/signing.pfx +/msix-e2e-out/ diff --git a/AGENTS.md b/AGENTS.md index 9823ed73..886bcd48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,7 @@ go test ./... -coverprofile=coverage.out -covermode=atomic ## E2E and release workflows - [scripts/e2e-test.sh](scripts/e2e-test.sh) and [scripts/e2e-test.ps1](scripts/e2e-test.ps1) install SDKs, change vfox state, and clean installation or user directories. Run them only in a disposable environment such as a CI runner or VM. Setting `VFOX_HOME` alone does not isolate the Unix script from the real user's vfox directory; Windows global use also writes user environment settings to the registry. +- [scripts/e2e-msix-test.ps1](scripts/e2e-msix-test.ps1) also requires a disposable Windows runner or VM. It creates a temporary trusted certificate, installs and upgrades MSIX bundles, and verifies global SDK selections through the host registry. It restores the fixture's registry values and removes its packages and certificate on exit. - `./scripts/bump.sh ` changes the runtime version, stages it, commits, and creates a local tag. Use it for requested versioning work with the staging state reviewed; it does not push. - `goreleaser release` runs the release workflow, including a GitHub draft release and configured distribution updates. Use it for requested release work. See [.goreleaser.yaml](.goreleaser.yaml) and [.github/workflows/go-releaser.yml](.github/workflows/go-releaser.yml). @@ -133,3 +134,4 @@ java = { version = "21", vendor = "openjdk" } | Shell initialization and exports | [shell/](internal/shell/) | | vfox settings | [config/](internal/config/) | | Shared utilities | [shared/](internal/shared/) | +| Windows MSIX packaging | [packaging/msix/](packaging/msix/) (`AppxManifest.xml`, `make-msix.ps1`, `gen-assets.ps1`; docs at [msix.md](docs/guides/msix.md); release via [compile-msix.yml](.github/workflows/compile-msix.yml), e2e via [e2e-msix-test.ps1](scripts/e2e-msix-test.ps1)) | diff --git a/cmd/commands/upgrade.go b/cmd/commands/upgrade.go index 0c95d2c0..e4114b1e 100644 --- a/cmd/commands/upgrade.go +++ b/cmd/commands/upgrade.go @@ -112,6 +112,15 @@ func downloadFile(c *http.Client, filepath string, url string) error { } func upgradeCmd(ctx context.Context, cmd *cli.Command) error { + exePath, err := os.Executable() + if err != nil { + return cli.Exit("Failed to get executable path: "+err.Error(), 1) + } + // An MSIX-installed vfox lives in the read-only WindowsApps directory; + // self-upgrading cannot replace the binary there. + if runtime.GOOS == "windows" && strings.Contains(strings.ToLower(exePath), `\windowsapps\`) { + return cli.Exit("vfox was installed as an MSIX package and cannot self-upgrade. Install a newer .msixbundle to upgrade.", 1) + } manager, err := internal.NewSdkManager() if err != nil { return err @@ -132,10 +141,6 @@ func upgradeCmd(ctx context.Context, cmd *cli.Command) error { if err = RequestPermission(); err != nil { return err } - exePath, err := os.Executable() - if err != nil { - return cli.Exit("Failed to get executable path: "+err.Error(), 1) - } exeDir, exeName := filepath.Split(exePath) binURL, diffURL := generateUrls(currVersion, latestVersion) tempFile := "vfox_latest.tar.gz" diff --git a/docs/.vitepress/en.ts b/docs/.vitepress/en.ts index 33007a6b..bc207910 100644 --- a/docs/.vitepress/en.ts +++ b/docs/.vitepress/en.ts @@ -94,6 +94,7 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'Misc', items: [ + {text: 'MSIX Bundle', link: '/guides/msix'}, ] }, ] diff --git a/docs/.vitepress/zh.ts b/docs/.vitepress/zh.ts index 31442fc3..98327c69 100644 --- a/docs/.vitepress/zh.ts +++ b/docs/.vitepress/zh.ts @@ -102,6 +102,7 @@ function sidebar(): DefaultTheme.Sidebar { { text: '其他', items: [ + {text: 'MSIX 安装包', link: '/zh-hans/guides/msix'}, ] }, ] diff --git a/docs/guides/msix.md b/docs/guides/msix.md new file mode 100644 index 00000000..e7acd15f --- /dev/null +++ b/docs/guides/msix.md @@ -0,0 +1,77 @@ +# MSIX Bundle + +The Windows assets on the [Releases](https://github.com/version-fox/vfox/releases) page include a `.msixbundle` package in addition to the setup `.exe`. MSIX is Microsoft's current application deployment format, intended to replace `.msi`/`.exe` installers. A bundle contains builds for `x86`, `x64` and `arm64`; Windows selects the matching architecture at install time. + +## Requirements + +- Windows 10 (1809+) or Windows Server 2025+ +- Optional: [App Installer](https://learn.microsoft.com/windows/msix/app-installer/installing-apps-pkg) for double-click installation + +## Installation + +Download the latest `vfox__windows.msixbundle` from the [Releases](https://github.com/version-fox/vfox/releases) page and install it by double-click, or from PowerShell: + +```powershell +Add-AppxPackage -Path .\vfox__windows.msixbundle +``` + +The package registers an app execution alias for `vfox.exe`, so the `vfox` command is available on PATH after installation without further configuration. + +::: warning ⚠️ Release bundles are currently unsigned +Windows refuses to install unsigned packages. Re-sign the bundle with your own certificate before installing (see below). +::: + +### Signing an unsigned bundle + +Windows must trust the signing certificate on the target machine. Keep the package publisher unchanged across upgrades, and reuse a long-lived certificate to avoid importing a new certificate for each release. Trusting that certificate is a one-time operation; each unsigned bundle you download must still be signed before installation. Include a trusted timestamp so the signed bundle remains installable after the signing certificate expires. + +First create a self-signed code-signing certificate whose subject matches the package publisher (`CN=VersionFox`) and import it into the trusted store. Importing into `Cert:\LocalMachine\TrustedPeople` requires an elevated (administrator) PowerShell session: + +```powershell +$cert = New-SelfSignedCertificate -Type Custom -Subject "CN=VersionFox" ` + -KeyUsage DigitalSignature -CertStoreLocation "Cert:\CurrentUser\My" ` + -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") +$password = ConvertTo-SecureString -String "pick-a-password" -Force -AsPlainText +Export-PfxCertificate -Cert $cert -FilePath vfox.pfx -Password $password | Out-Null +Import-PfxCertificate -FilePath vfox.pfx -CertStoreLocation Cert:\LocalMachine\TrustedPeople -Password $password +``` + +Then sign the bundle and install it ([signtool](https://learn.microsoft.com/windows/win32/seccrypto/signtool) comes with the Windows SDK): + +```powershell +signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f vfox.pfx /p pick-a-password .\vfox__windows.msixbundle +Add-AppxPackage -Path .\vfox__windows.msixbundle +``` + +## Uninstallation + +```powershell +Get-AppxPackage *vfox* | Remove-AppxPackage +``` + +Alternatively open **Settings** > **Apps** > **Installed apps**, select **vfox** and click **Uninstall**. Uninstalling removes the execution alias together with the package; the `~/.vfox` data directory is kept. + +## Notes for maintainers + +Release bundles are built by the `compile-msix` workflow on every release. Signing is optional; two options: + +- **Self-signed certificate (no external account needed).** Generate one PFX and keep it stable across releases, then configure the repository secrets `MSIX_PFX_BASE64` (Base64-encoded certificate) and `MSIX_PFX_PASSWORD`; the subject must match the manifest publisher (`CN=VersionFox`). Releases are signed automatically from then on. Publish the public `.cer` file alongside the releases so users only need to import it once. Keep the publisher identity stable when renewing the certificate, and distribute the new public certificate if it also needs to be trusted. +- **Publicly trusted certificate.** Packages signed by a CA-issued code-signing certificate or through [Azure Trusted Signing](https://learn.microsoft.com/azure/trusted-signing/overview) install without any user-side trust configuration. Obtaining one involves identity verification, and OV/EV certificates mandate hardware-protected private keys, which cannot be placed in CI secrets. + +Signed builds use an RFC 3161 timestamp from `http://timestamp.digicert.com`; set `MSIX_TIMESTAMP_URL` to use another timestamp service. The build fails if signing or timestamping fails. Both the architecture packages and the outer bundle use the source release version. + +All packaging files reside in the [`packaging/msix/`](https://github.com/version-fox/vfox/tree/main/packaging/msix) directory: + +| File | Purpose | +|------|---------| +| `AppxManifest.xml` | Manifest template; `@@VERSION@@`, `@@ARCHITECTURE@@` and `@@PUBLISHER@@` are substituted at build time. | +| `gen-assets.ps1` | Generates the tile icons from the repository logo (`logo.png`) via System.Drawing: transparent margins are cropped and the artwork is scaled onto a transparent canvas. PNG is required by AppX deployment (SVG is rejected), so these binaries are generated at build time instead of being committed. | +| `make-msix.ps1` | Builds `vfox` from source with Go, renders the manifest for each architecture, packs them with `MakeAppx.exe`, combines the packages into a `.msixbundle` and optionally signs it with `SignTool.exe`. | + +Local packaging requires Windows, the [Windows SDK](https://developer.microsoft.com/windows/downloads/windows-sdk/) (provides `MakeAppx.exe` / `SignTool.exe`) and [Go](https://go.dev/dl/). The script takes no parameters: it reads the version from `internal/version.go`, so a locally built bundle always matches the source tree: + +```powershell +./packaging/msix/make-msix.ps1 +``` + +Artifacts are written to `packaging/msix/Output/`; `assets/`, `staging/` and `build/` are scratch directories and gitignored. Only stable versions without prerelease or build metadata are supported; they are normalized to four-part versions (`1.2.3` → `1.2.3.0`). diff --git a/docs/guides/quick-start.md b/docs/guides/quick-start.md index e588c318..e16c5886 100644 --- a/docs/guides/quick-start.md +++ b/docs/guides/quick-start.md @@ -25,6 +25,21 @@ winget install vfox Go to the [Releases](https://github.com/version-fox/vfox/releases) page to download the latest version of the `setup` installer, then follow the installation wizard to install. + + + +Go to the [Releases](https://github.com/version-fox/vfox/releases) page to download the latest version of the `.msixbundle` package, then install it with [App Installer](https://learn.microsoft.com/windows/msix/app-installer/installing-apps-pkg) or PowerShell: + +```powershell +Add-AppxPackage -Path .\vfox__windows.msixbundle +``` + +The package registers an [app execution alias](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) for `vfox.exe`, so the `vfox` command is available on PATH after installation without further configuration. Requires Windows 10 (1809+) or Windows Server 2025+. + +::: warning ⚠️ Unsigned Packages +Release `.msixbundle` artifacts are currently unsigned, and Windows blocks installation of unsigned packages. Re-sign the bundle with your own certificate first; see [MSIX Bundle](./msix.md) for the detailed steps. +::: + diff --git a/docs/guides/uninstallation.md b/docs/guides/uninstallation.md index d18c252c..87a6f160 100644 --- a/docs/guides/uninstallation.md +++ b/docs/guides/uninstallation.md @@ -131,6 +131,16 @@ winget uninstall vfox ::: +::: details MSIX Bundle + +```powershell +Get-AppxPackage *vfox* | Remove-AppxPackage +``` + +Alternatively open **Settings** > **Apps** > **Installed apps**, select **vfox** and click **Uninstall**. Uninstalling also removes the `vfox` app execution alias. + +::: + ::: details Manual Installation 1. Delete the directory where you extracted vfox diff --git a/docs/zh-hans/guides/msix.md b/docs/zh-hans/guides/msix.md new file mode 100644 index 00000000..2279dafc --- /dev/null +++ b/docs/zh-hans/guides/msix.md @@ -0,0 +1,77 @@ +# MSIX 安装包 + +[Releases](https://github.com/version-fox/vfox/releases) 页面的 Windows 资产中,除 setup `.exe` 安装器外还提供 `.msixbundle` 安装包。MSIX 是微软当前的应用部署格式,用于取代 `.msi`/`.exe` 安装器。一个 bundle 内含 `x86`、`x64`、`arm64` 三种架构的构建,安装时由 Windows 自动选择匹配的架构。 + +## 环境要求 + +- Windows 10(1809+)或 Windows Server 2025+ +- 可选:[应用安装程序](https://learn.microsoft.com/windows/msix/app-installer/installing-apps-pkg)(用于双击安装) + +## 安装 + +从 [Releases](https://github.com/version-fox/vfox/releases) 页面下载最新版本的 `vfox__windows.msixbundle`,双击安装,或在 PowerShell 中执行: + +```powershell +Add-AppxPackage -Path .\vfox__windows.msixbundle +``` + +安装包会注册 `vfox.exe` 的应用执行别名,因此安装完成后 `vfox` 命令即可直接使用,无需手动配置 PATH。 + +::: warning ⚠️ 当前发布的安装包未签名 +Windows 会拒绝安装未签名包。安装前请先使用自己的证书对安装包重新签名(方法见下文)。 +::: + +### 为未签名安装包签名 + +Windows 必须信任安装包的签名证书。升级时需要保持包发布者一致,复用一张长期持有的证书可以避免每次发布都重新导入证书。信任证书只需操作一次,但每次下载的未签名 bundle 仍需分别签名后才能安装。签名时应加入可信时间戳,让安装包在签名证书到期后仍可安装。 + +首次操作需创建一张主题与包发布者(`CN=VersionFox`)一致的自签名代码签名证书,并导入本机受信任存储。导入 `Cert:\LocalMachine\TrustedPeople` 需要以管理员身份运行 PowerShell: + +```powershell +$cert = New-SelfSignedCertificate -Type Custom -Subject "CN=VersionFox" ` + -KeyUsage DigitalSignature -CertStoreLocation "Cert:\CurrentUser\My" ` + -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") +$password = ConvertTo-SecureString -String "pick-a-password" -Force -AsPlainText +Export-PfxCertificate -Cert $cert -FilePath vfox.pfx -Password $password | Out-Null +Import-PfxCertificate -FilePath vfox.pfx -CertStoreLocation Cert:\LocalMachine\TrustedPeople -Password $password +``` + +随后对安装包签名并安装([signtool](https://learn.microsoft.com/windows/win32/seccrypto/signtool) 随 Windows SDK 提供): + +```powershell +signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f vfox.pfx /p pick-a-password .\vfox__windows.msixbundle +Add-AppxPackage -Path .\vfox__windows.msixbundle +``` + +## 卸载 + +```powershell +Get-AppxPackage *vfox* | Remove-AppxPackage +``` + +也可以在 **设置** > **应用** > **已安装的应用** 中选择 **vfox** 并点击 **卸载**。卸载会同时移除应用执行别名;`~/.vfox` 数据目录会被保留。 + +## 维护者须知 + +发布版 bundle 由 `compile-msix` 工作流在每次 release 时构建。签名可选,有两种方式: + +- **自签证书(无需外部账号)**:生成一张 PFX 证书并在各次发布间保持不变,然后配置仓库 secrets:`MSIX_PFX_BASE64`(Base64 编码的证书文件)与 `MSIX_PFX_PASSWORD`;证书主题必须与清单中的发布者一致(`CN=VersionFox`)。此后每次发布都会自动签名。同时将公开证书(`.cer` 文件)随 release 一并分发,用户只需导入一次。续签证书时应保持发布者标识不变;如果新证书也需要手动信任,应一并分发新的公开证书。 +- **受信 CA 证书**:由公共 CA 签发的代码签名证书或 [Azure Trusted Signing](https://learn.microsoft.com/azure/trusted-signing/overview) 服务签名的包,用户机器默认信任,安装时无需任何额外配置。但证书申请需要实名验证,OV/EV 证书的私钥按规定必须保存在硬件介质中,无法放入 CI secrets。 + +启用签名后,构建会从 `http://timestamp.digicert.com` 获取 RFC 3161 时间戳;可通过 `MSIX_TIMESTAMP_URL` 指定其他时间戳服务。签名或时间戳操作失败会终止构建。各架构安装包和外层 bundle 均使用源码中的发布版本号。 + +打包相关文件位于仓库的 [`packaging/msix/`](https://github.com/version-fox/vfox/tree/main/packaging/msix) 目录: + +| 文件 | 用途 | +|------|------| +| `AppxManifest.xml` | 清单模板;`@@VERSION@@`、`@@ARCHITECTURE@@`、`@@PUBLISHER@@` 在构建时替换。 | +| `gen-assets.ps1` | 通过 System.Drawing 从仓库 logo(`logo.png`)生成磁贴图标:裁剪透明边距并将图案缩放至透明画布。PNG 是 AppX 部署的强制要求(不支持 SVG),因此这些二进制文件不入库,在每次构建时重新生成。 | +| `make-msix.ps1` | 使用 Go 从源码构建 `vfox`,为每种架构渲染清单,经 `MakeAppx.exe` 打包后合并为 `.msixbundle`,并可选通过 `SignTool.exe` 签名。 | + +本地打包需要 Windows、[Windows SDK](https://developer.microsoft.com/windows/downloads/windows-sdk/)(提供 `MakeAppx.exe` / `SignTool.exe`)与 [Go](https://go.dev/dl/)。脚本不接受参数:版本号从 `internal/version.go` 读取,因此本地构建的产物版本始终与源码树一致: + +```powershell +./packaging/msix/make-msix.ps1 +``` + +产物输出到 `packaging/msix/Output/`;`assets/`、`staging/`、`build/` 为临时目录,均已 gitignore。仅支持不含预发布或构建元数据后缀的正式版本,并会归一化为四段版本号(`1.2.3` → `1.2.3.0`)。 diff --git a/docs/zh-hans/guides/quick-start.md b/docs/zh-hans/guides/quick-start.md index 3d074087..143ff2a1 100644 --- a/docs/zh-hans/guides/quick-start.md +++ b/docs/zh-hans/guides/quick-start.md @@ -25,6 +25,21 @@ winget install vfox 前往 [Releases](https://github.com/version-fox/vfox/releases) 页面下载最新版本的 `setup` 安装器,然后按照安装向导进行安装。 + + + +前往 [Releases](https://github.com/version-fox/vfox/releases) 页面下载最新版本的 `.msixbundle` 安装包,然后通过[应用安装程序](https://learn.microsoft.com/windows/msix/app-installer/installing-apps-pkg)或 PowerShell 进行安装: + +```powershell +Add-AppxPackage -Path .\vfox__windows.msixbundle +``` + +安装包会为 `vfox.exe` 注册[应用执行别名](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions),因此安装完成后 `vfox` 命令即可直接使用,无需手动配置 PATH。要求 Windows 10(1809+)或 Windows Server 2025 及以上版本。 + +::: warning ⚠️ 关于未签名包 +目前发布的 `.msixbundle` 安装包是未签名的,Windows 会拒绝安装未签名包。请先使用自己的证书对安装包重新签名,详细步骤参见 [MSIX 安装包](./msix.md)。 +::: + diff --git a/docs/zh-hans/guides/uninstallation.md b/docs/zh-hans/guides/uninstallation.md index 1d5edd32..3bd19943 100644 --- a/docs/zh-hans/guides/uninstallation.md +++ b/docs/zh-hans/guides/uninstallation.md @@ -131,6 +131,16 @@ winget uninstall vfox ::: +::: details MSIX 安装包 + +```powershell +Get-AppxPackage *vfox* | Remove-AppxPackage +``` + +也可以在 **设置** > **应用** > **已安装的应用** 中选择 **vfox** 并点击 **卸载**。卸载会同时移除 `vfox` 的应用执行别名。 + +::: + ::: details 手动安装 1. 删除您解压 vfox 的目录 diff --git a/packaging/msix/AppxManifest.xml b/packaging/msix/AppxManifest.xml new file mode 100644 index 00000000..9cebef6b --- /dev/null +++ b/packaging/msix/AppxManifest.xml @@ -0,0 +1,70 @@ + + + + + + + + vfox + VersionFox + assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/msix/gen-assets.ps1 b/packaging/msix/gen-assets.ps1 new file mode 100644 index 00000000..51cf9ab8 --- /dev/null +++ b/packaging/msix/gen-assets.ps1 @@ -0,0 +1,129 @@ +#!/usr/bin/env pwsh +# +# Copyright 2026 Han Li and contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +<# +.SYNOPSIS + Generates the MSIX tile icons (msix/assets) from the repository logo. + +.DESCRIPTION + The AppxManifest requires raster PNG logos (SVG is rejected by AppX + deployment), but those binaries are not committed: msix/make-msix.ps1 runs + this script right before packing, so the icons are always derived from the + canonical logo at build time. Transparent margins are cropped away and the + artwork is scaled (high-quality bicubic) onto a transparent square canvas. + + Requires Windows (System.Drawing). + +.PARAMETER LogoPath + Path to the source logo PNG. + +.PARAMETER OutDir + Output directory for the generated icons. +#> + +param( + [Parameter(Mandatory = $true)] + [string]$LogoPath, + + [Parameter(Mandatory = $true)] + [string]$OutDir +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +Add-Type -AssemblyName System.Drawing + +$targets = @( + @{ Name = "Square44x44Logo.png"; Size = 44 }, + @{ Name = "Square150x150Logo.png"; Size = 150 }, + @{ Name = "StoreLogo.png"; Size = 50 } +) + +$src = [System.Drawing.Bitmap]::new((Resolve-Path $LogoPath).Path) +try { + # Locate the artwork bounding box via the alpha channel (LockBits: fast). + $bounds = [System.Drawing.Rectangle]::new(0, 0, $src.Width, $src.Height) + $data = $src.LockBits($bounds, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) + try { + $minX, $minY, $maxX, $maxY = $src.Width, $src.Height, -1, -1 + $bytes = New-Object byte[] ($data.Stride * $src.Height) + [System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $bytes, 0, $bytes.Length) + for ($y = 0; $y -lt $src.Height; $y++) { + $rowBase = $y * $data.Stride + for ($x = 0; $x -lt $src.Width; $x++) { + if ($bytes[$rowBase + $x * 4 + 3] -gt 8) { # BGRA: alpha at +3 + if ($x -lt $minX) { $minX = $x } + if ($y -lt $minY) { $minY = $y } + if ($x -gt $maxX) { $maxX = $x } + if ($y -gt $maxY) { $maxY = $y } + } + } + } + } + finally { + $src.UnlockBits($data) + } + + if ($maxX -lt 0) { + $crop = $bounds + } + else { + $crop = [System.Drawing.Rectangle]::new($minX, $minY, $maxX - $minX + 1, $maxY - $minY + 1) + } + Write-Host "source $LogoPath`: canvas $($src.Width)x$($src.Height), content $($crop.Width)x$($crop.Height)" + + New-Item -ItemType Directory -Path $OutDir -Force | Out-Null + + foreach ($target in $targets) { + $size = $target.Size + $dst = [System.Drawing.Bitmap]::new($size, $size) + try { + $graphics = [System.Drawing.Graphics]::FromImage($dst) + try { + $graphics.Clear([System.Drawing.Color]::Transparent) + $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic + $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias + $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality + + $scale = [Math]::Min($size / $crop.Width, $size / $crop.Height) + $width = [Math]::Max(1, [int][Math]::Round($crop.Width * $scale)) + $height = [Math]::Max(1, [int][Math]::Round($crop.Height * $scale)) + $offsetX = [int](($size - $width) / 2) + $offsetY = [int](($size - $height) / 2) + + # TileFlipXY prevents translucent ghosting from bicubic edge sampling. + $attributes = [System.Drawing.Imaging.ImageAttributes]::new() + $attributes.SetWrapMode([System.Drawing.Drawing2D.WrapMode]::TileFlipXY) + + $destRect = [System.Drawing.Rectangle]::new($offsetX, $offsetY, $width, $height) + $graphics.DrawImage($src, $destRect, $crop.X, $crop.Y, $crop.Width, $crop.Height, [System.Drawing.GraphicsUnit]::Pixel, $attributes) + } + finally { + $graphics.Dispose() + } + $outPath = Join-Path $OutDir $target.Name + $dst.Save($outPath, [System.Drawing.Imaging.ImageFormat]::Png) + Write-Host "wrote $outPath ($size x $size)" + } + finally { + $dst.Dispose() + } + } +} +finally { + $src.Dispose() +} diff --git a/packaging/msix/make-msix.ps1 b/packaging/msix/make-msix.ps1 new file mode 100644 index 00000000..8cbf2c2b --- /dev/null +++ b/packaging/msix/make-msix.ps1 @@ -0,0 +1,249 @@ +#!/usr/bin/env pwsh +# +# Copyright 2026 Han Li and contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +<# +.SYNOPSIS + Builds vfox from source, packs it into a .msixbundle and optionally signs it. + +.DESCRIPTION + Requires Windows with the Windows SDK (MakeAppx.exe / SignTool.exe) and Go. + Takes no parameters. The version is read from internal/version.go (the + single source of truth) and normalized to a four-part MSIX version + (1.2.3 -> 1.2.3.0); prerelease or build metadata segments are rejected. + Icons are generated from the repository logo, each architecture is built + from source and packed into its own .msix rendered from AppxManifest.xml, + and all packages are combined into vfox__windows.msixbundle. + The publisher is fixed to "CN=VersionFox". Signing is optional and + configured through the environment only: + MSIX_SIGN_PFX_PATH (falls back to