Skip to content
Open
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
56 changes: 56 additions & 0 deletions .github/workflows/compile-msix.yml
Original file line number Diff line number Diff line change
@@ -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
31 changes: 30 additions & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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:
Expand All @@ -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."
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions cmd/commands/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ function sidebar(): DefaultTheme.Sidebar {
{
text: 'Misc',
items: [
{text: 'MSIX Bundle', link: '/guides/msix'},
]
},
]
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ function sidebar(): DefaultTheme.Sidebar {
{
text: '其他',
items: [
{text: 'MSIX 安装包', link: '/zh-hans/guides/msix'},
]
},
]
Expand Down
75 changes: 75 additions & 0 deletions docs/guides/msix.md
Original file line number Diff line number Diff line change
@@ -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_<version>_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_<version>_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_<version>_windows.msixbundle
Add-AppxPackage -Path .\vfox_<version>_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`).
Comment on lines +54 to +75

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is the best way to write the documentation, but surely the signature shouldn't be provided by me, right?

15 changes: 15 additions & 0 deletions docs/guides/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

</TabItem>
<TabItem label="MSIX Bundle">

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_<version>_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.
:::

</TabItem>
</Tabs>

Expand Down
10 changes: 10 additions & 0 deletions docs/guides/uninstallation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions docs/zh-hans/guides/msix.md
Original file line number Diff line number Diff line change
@@ -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_<version>_windows.msixbundle`,双击安装,或在 PowerShell 中执行:

```powershell
Add-AppxPackage -Path .\vfox_<version>_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_<version>_windows.msixbundle
Add-AppxPackage -Path .\vfox_<version>_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`)。
15 changes: 15 additions & 0 deletions docs/zh-hans/guides/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ winget install vfox

前往 [Releases](https://github.com/version-fox/vfox/releases) 页面下载最新版本的 `setup` 安装器,然后按照安装向导进行安装。

</TabItem>
<TabItem label="MSIX 安装包">

前往 [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_<version>_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)。
:::

</TabItem>
</Tabs>

Expand Down
Loading