Windows 交付改單一 exe+版本號不再手打(leo 08-06 回報②與「整串該機械化」)

## ② 兩個檔很迷惑 → 一個檔
leo 圖一標註:「下載還是 ZIP ⇒ 應該是 MSIX 或 EXE;解開還是 2 個檔 ⇒ 應該只有 1 個檔案」。
舊做法 zip 裡放 Arcrun.exe+arcrun-collector.exe 同層,少解一個檔就永遠不會同步,
而畫面不會說是這個原因。
修:collector 用 go:embed 收進 Arcrun.exe,第一次執行攤到 ~/.arcrun-rag/bin/
    (冪等:sha256 相同不重寫;先寫暫存再 rename,避免留半截執行檔)。
    build-win.sh 直接產單一 Arcrun-win-<ver>.exe,不再打 zip。
    加機械閘:產出的 exe 若比 collector 還小 ⇒ 沒 embed 成功 ⇒ 中止(過去只會
    默默得到一顆「裝了也不會同步」的空殼)。
不走 MSIX:未上架的 MSIX 要使用者先開「開發人員模式」,比 zip 更麻煩。

## 版本號機械化(leo:「這一整串都應該是機械化,不應該每次手工做」)
病根實測:本 repo **一個 git tag 都沒有**,而四支 build 腳本寫的是
`git describe --tags` ⇒ 永遠拿不到 v0.18.x ⇒ 只能靠打包時人工敲 VERSION=
⇒ 兩條打包線各敲各的 ⇒ manifest 宣告 0.18.5、產物其實是 0.18.4。

修:新增 daemon-version.sh 當唯一產生器,沿用 release.mjs 已在跑的原則
   (「版本號由內容算出來,不是由人宣告」),不另立新制:
     MINOR ← DAEMON_LINE(人決定,大改版才動)
     PATCH ← 動過 collector/ 的 commit 數 − DAEMON_PATCH_BASE(機器決定)
   ⇒ 沒改 daemon 重跑打包不會虛增;改了才 +1;四支腳本在同一 commit 必得同號。
   ⇒ 不需要 git tag/Actions/輪詢(守 D20 紅線)。
   校準:DAEMON_LINE=0.18、BASE=87 ⇒ 現在的樹算出 v0.18.7(上次出貨過 0.18.6)。
   build-win/mac/dmg/msix 四支全部改用它。

## icon 漂移根治接上打包
build-win.sh 步驟⓪ 每次都跑 gen-icons.py 重產 icon.ico/trayicon.ico。

## 驗(實測輸出,非推測)
· ./build-win.sh 全程跑通,產出 dist/Arcrun-win-v0.18.7.exe(27,445,248 bytes)
· strings 抽內嵌版本 = v0.18.7、buildTime = 20260806-1233(機器算的,沒人敲)
· 位元組比對該 exe:
    CIS trayicon 在裡面        = True
    Wails「W」圖還在           = False
    collector 有 embed 進去    = True(16,104,960 bytes)
· go vet 全綠(darwin + windows);mac build OK
· 未驗:Windows 真機行為(需封測者實跑)⇒ 狀態 ◐,不是 
· 未做:CHANGELOG 閘、manifest「宣告版本 == 產物版本」驗證閘、前端顯示版本與下載入口
This commit is contained in:
2026-08-06 12:35:54 +08:00
parent ee2fc2a803
commit a55488040d
8 changed files with 194 additions and 20 deletions
+5 -1
View File
@@ -6,7 +6,11 @@
# 而且 macOS 對「不在 /Applications 的常駐程式」權限行為也不一致。
set -euo pipefail
cd "$(dirname "$0")"
VERSION="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}"
# 🔴 2026-08-06:版本號改由 daemon-version.sh 機械產生(不再手打/不靠 git describe)。
# 病根:本 repo 一個 git tag 都沒有 ⇒ git describe 永遠拿不到 v0.18.x
# ⇒ 只能靠打包時人工敲 VERSION= ⇒ 兩條打包線各敲各的 ⇒ manifest 說謊。
# 理由與算法全文見 daemon-version.sh。
VERSION="${VERSION:-$(./daemon-version.sh)}"
echo "① 先確保 .app 是最新的"
VERSION="$VERSION" bash build-mac.sh >/dev/null
+5 -1
View File
@@ -6,7 +6,11 @@
# 沒同綑 ⇒ 裝了也不會同步。
set -euo pipefail
cd "$(dirname "$0")"
VERSION="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}"
# 🔴 2026-08-06:版本號改由 daemon-version.sh 機械產生(不再手打/不靠 git describe)。
# 病根:本 repo 一個 git tag 都沒有 ⇒ git describe 永遠拿不到 v0.18.x
# ⇒ 只能靠打包時人工敲 VERSION= ⇒ 兩條打包線各敲各的 ⇒ manifest 說謊。
# 理由與算法全文見 daemon-version.sh。
VERSION="${VERSION:-$(./daemon-version.sh)}"
BUILD_TIME="$(date '+%Y%m%d-%H%M')"
LDFLAGS_VER="-X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}"
export PATH="$PATH:$(go env GOPATH)/bin"
+1 -1
View File
@@ -34,7 +34,7 @@ MAKEMSIX="$MSIX_SDK_DIR/.vs/bin/makemsix"
exit 1
}
VERSION_RAW="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}"
VERSION_RAW="${VERSION:-$(./daemon-version.sh)}" # 版本由 daemon-version.sh 機械產生(2026-08-06
# msix 版本必須是四段數字 a.b.c.d,且**最後一段必須是 0**(Store 規定,保留給微軟)。
MSIX_VERSION="$(echo "$VERSION_RAW" | sed 's/^v//' | awk -F. '{printf "%d.%d.%d.0", $1, $2, $3}')"
# ⚠️ 變數後緊接全形括號必須用 ${} 包起來——全形字元會被 bash 當成變數名的一部分。
+40 -16
View File
@@ -20,7 +20,12 @@ set -euo pipefail
cd "$(dirname "$0")"
export PATH="$PATH:$(go env GOPATH)/bin"
VERSION="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}"
# 🔴 2026-08-06:版本號**不再手打、也不再靠 git describe**。
# 舊寫法 `git describe --tags` 在這個 repo 永遠拿不到 v0.18.x(一個 tag 都沒有)
# ⇒ 版本只能靠打包時人工 `VERSION=v0.18.4 ./build-win.sh` 敲 ⇒ manifest 說謊的病根。
# 現在由 daemon-version.sh 從 DAEMON_LINE + collector/ 的 commit 數算出來,
# 兩條打包線(win/mac)在同一個 commit 上必得同一個號碼。理由全文見該腳本。
VERSION="${VERSION:-$(./daemon-version.sh)}"
BUILD_TIME="$(date '+%Y%m%d-%H%M')"
echo "🏷 版本:${VERSION}build ${BUILD_TIME}"
@@ -30,7 +35,13 @@ command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1 || {
exit 1
}
echo "① 編 collector.exe(同綑執行檔,純 stdlib、CGO_ENABLED=0"
echo "⓪ 由 appicon.png 產生 Windows 圖示(icon.icotrayicon.ico"
# 🔴 2026-08-06:不產就會沿用上一次留在磁碟上的檔——而上一次那顆是 `wails init`
# 留下的 Wails 預設「W」圖,leo 封測看到視窗與托盤兩處都是 W。
# 每次 build 都重產 ⇒ 「換了 appicon 卻沒換 Windows」這種漂移不可能再發生。
python3 gen-icons.py
echo "① 編 collector.exe(會被 embed 進 Arcrun.exe,純 stdlib、CGO_ENABLED=0"
# 🔴 2026-08-06:同 build-mac.sh 補回版本注入(Wails 換代時掉的,理由見該檔註解)。
# 2b83c47 當年就修過一次「Mac 有、Windows 漏了」——換代後兩邊又一起掉,這次一起補。
( cd ../.. && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 \
@@ -57,23 +68,36 @@ CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
-ldflags "-X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}"
OUT="dist"; mkdir -p "$OUT"
STAGE="$(mktemp -d)/Arcrun"
mkdir -p "$STAGE"
echo "③ 同綑 collector.exeApp 用 os.Executable() 找同層,見 supervise.go"
cp build/bin/arcrun-app.exe "$STAGE/Arcrun.exe"
cp arcrun-collector.exe "$STAGE/arcrun-collector.exe"
echo "③ 產出**單一執行檔**"
# 🔴 2026-08-06 交付形態改變(leo 封測回報②,圖一標註):
# 「下載還是 ZIP ⇒ 應該是 MSIX 或 EXE」「解開還是 2 個檔 ⇒ 應該只有 1 個檔案」
# 「兩個檔案令人很迷惑,User unfriendly」
#
# 舊做法=zip 裡放 Arcrun.exearcrun-collector.exe 同層。使用者只要少解一個檔、
# 或只把 Arcrun.exe 拖出來,同步就永遠不會動,而畫面不會說是這個原因。
# 新做法=collector 在步驟②被 go:embed 收進 Arcrun.exe(見 bundled_collector_windows.go),
# 第一次執行時攤到 ~/.arcrun-rag/bin/。**下載下來就是一個檔,雙擊就跑。**
#
# 為什麼不出 MSIX:未上架的 MSIX 要使用者先開「開發人員模式」才裝得起來,
# 比 zip 更麻煩。上架 MS Store 後再回頭走 build-msix.sh。
EXE="$(cd "$OUT" && pwd)/Arcrun-win-${VERSION}.exe"
rm -f "$EXE"
cp build/bin/arcrun-app.exe "$EXE"
echo "④ 打包 zip"
# 用絕對路徑:下面會 cd 進暫存目錄打包,相對路徑會找不到(實撞:zip I/O error)。
ZIP="$(cd "$OUT" && pwd)/ArcrunRAG-win-unsigned-${VERSION}.zip"
rm -f "$ZIP"
( cd "$(dirname "$STAGE")" && zip -qr "$ZIP" "Arcrun" )
rm -rf "$(dirname "$STAGE")"
# 🔴 機械閘:embed 進去的 collector 至少要有幾 MB,否則就是「編了個空殼」。
# (步驟①失敗但 wails build 仍成功時,過去只會得到一個不會同步的 exe。)
APP_SIZE=$(stat -f%z "$EXE" 2>/dev/null || stat -c%s "$EXE")
COL_SIZE=$(stat -f%z arcrun-collector.exe 2>/dev/null || stat -c%s arcrun-collector.exe)
if [ "$APP_SIZE" -lt "$COL_SIZE" ]; then
echo "❌ 產出的 exe${APP_SIZE} bytes)比 collector${COL_SIZE} bytes)還小"
echo " ⇒ collector 沒被 embed 進去,這顆裝了也不會同步。中止。"
exit 1
fi
SIZE=$(ls -lh "$ZIP" | awk '{print $5}')
echo "✅ 完成:${ZIP}${SIZE}"
SIZE=$(ls -lh "$EXE" | awk '{print $5}')
echo "✅ 完成:${EXE}${SIZE},內含同步引擎"
echo
echo "🔬 使用者流程:解壓縮執行 Arcrun.exe"
echo "🔬 使用者流程:下載雙擊 Arcrun.exe(不必解壓縮、不必找第二個檔)"
echo " ⚠️ 未簽章 ⇒ SmartScreen 會攔(「更多資訊」→「仍要執行」)。"
echo " 要免除這一步=上 MS Store(見 build-msix.sh)或買 EV 憑證。"
+12
View File
@@ -0,0 +1,12 @@
//go:build !windows
package main
// bundled_collector_other.go — 非 Windows 平台不內嵌 collector。
//
// macOS 走 .app bundlecollector 就放在 Contents/MacOS/ 裡,對使用者而言
// **本來就只有一個可拖曳的東西**(Arcrun.app),沒有「兩個檔很迷惑」的問題。
// 所以只有 Windows 需要把 collector 收進單一 exe(見 bundled_collector_windows.go)。
// bundledCollectorPath 回空字串=沒有內嵌版,呼叫端退回「找同層」。
func bundledCollectorPath() string { return "" }
@@ -0,0 +1,75 @@
//go:build windows
package main
// bundled_collector_windows.go — 把 collector 內嵌進單一 Arcrun.exe2026-08-06
//
// 🔴 leo 08-06 封測回報②:
//
// 「下載還是 ZIP ⇒ 應該是 MSIX 或 EXE;解開還是 2 個檔 ⇒ 應該只有 1 個檔案」
// 「兩個檔案令人很迷惑,User unfriendly」
//
// 舊做法:zip 裡放 Arcrun.exearcrun-collector.exe 同層,App 用 os.Executable()
// 找隔壁那支。使用者只要少解壓一個檔、或只把 Arcrun.exe 拖出來,同步就永遠不會動
// ——而畫面不會說是這個原因。
//
// 新做法:collector 直接 embed 進 Arcrun.exe,第一次執行時攤到
// `~/.arcrun-rag/bin/arcrun-collector.exe`(與 config.jsoncollector.log 同一個資料夾,
// 使用者要備份或清乾淨只有一個地方)。
//
// 為什麼不選 MSIX:未上架的 MSIX 要使用者先開「開發人員模式」才裝得起來,
// 比 zip 更麻煩(見 docs-site 的 install-windows 方式 B)。上架後再回頭走 MSIX。
//
// ⚠️ 編譯前提:`arcrun-collector.exe` 必須先存在同目錄(build-win.sh 步驟①會先編)。
// 它被 .gitignore 排除,所以**不能**直接 `go build` 一個乾淨 clone——一律走 build-win.sh。
import (
"crypto/sha256"
_ "embed"
"encoding/hex"
"os"
"path/filepath"
)
//go:embed arcrun-collector.exe
var bundledCollector []byte
// bundledCollectorPath 把內嵌的 collector 攤到磁碟並回傳路徑。
//
// 冪等:內容一樣就不重寫(避免每次啟動都寫 16MB,也避免正在跑的那支被覆寫)。
// 出錯回空字串——呼叫端會退回「找同層」的舊行為,並由 appLog 留下痕跡。
func bundledCollectorPath() string {
if len(bundledCollector) == 0 {
return ""
}
dir := filepath.Join(appDir(), "bin")
dst := filepath.Join(dir, "arcrun-collector.exe")
want := sha256.Sum256(bundledCollector)
if have, err := os.ReadFile(dst); err == nil {
if got := sha256.Sum256(have); got == want {
return dst // 已是同一版,不動它
}
}
if err := os.MkdirAll(dir, 0o755); err != nil {
appLog("攤開內嵌同步引擎失敗(建資料夾):%v", err)
return ""
}
// 先寫暫存再 rename:中途失敗不會留下半截的執行檔。
tmp := dst + ".new"
if err := os.WriteFile(tmp, bundledCollector, 0o755); err != nil {
appLog("攤開內嵌同步引擎失敗(寫檔):%v", err)
return ""
}
// Windows 不允許覆寫「正在執行」的檔案;先移開舊的再換上新的。
_ = os.Remove(dst + ".old")
_ = os.Rename(dst, dst+".old")
if err := os.Rename(tmp, dst); err != nil {
appLog("攤開內嵌同步引擎失敗(換檔):%v", err)
return ""
}
_ = os.Remove(dst + ".old")
appLog("已攤開內嵌同步引擎:%ssha256 %s", dst, hex.EncodeToString(want[:8]))
return dst
}
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# daemon-version.sh — daemon(桌面 App)版本號的**唯一**產生器(2026-08-06
#
# 🔴 leo 08-06:「給版本號、本版更新內容、打包產品、顯示在前端,
# 這一整串都應該是機械化,**不應該每次手工做**」
#
# ── 病根(實測,不是猜的)────────────────────────────────────────────
# manifest 宣告 daemon v0.18.5,但線上 zip 拆開內嵌版本是 **v0.18.4**。
# 原因:本 repo **一個 git tag 都沒有**,而各 build 腳本寫的是
# `VERSION=$(git describe --tags --always --dirty)` ⇒ 永遠拿不到 v0.18.x
# ⇒ 那個號碼只可能是每次打包時人工 `VERSION=v0.18.4 ./build-win.sh` 敲進去的。
# 人工敲 ⇒ 兩條打包線(DMG/zip)只重打一條就對不上 ⇒ manifest 說謊。
#
# ── 做法(沿用 installer/scripts/release.mjs 已經在跑的原則,不另立新制)──
# release.mjs 開宗明義:「版本號**由內容算出來**,不是由人(或 AI)宣告」。
# 雲端 bundle1.4.x)早就這樣跑了,daemon 只是一直被排除在外。這裡補上:
#
# MINOR ← 人決定:`DAEMON_LINE` 檔(大改版才動,平常不碰)
# PATCH ← 機器決定:**動過 collector/ 的 commit 數** 減去 `DAEMON_PATCH_BASE`
#
# ⇒ 沒改 daemon 就重跑打包,版本**不會虛增**(commit 數沒變)。
# ⇒ 改了 daemon 才會 +1,且兩條打包線在同一個 commit 上算出**同一個號碼**。
# ⇒ 不需要 git tag、不需要 Actions、不需要輪詢(守 D20 紅線)。
#
# 換線(0.18 → 0.19)時:改 DAEMON_LINE,並把 DAEMON_PATCH_BASE 設成當下的
# `git rev-list --count HEAD -- collector/`,讓新線從 .0 開始。
#
# 用法:VERSION="$(./daemon-version.sh)" → 印出 v0.18.7 這種字串
set -euo pipefail
cd "$(dirname "$0")"
REPO_ROOT="$(git rev-parse --show-toplevel)"
LINE="$(tr -d ' \n' < "$REPO_ROOT/DAEMON_LINE")"
BASE="$(tr -d ' \n' < "$REPO_ROOT/DAEMON_PATCH_BASE")"
[[ "$LINE" =~ ^[0-9]+\.[0-9]+$ ]] || { echo "DAEMON_LINE 必須是 MAJOR.MINOR(例 0.18),現在是 '$LINE'" >&2; exit 1; }
[[ "$BASE" =~ ^[0-9]+$ ]] || { echo "DAEMON_PATCH_BASE 必須是整數,現在是 '$BASE'" >&2; exit 1; }
COUNT="$(git -C "$REPO_ROOT" rev-list --count HEAD -- collector/)"
PATCH=$(( COUNT - BASE ))
if [ "$PATCH" -lt 0 ]; then
echo "DAEMON_PATCH_BASE$BASE)大於實際 commit 數($COUNT)——換線時忘了更新?" >&2
exit 1
fi
echo "v${LINE}.${PATCH}"
+10 -1
View File
@@ -23,8 +23,17 @@ import (
var sup *supervisor.Supervisor
// collectorBinPath 找同綑的 collector 執行檔。
// 規則與 arcrun-tray 一致:跟 App 執行檔同層(build 時會複製進 .app)。
//
// 兩種同綑方式,依平台:
// - Windowscollector **embed 在 Arcrun.exe 裡**,第一次執行攤到 ~/.arcrun-rag/bin/
// leo 08-06:「解開還是 2 個檔 ⇒ 應該只有 1 個檔案」)。見 bundled_collector_windows.go。
// - macOS:放在 .app 的 Contents/MacOS/ 同層(使用者只看到一個可拖曳的 Arcrun.app)。
//
// 內嵌攤開失敗時**退回找同層**——舊版打包出來的資料夾仍然能跑,不會因為換機制而全壞。
func collectorBinPath() string {
if p := bundledCollectorPath(); p != "" {
return p
}
name := "arcrun-collector"
if runtime.GOOS == "windows" {
name += ".exe"