docs(plan): 多平台打包实施计划
10 个任务覆盖: - tauri.conf.json 改造 + main.rs 平台条件 - TDD 平台资产选择器 + 单元测 - check_for_update / download_update 接入 - 版本一致性脚本 - GitHub Actions release workflow - Docker Linux 构建 - BUILD.md - README 三平台下载 + workflow 徽章 - E2E 验证 (pre-release tag)
This commit is contained in:
1 parent
00322fb503
commit
63ae664e72
1 file changed
+867
@@ -0,0 +1,867 @@
|
|||||||
|
# Kimi Switch Multi-Platform Packaging Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add macOS + Linux packaging support on top of existing Windows MSI; release driven by GitHub Actions tag-push workflow.
|
||||||
|
|
||||||
|
**Architecture:** Source-level changes are minimal (Tauri config tweaks + platform-conditional Rust + asset picker). Build infrastructure is the heavy lifting: one GH Actions workflow (version-check + 3-platform matrix using `tauri-action`) and a local Docker setup for Linux dev iteration.
|
||||||
|
|
||||||
|
**Tech Stack:** Tauri 2.x, Rust 2021, React 18 + TypeScript 5.5, GitHub Actions (`tauri-apps/tauri-action`), Docker (Ubuntu 22.04 + webkit2gtk-4.1), bash.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- macOS assets: `.app` + `.dmg`, **unsigned** (no Apple Developer ID provided)
|
||||||
|
- Linux assets: `.deb` + `.AppImage` + `.rpm` (all three)
|
||||||
|
- Windows assets: `.msi` (existing)
|
||||||
|
- Bundle target string in `tauri.conf.json`: `"all"` (allow per-platform build)
|
||||||
|
- macOS minimum system version: `10.15`
|
||||||
|
- Update checker picks asset by file extension: `.msi` / `.dmg` / `.AppImage`
|
||||||
|
- Temp filename for downloaded installer: `KimiSwitch_update.<ext>` (ext from OS)
|
||||||
|
- Version consistency: `git tag` must equal `package.json:version` = `Cargo.toml:version` = `tauri.conf.json:version`
|
||||||
|
- Cargo behavior: keep `tauri = "2.0.0"`, `tauri-plugin-opener = "2.0.0"`, `tauri-plugin-single-instance = "2.0.0"`
|
||||||
|
- No new lint failures; no new dependencies beyond what's already in `Cargo.toml`
|
||||||
|
- Spec location: `docs/superpowers/specs/2026-07-29-multi-platform-packaging-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Tauri config — bundle targets and macOS section
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src-tauri/tauri.conf.json`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: existing Windows-only config
|
||||||
|
- Produces: `bundle.targets = "all"`, `bundle.macOS.minimumSystemVersion`, expanded icon list
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update `tauri.conf.json`**
|
||||||
|
|
||||||
|
Replace the existing `bundle` block with:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.ico",
|
||||||
|
"icons/icon.icns"
|
||||||
|
],
|
||||||
|
"windows": {
|
||||||
|
"webviewInstallMode": {
|
||||||
|
"type": "downloadBootstrapper"
|
||||||
|
},
|
||||||
|
"nsis": null
|
||||||
|
},
|
||||||
|
"macOS": {
|
||||||
|
"minimumSystemVersion": "10.15"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"deb": {
|
||||||
|
"depends": []
|
||||||
|
},
|
||||||
|
"appimage": {
|
||||||
|
"bundleMediaFramework": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify JSON is valid**
|
||||||
|
|
||||||
|
Run: `node -e "JSON.parse(require('fs').readFileSync('src-tauri/tauri.conf.json','utf8'))"`
|
||||||
|
Expected: no output (silent success)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src-tauri/tauri.conf.json
|
||||||
|
git commit -m "feat(bundle): enable all-platform targets + macOS/Linux config"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Platform-conditional `windows_subsystem` in main.rs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src-tauri/src/main.rs:1`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: existing `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]`
|
||||||
|
- Produces: Windows-only windows_subsystem; macOS/Linux keep console subsystem so logs are visible
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the directive at line 1**
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
```rust
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
```
|
||||||
|
|
||||||
|
With:
|
||||||
|
```rust
|
||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify Windows build still compiles**
|
||||||
|
|
||||||
|
Run: `cd src-tauri && cargo check --message-format=short 2>&1 | tail -5`
|
||||||
|
Expected: `Finished \`dev\` profile [unoptimized + debuginfo] target(s)` with no errors
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify Linux target compiles (cross-check)**
|
||||||
|
|
||||||
|
Run: `cd src-tauri && cargo check --target x86_64-unknown-linux-gnu --message-format=short 2>&1 | tail -5`
|
||||||
|
Expected: finishes clean (may need `rustup target add x86_64-unknown-linux-gnu` first; if rustup target is missing, run that and retry)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src-tauri/src/main.rs
|
||||||
|
git commit -m "fix(build): only set windows_subsystem on Windows targets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: TDD — platform-aware asset picker
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src-tauri/src/commands.rs` (append at end)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces (public to module): `fn pick_asset_for_current_os(assets: &[serde_json::Value]) -> Option<String>`
|
||||||
|
- Pure function, no side effects; testable in isolation
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests**
|
||||||
|
|
||||||
|
Add to the end of `src-tauri/src/commands.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(test)]
|
||||||
|
mod asset_picker_tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
fn assets(names: &[&str]) -> Vec<serde_json::Value> {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.map(|n| {
|
||||||
|
json!({
|
||||||
|
"name": n,
|
||||||
|
"browser_download_url": format!("https://example.com/{n}"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_picks_msi() {
|
||||||
|
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi", "KimiSwitch_0.7.0_aarch64.dmg", "KimiSwitch_0.7.0_amd64.AppImage"]);
|
||||||
|
let url = pick_asset_for_current_os(&assets);
|
||||||
|
assert_eq!(url.as_deref(), Some("https://example.com/KimiSwitch_0.7.0_x64_en-US.msi"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn macos_picks_dmg() {
|
||||||
|
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi", "KimiSwitch_0.7.0_aarch64.dmg", "KimiSwitch_0.7.0_amd64.AppImage"]);
|
||||||
|
let url = pick_asset_for_current_os(&assets);
|
||||||
|
assert_eq!(url.as_deref(), Some("https://example.com/KimiSwitch_0.7.0_aarch64.dmg"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linux_picks_appimage() {
|
||||||
|
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi", "KimiSwitch_0.7.0_aarch64.dmg", "KimiSwitch_0.7.0_amd64.AppImage"]);
|
||||||
|
let url = pick_asset_for_current_os(&assets);
|
||||||
|
assert_eq!(url.as_deref(), Some("https://example.com/KimiSwitch_0.7.0_amd64.AppImage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_target_returns_none() {
|
||||||
|
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi"]);
|
||||||
|
let url = pick_asset_for_current_os(&assets);
|
||||||
|
assert_eq!(url, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_assets_returns_none() {
|
||||||
|
let url = pick_asset_for_current_os(&[]);
|
||||||
|
assert_eq!(url, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd src-tauri && cargo test asset_picker_tests --message-format=short 2>&1 | tail -10`
|
||||||
|
Expected: compile error `function \`pick_asset_for_current_os\` not found`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the function**
|
||||||
|
|
||||||
|
Add to `src-tauri/src/commands.rs` (above the test module):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Pick the GitHub release asset matching the current OS by file extension.
|
||||||
|
/// Preference for Linux is AppImage (portable, no install). If the preferred
|
||||||
|
/// extension is not present, returns None so the UI can fall back to
|
||||||
|
/// "open release page" instead of guessing a less-preferred format.
|
||||||
|
fn pick_asset_for_current_os(assets: &[serde_json::Value]) -> Option<String> {
|
||||||
|
let target_ext = match std::env::consts::OS {
|
||||||
|
"macos" => "dmg",
|
||||||
|
"linux" => "AppImage",
|
||||||
|
"windows" => "msi",
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| {
|
||||||
|
a.get("name")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|n| n.ends_with(target_ext))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.and_then(|a| a.get("browser_download_url"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the platform-specific temp filename for the downloaded installer.
|
||||||
|
fn update_temp_filename() -> String {
|
||||||
|
let ext = match std::env::consts::OS {
|
||||||
|
"macos" => "dmg",
|
||||||
|
"linux" => "AppImage",
|
||||||
|
"windows" => "msi",
|
||||||
|
_ => "bin",
|
||||||
|
};
|
||||||
|
format!("KimiSwitch_update.{ext}")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `cd src-tauri && cargo test asset_picker_tests --message-format=short 2>&1 | tail -10`
|
||||||
|
Expected: `test result: ok. 5 passed; 0 failed`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src-tauri/src/commands.rs
|
||||||
|
git commit -m "feat(update): TDD platform-aware asset picker + tests"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Wire asset picker into `check_for_update` and `download_update`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src-tauri/src/commands.rs` (the `check_for_update` and `download_update` functions)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `pick_asset_for_current_os` (Task 3) and `update_temp_filename` (Task 3)
|
||||||
|
- Produces: `check_for_update` returns `download_url` for the current OS only; `download_update` writes to a platform-specific filename
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the asset-selection block in `check_for_update`**
|
||||||
|
|
||||||
|
In `check_for_update`, find the existing block:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let download_url = first
|
||||||
|
.get("assets")
|
||||||
|
.and_then(|a| a.as_array())
|
||||||
|
.and_then(|a| a.first())
|
||||||
|
.and_then(|a| a.get("browser_download_url"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let download_url = first
|
||||||
|
.get("assets")
|
||||||
|
.and_then(|a| a.as_array())
|
||||||
|
.and_then(|a| pick_asset_for_current_os(a));
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace the temp filename in `download_update`**
|
||||||
|
|
||||||
|
In `download_update`, find the existing line:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let file_path = temp_dir.join("KimiSwitch_update.msi");
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let file_path = temp_dir.join(update_temp_filename());
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify on Windows**
|
||||||
|
|
||||||
|
Run: `cd src-tauri && cargo check --message-format=short 2>&1 | tail -5`
|
||||||
|
Expected: finishes clean
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the unit tests**
|
||||||
|
|
||||||
|
Run: `cd src-tauri && cargo test asset_picker_tests --message-format=short 2>&1 | tail -5`
|
||||||
|
Expected: `5 passed; 0 failed`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src-tauri/src/commands.rs
|
||||||
|
git commit -m "feat(update): use platform-aware asset and filename in update flow"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Version consistency check script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/check-version.sh`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Inputs: `expected_version` from CLI arg; reads `package.json`, `src-tauri/Cargo.toml`, `src-tauri/tauri.conf.json`
|
||||||
|
- Output: exit 0 if all match, exit 1 otherwise (with diff to stderr)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the script**
|
||||||
|
|
||||||
|
Create `scripts/check-version.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Verify that the version across the project matches the expected version.
|
||||||
|
# Usage: check-version.sh <expected>
|
||||||
|
# Reads:
|
||||||
|
# package.json : root
|
||||||
|
# src-tauri/Cargo.toml : version field
|
||||||
|
# src-tauri/tauri.conf.json : version field
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
EXPECTED="${1:-}"
|
||||||
|
|
||||||
|
if [ -z "$EXPECTED" ]; then
|
||||||
|
echo "usage: check-version.sh <expected>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strip leading 'v' for comparison (we expect semver without v)
|
||||||
|
EXPECTED_BARE="${EXPECTED#v}"
|
||||||
|
|
||||||
|
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
CARGO_VERSION=$(grep -E '^version\s*=' src-tauri/Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
TAURI_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
check() {
|
||||||
|
local label="$1"
|
||||||
|
local actual="$2"
|
||||||
|
if [ "$actual" != "$EXPECTED_BARE" ]; then
|
||||||
|
echo "FAIL: $label = $actual (expected $EXPECTED_BARE)" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check "package.json" "$PKG_VERSION"
|
||||||
|
check "src-tauri/Cargo.toml" "$CARGO_VERSION"
|
||||||
|
check "src-tauri/tauri.conf.json" "$TAURI_VERSION"
|
||||||
|
|
||||||
|
if [ $fail -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK: all versions = $EXPECTED_BARE"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Make it executable**
|
||||||
|
|
||||||
|
Run: `chmod +x scripts/check-version.sh`
|
||||||
|
Expected: no output
|
||||||
|
|
||||||
|
- [ ] **Step 3: Test happy path**
|
||||||
|
|
||||||
|
Run: `./scripts/check-version.sh v0.6.0`
|
||||||
|
Expected: `OK: all versions = 0.6.0`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Test mismatch**
|
||||||
|
|
||||||
|
Run: `./scripts/check-version.sh v9.9.9`
|
||||||
|
Expected: 3 lines starting with `FAIL:` and exit code 1
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/check-version.sh
|
||||||
|
git commit -m "ci: add version-consistency check script"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: GitHub Actions release workflow
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `.github/workflows/release.yml`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Trigger: `push: tags: ['v*']`
|
||||||
|
- Two jobs: `version-check` (sequential) and `build` (matrix ubuntu/macos/windows)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the workflow file**
|
||||||
|
|
||||||
|
Create `.github/workflows/release.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version-check:
|
||||||
|
name: Version consistency
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Verify tag matches package.json / Cargo.toml / tauri.conf.json
|
||||||
|
run: |
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
chmod +x scripts/check-version.sh
|
||||||
|
./scripts/check-version.sh "$TAG"
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build (${{ matrix.platform }})
|
||||||
|
needs: version-check
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: ubuntu-latest
|
||||||
|
args: --bundles deb,appimage,rpm
|
||||||
|
- platform: macos-latest
|
||||||
|
args: --bundles app,dmg
|
||||||
|
- platform: windows-latest
|
||||||
|
args: --bundles msi
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- uses: actions/setup-rust@v1
|
||||||
|
with:
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install Linux build dependencies
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
libappindicator3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
patchelf \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
file \
|
||||||
|
libssl-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libayatana-appindicator3-dev
|
||||||
|
|
||||||
|
- name: Build and upload release
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
with:
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: ${{ github.ref_name }}
|
||||||
|
releaseBody: |
|
||||||
|
## ${{ github.ref_name }}
|
||||||
|
|
||||||
|
### Downloads by platform
|
||||||
|
|
||||||
|
- **Windows**: `.msi` installer
|
||||||
|
- **macOS**: `.dmg` (unsigned — first launch needs right-click → Open) + `.app`
|
||||||
|
- **Linux**: `.deb` / `.AppImage` / `.rpm`
|
||||||
|
|
||||||
|
Full changelog: see commit history.
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }}
|
||||||
|
args: ${{ matrix.args }}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Sanity-check the file is in place**
|
||||||
|
|
||||||
|
Run: `ls -la .github/workflows/release.yml && head -5 .github/workflows/release.yml`
|
||||||
|
Expected: file exists and the first line is `name: Release` (GitHub Actions will catch any YAML syntax errors at runtime)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .github/workflows/release.yml
|
||||||
|
git commit -m "ci: add multi-platform release workflow on tag push"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Local Docker for Linux builds
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Dockerfile`
|
||||||
|
- Create: `docker-compose.yml`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Input: source tree at `/workspace`
|
||||||
|
- Output: built packages at `/workspace/src-tauri/target/release/bundle/{deb,appimage,rpm}/`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the Dockerfile**
|
||||||
|
|
||||||
|
Create `Dockerfile`:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Build Kimi Switch Linux packages (deb / AppImage / rpm) inside a clean
|
||||||
|
# Ubuntu 22.04 container. Used both locally (`docker compose run`) and as a
|
||||||
|
# reference for GH Actions' ubuntu-latest job.
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV CARGO_TERM_COLOR=always
|
||||||
|
ENV RUSTUP_HOME=/usr/local/rustup
|
||||||
|
ENV CARGO_HOME=/usr/local/cargo
|
||||||
|
ENV PATH=/usr/local/cargo/bin:$PATH
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
build-essential \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
libsoup-3.0-dev \
|
||||||
|
libjavascriptcoregtk-4.1-dev \
|
||||||
|
patchelf \
|
||||||
|
file \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
python3 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
||||||
|
|
||||||
|
RUN cargo install tauri-cli --version "^2.0" --locked
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
CMD ["/bin/bash"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create docker-compose.yml**
|
||||||
|
|
||||||
|
Create `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
build-linux:
|
||||||
|
build: .
|
||||||
|
container_name: kimiswitch-build-linux
|
||||||
|
volumes:
|
||||||
|
- .:/workspace
|
||||||
|
- cargo-cache:/usr/local/cargo/registry
|
||||||
|
- target-cache:/workspace/src-tauri/target
|
||||||
|
working_dir: /workspace
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cargo-cache:
|
||||||
|
target-cache:
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build the Docker image**
|
||||||
|
|
||||||
|
Run: `docker compose build build-linux 2>&1 | tail -10`
|
||||||
|
Expected: ends with `naming to docker.io/library/...build-linux done` (or similar successful build output)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add Dockerfile docker-compose.yml
|
||||||
|
git commit -m "ci: add Linux Docker build environment"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Build documentation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/BUILD.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Audience: contributors who want to build the project locally
|
||||||
|
- Output: instructions for Windows / macOS / Linux local builds + Docker Linux build
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the doc**
|
||||||
|
|
||||||
|
Create `docs/BUILD.md`:
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
# Build
|
||||||
|
|
||||||
|
Docker (Linux), native (any platform), and CI (multi-platform) build instructions.
|
||||||
|
|
||||||
|
## Local builds by host
|
||||||
|
|
||||||
|
### Windows (current primary dev environment)
|
||||||
|
|
||||||
|
Prerequisites: Rust stable, Node 20, Microsoft C++ Build Tools, WebView2.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run tauri build -- --bundles msi
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `src-tauri/target/release/bundle/msi/`.
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
Prerequisites: Xcode command-line tools, Rust stable, Node 20.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run tauri build -- --bundles app,dmg
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `src-tauri/target/release/bundle/{macos,dmg}/`.
|
||||||
|
|
||||||
|
Unsigned: first launch requires right-click → Open to bypass Gatekeeper.
|
||||||
|
|
||||||
|
### Linux (native)
|
||||||
|
|
||||||
|
Prerequisites: see `Dockerfile` for the full apt-get list (webkit2gtk-4.1, librsvg2, etc.).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run tauri build -- --bundles deb,appimage,rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `src-tauri/target/release/bundle/{deb,appimage,rpm}/`.
|
||||||
|
|
||||||
|
### Linux via Docker (works on Windows / macOS too)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build build-linux
|
||||||
|
docker compose run --rm build-linux bash -c '
|
||||||
|
npm ci &&
|
||||||
|
npm run tauri build -- --bundles deb,appimage,rpm
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
Output is mounted back to `src-tauri/target/release/bundle/` on the host.
|
||||||
|
|
||||||
|
## CI: GitHub Actions
|
||||||
|
|
||||||
|
`.github/workflows/release.yml` runs on `git tag v* && git push origin v*`.
|
||||||
|
|
||||||
|
1. `version-check` runs `scripts/check-version.sh` to ensure tag matches `package.json`, `Cargo.toml`, `tauri.conf.json`.
|
||||||
|
2. Three parallel jobs build for `ubuntu-latest`, `macos-latest`, `windows-latest` using `tauri-action`.
|
||||||
|
3. Artifacts are uploaded to the GitHub Release (auto-draft for non-prerelease tags).
|
||||||
|
|
||||||
|
To cut a release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bump version in three places
|
||||||
|
# package.json: "version": "0.7.0"
|
||||||
|
# src-tauri/Cargo.toml: version = "0.7.0"
|
||||||
|
# src-tauri/tauri.conf.json: "version": "0.7.0"
|
||||||
|
# 2. Commit
|
||||||
|
git commit -am "chore(release): bump to v0.7.0"
|
||||||
|
# 3. Tag and push
|
||||||
|
git tag v0.7.0
|
||||||
|
git push origin v0.7.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update checker asset selection
|
||||||
|
|
||||||
|
`src-tauri/src/commands.rs::pick_asset_for_current_os` picks the right asset by file extension:
|
||||||
|
|
||||||
|
| OS | Extension |
|
||||||
|
|---------|------------|
|
||||||
|
| Windows | `.msi` |
|
||||||
|
| macOS | `.dmg` |
|
||||||
|
| Linux | `.AppImage` (preferred; deb / rpm downloadable manually from the release page) |
|
||||||
|
|
||||||
|
If the preferred extension is missing, the UI falls back to opening the release page.
|
||||||
|
````
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/BUILD.md
|
||||||
|
git commit -m "docs: build instructions for all platforms + Docker + CI"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9: README — multi-platform download links + workflow badge
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `README.md`
|
||||||
|
- Modify: `README_EN.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Adds a "下载" / "Downloads" section with three platform bullets
|
||||||
|
- Adds a build-from-source link to `docs/BUILD.md`
|
||||||
|
- Adds a GitHub Actions badge
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the badges (after the existing license badge)**
|
||||||
|
|
||||||
|
In `README.md`, locate the line:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[](./LICENSE)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add two new badges below it:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[](https://github.com/billowliu2/KimiSwitch/releases/latest)
|
||||||
|
```
|
||||||
|
|
||||||
|
(Do the same in `README_EN.md`.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add a "下载" section right after "这是什么"**
|
||||||
|
|
||||||
|
In `README.md`, after the paragraph immediately above `## 核心特性`, insert:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 下载
|
||||||
|
|
||||||
|
- **Windows**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.msi`
|
||||||
|
- **macOS**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.dmg`(**未签名** —— 首次启动请右键 → 打开绕过 Gatekeeper)
|
||||||
|
- **Linux**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.deb` / `.AppImage` / `.rpm`
|
||||||
|
|
||||||
|
历史版本(v0.5.x 及之前)请前往 [git.codingplan.site 仓库](https://git.codingplan.site/admin/KimiCodeSwitch/releases)。
|
||||||
|
|
||||||
|
构建说明请见 [`docs/BUILD.md`](./docs/BUILD.md)。
|
||||||
|
```
|
||||||
|
|
||||||
|
In `README_EN.md`, insert the English equivalent:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Downloads
|
||||||
|
|
||||||
|
- **Windows**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.msi`
|
||||||
|
- **macOS**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.dmg` (**unsigned** — first launch needs right-click → Open to bypass Gatekeeper)
|
||||||
|
- **Linux**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.deb` / `.AppImage` / `.rpm`
|
||||||
|
|
||||||
|
Historical versions (v0.5.x and earlier) live on the [git.codingplan.site repository](https://git.codingplan.site/admin/KimiCodeSwitch/releases).
|
||||||
|
|
||||||
|
Build instructions: [`docs/BUILD.md`](./docs/BUILD.md).
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md README_EN.md
|
||||||
|
git commit -m "docs(readme): multi-platform downloads + workflow badge"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10: End-to-end test with a pre-release tag
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- (No file changes — pure verification)
|
||||||
|
|
||||||
|
**Goal:** Confirm the GH Actions workflow actually produces all 6 assets. Use a pre-release tag so it doesn't pollute the main `v0.6.0` release.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Confirm current version is in sync**
|
||||||
|
|
||||||
|
Run: `./scripts/check-version.sh v0.6.0`
|
||||||
|
Expected: `OK: all versions = 0.6.0`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Push all un-pushed commits**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Tag a pre-release on the current commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v0.6.0-rc1
|
||||||
|
git push origin v0.6.0-rc1
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Watch the workflow run**
|
||||||
|
|
||||||
|
Open: https://github.com/billowliu2/KimiSwitch/actions
|
||||||
|
|
||||||
|
Expected: `version-check` job passes, then 3 `build` jobs run in parallel. Total time roughly 10-15 minutes per platform.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify the pre-release**
|
||||||
|
|
||||||
|
Open: https://github.com/billowliu2/KimiSwitch/releases/tag/v0.6.0-rc1
|
||||||
|
|
||||||
|
Expected: 6 assets attached (`.msi`, `.dmg`, `.app`, `.deb`, `.AppImage`, `.rpm`); release marked as "Pre-release".
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify version mismatch fails the workflow**
|
||||||
|
|
||||||
|
On a clean branch, change only `package.json` version to `0.9.0`, commit, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v0.6.0-rc2
|
||||||
|
git push origin v0.6.0-rc2
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the new workflow run. Expected: `version-check` job fails with `FAIL: package.json = 0.9.0 (expected 0.6.0)`. Build jobs are skipped.
|
||||||
|
|
||||||
|
Then revert the package.json change and delete the rc2 tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main -- package.json
|
||||||
|
git tag -d v0.6.0-rc2
|
||||||
|
git push origin :refs/tags/v0.6.0-rc2
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Clean up the rc1 tag**
|
||||||
|
|
||||||
|
After verifying the rc1 assets are good, delete the pre-release to keep the release list clean:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -d v0.6.0-rc1
|
||||||
|
git push origin :refs/tags/v0.6.0-rc1
|
||||||
|
# Then delete the GitHub Release via the web UI (or leave it as a historical rc)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: Final commit — none if Tasks 1-9 are clean**
|
||||||
|
|
||||||
|
If no edits were needed, no commit. Otherwise, commit any fixups.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
After all 10 tasks, the project ships with:
|
||||||
|
- macOS + Linux + Windows builds produced from a single `git tag v0.7.0` push
|
||||||
|
- Local Docker for Linux dev iteration
|
||||||
|
- Per-platform asset picker in the update checker
|
||||||
|
- README + BUILD docs covering all three platforms
|
||||||
|
- Version-consistency gate preventing drift between tag and the three version files
|
||||||
Reference in new issue
Block a user