diff --git a/.github/workflows/compile-msix.yml b/.github/workflows/compile-msix.yml new file mode 100644 index 00000000..3c4b59a2 --- /dev/null +++ b/.github/workflows/compile-msix.yml @@ -0,0 +1,56 @@ +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 -X86/-X64/-Arm64 inputs: make-msix.ps1 builds from source itself. + - name: Build MSIX bundle + env: + TAG_NAME: ${{ github.event.release.tag_name }} + MSIX_SIGN_PFX_PASSWORD: ${{ secrets.MSIX_PFX_PASSWORD }} + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $packArgs = @{ Version = ($env:TAG_NAME -replace '^v', '') } + if (Test-Path "./signing.pfx") { + $packArgs.SignPfxPath = (Resolve-Path "./signing.pfx").Path + } + ./make-msix.ps1 @packArgs + - 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 fd03c9ae..7ca032e3 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -47,6 +47,34 @@ 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 + steps: + - name: Checkout code + uses: actions/checkout@v7 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.24' + - 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 +117,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,6 +129,7 @@ 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." 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/.goreleaser.yaml b/.goreleaser.yaml index 7140da51..e863316b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -33,7 +33,7 @@ builds: flags: - -trimpath ldflags: - - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser -X main.treeState={{ .IsGitDirty }} + - -s -w -X github.com/version-fox/vfox/internal.RuntimeVersion={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser -X main.treeState={{ .IsGitDirty }} checksum: name_template: "checksums.txt" diff --git a/AGENTS.md b/AGENTS.md index 713d31b3..f9c13dcb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,6 +126,7 @@ sdk.EnvKeysForScope(version, env.Project) | **Paths** | `internal/pathmeta/path_meta.go` | | **Environment** | `internal/env/env.go` (scope-aware merging) | | **Config** | `internal/config/config.go` | +| **Windows MSIX packaging** | `packaging/msix/` (manifest template, `make-msix.ps1`, `gen-assets.ps1`; docs at `docs/guides/msix.md`; release via `compile-msix.yml`, e2e via `scripts/e2e-msix-test.ps1`) | ## KEY DEPENDENCIES - `github.com/urfave/cli/v3` - CLI framework 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 895efbd5..e71f33fe 100644 --- a/docs/.vitepress/en.ts +++ b/docs/.vitepress/en.ts @@ -91,6 +91,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 4bafcd88..e4d213c3 100644 --- a/docs/.vitepress/zh.ts +++ b/docs/.vitepress/zh.ts @@ -99,6 +99,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..27eef007 --- /dev/null +++ b/docs/guides/msix.md @@ -0,0 +1,75 @@ +# 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 + +The build cannot perform signing on behalf of users. At install time Windows verifies the signer against certificates trusted on the target machine, so a certificate generated during packaging is treated the same as no signature at all. In addition, MSIX requires an upgrade package to be signed with the same certificate as the installed one; per-build certificates would therefore break upgrades. Signing with a long-lived certificate of your own is a one-time operation: once the certificate is trusted, subsequent versions install without extra steps. + +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 /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 — because the signature stays identical across versions, upgrades are unaffected. +- **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. + +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/): + +```powershell +./packaging/msix/make-msix.ps1 -Version 1.2.3 +``` + +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..cbbc8186 --- /dev/null +++ b/docs/zh-hans/guides/msix.md @@ -0,0 +1,75 @@ +# 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 在安装时校验签名证书是否属于目标机器信任的证书,打包阶段生成的临时证书与不签名效果相同;此外 MSIX 要求升级包与已安装包使用同一证书签名,每次构建使用不同证书会导致无法覆盖升级。因此应使用一张长期持有的证书完成签名:证书受信任后,后续版本均可直接安装。 + +首次操作需创建一张主题与包发布者(`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 /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。 + +打包相关文件位于仓库的 [`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/): + +```powershell +./packaging/msix/make-msix.ps1 -Version 1.2.3 +``` + +产物输出到 `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/internal/version.go b/internal/version.go index ea4f155f..0adaa087 100644 --- a/internal/version.go +++ b/internal/version.go @@ -16,4 +16,6 @@ package internal -const RuntimeVersion = "1.0.11" +// RuntimeVersion is overridden at link time by the release and packaging +// pipelines (-X github.com/version-fox/vfox/internal.RuntimeVersion=). +var RuntimeVersion = "1.0.11" 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..181e9434 --- /dev/null +++ b/packaging/msix/make-msix.ps1 @@ -0,0 +1,241 @@ +#!/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. + 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. + Signing is optional: when -SignPfxPath is provided the bundle is signed + with SignTool; the certificate subject must exactly match -Publisher. + +.PARAMETER Version + Stable vfox version to encode in the manifest, e.g. "1.2.3". Prerelease + or build metadata segments are rejected; the value is normalized to a + four-part MSIX version (1.2.3 -> 1.2.3.0). + +.PARAMETER OutputDir / WorkDir + Artifact and scratch directories. Default to