visionA/local-tool/Makefile
jim800121chen a05d206c07 fix(build): copy_bundled_data 改為冪等,清掉前次 build 的殘留
Windows/Linux build 卡在「預期 2 個 .nef,實際 8 個」。

payload-macos 呼叫前有 rm -rf payload/darwin,但 payload-windows 與
payload-linux 刻意不清(build-server-* 已先把 binary 放進 bin/),
導致 data/ 留著白名單機制加入前那 8 個 .nef,與新複製的 2 個相加後
觸發後置檢查。

後置檢查本身是對的 —— installer/windows/visiona-local.iss:86 是
recursesubdirs 整包收,殘留什麼就出貨什麼。問題在 helper 不冪等。

改為複製前先清空目標 data 目錄。清整個目錄而非只清 nef/,因為該目錄
內容全由此 helper 產生,只清 nef/ 無法處理未來移除非 nef 檔案的情況。

rm -rf 加四層防護:拒絕空值、絕對路徑與 ~、含 .. 的跳脫、不符
payload/*/data 格式的目標,另檢查目標不是檔案。用 case 而非 [[ ]]
以相容 Git Bash。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 22:15:26 +08:00

771 lines
39 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# visionA-local — Makefile骨架M1-1
#
# 這份 Makefile 目前所有 targets 都只是 placeholder實際內容會在後續任務逐步填入
# - M1-2 複製 server core
# - M1-4 複製 frontend
# - M1-8 build-server / build-frontend 實作
# - M1-9 visiona-local (Wails) shell
# - M1-11 payload-macos
# - M1-12 installer-macos (dmg)
# - M2+ Windows / Linux / CI
#
# 詳見 .autoflow/04-architecture/build-pipeline.md
SHELL := /bin/bash
VERSION ?= v0.1.0-dev
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
OS := $(shell uname -s | tr A-Z a-z)
DIST := dist
PAYLOAD := visiona-local/payload
# ── 打包用 .nef 白名單M5-a────────────────────────────────────────
#
# server/data/ 底下有 8 個 .nefKL520 五個、KL720 三個),但安裝包只帶
# 白名單內的這幾個,其餘不進 payload藉此縮小安裝檔體積。
#
# models.json 不做任何過濾7 個 model 定義全部照原樣複製。執行期由
# server/internal/model/repository.go 的 NewRepository() 檢查每個 model 的
# filePath 是否實際存在,不存在的直接不載入(見 M5-c。因此使用者在 UI
# 只會看到白名單內的 model未打包的不會出現、也不會選到後拿到莫名錯誤。
#
# 未來要把某個 model 加回安裝包:把對應的 .nef 相對路徑加進下面這個變數即可,
# models.json 不用動。
#
# 路徑相對於 server/data/。
BUNDLED_NEFS := \
nef/kl520/kl520_20004_fcos-drk53s_w512h512.nef \
nef/kl520/kl520_tiny_yolo_v3.nef
# copy_bundled_data把 server/data/ 複製到 $(1),但 nef/ 只帶 BUNDLED_NEFS 白名單。
# $(1) = 目標 data 目錄payload/darwin/data
#
# 步驟:(a) 先清空 $(1),確保這個 helper 是冪等的(見下方「為什麼要清空」)
# (b) 複製 server/data/ 下除了 nef/ 以外的所有東西models.json 等)
# (c) 再逐一複製白名單內的 .nef
# (d) 白名單檔案不存在就直接 fail避免安靜地產出缺 model 的安裝包
# (e) 最後數 .nef 個數,與白名單不符就 fail
#
# 為什麼要清空 $(1)(a) 步驟):
# payload-windows / payload-linux 刻意不 rm -rf 整個 payload/<os>/(因為
# build-server-* 已先把 binary 放進 bin/),所以先前 build 留下的檔案會殘留。
# 白名單機制M5-a之前的舊 build 會把 8 個 .nef 全部複製進去,升級到白名單
# 版本後再 build就變成「新複製 2 個 + 殘留 6 個 = 8 個」,被下方的後置檢查
# 擋下來build 直接失敗。清空目標目錄讓這個 helper 重複執行結果一致,不受
# 目標目錄既有內容影響。
# payload-macos 沒踩到只是因為它有 rm -rf payload/darwin。
#
# 清整個 $(1) 而不是只清 nef/$(1) 的內容 100% 由本 helper 產生Makefile 中
# 只有這裡寫入 payload/<os>/data/),沒有其他來源的檔案需要保留。只清 nef/ 的話,
# models.json 以外的殘留(例如未來從 server/data/ 移除的檔案)仍會被 installer
# 打包進去 —— installer/windows/visiona-local.iss 是用 data\* + recursesubdirs
# 整包收,殘留什麼就出貨什麼。清整包才是真正的冪等。
#
# rm -rf 安全防護($(1) 來自 make 變數,打錯會刪掉不該刪的東西):
# 1. 空值檢查:$(1) 展開為空就 fail避免變成 `rm -rf /`rm -rf "" 在部分
# shell 下是 no-op、但空值代表呼叫端寫錯直接擋掉比較安全
# 2. 絕對路徑 / 跳脫檢查:只接受相對路徑、且不得含 `..`,避免 `/`、`$$HOME`、
# `../..` 這類目標
# 3. 白名單前綴:路徑必須以 payload/ 開頭並以 /data 結尾,鎖死在 build 產物區
# 4. 刪的是「已存在且確定是目錄」的路徑,且刪除後立刻重建
#
# 只用 POSIX find / cp不用 rsync —— Windows CI 跑在 Git Bashwindows-2022 +
# shell: bash該環境沒有 rsync。
define copy_bundled_data
@set -e; \
echo "==> 複製 server/data → $(1).nef 白名單:$(words $(BUNDLED_NEFS)) 個)"; \
if [ ! -d server/data ]; then echo "!! ERROR: server/data 不存在 !!"; exit 1; fi; \
target='$(strip $(1))'; \
if [ -z "$$target" ]; then \
echo "!! ERROR: copy_bundled_data 的目標目錄為空,拒絕執行 !!"; exit 1; \
fi; \
case "$$target" in \
/*|~*) echo "!! ERROR: 目標目錄必須是相對路徑,收到 '$$target' !!"; exit 1 ;; \
*..*) echo "!! ERROR: 目標目錄不得含 '..',收到 '$$target' !!"; exit 1 ;; \
esac; \
case "$$target" in \
payload/*/data) : ;; \
*) echo "!! ERROR: 目標目錄必須符合 payload/<os>/data,收到 '$$target' !!"; exit 1 ;; \
esac; \
if [ -e "$$target" ] && [ ! -d "$$target" ]; then \
echo "!! ERROR: '$$target' 存在但不是目錄,拒絕刪除 !!"; exit 1; \
fi; \
if [ -d "$$target" ]; then \
echo " 清空既有的 $${target}(確保冪等,不留前次 build 的殘留)"; \
rm -rf -- "$$target"; \
fi; \
mkdir -p "$$target"; \
dest="$$(cd "$$target" && pwd)"; \
( cd server/data && \
find . -path ./nef -prune -o -type d -print | while read -r d; do mkdir -p "$$dest/$$d"; done && \
find . -path ./nef -prune -o -type f -print | while read -r f; do cp "$$f" "$$dest/$$f"; done ); \
if [ ! -f "$$dest/models.json" ]; then \
echo "!! ERROR: models.json 沒有被複製到 $$dest !!"; exit 1; \
fi; \
for nef in $(BUNDLED_NEFS); do \
if [ ! -f "server/data/$$nef" ]; then \
echo "!! ERROR: BUNDLED_NEFS 列出的 server/data/$$nef 不存在 !!"; \
exit 1; \
fi; \
mkdir -p "$$dest/$$(dirname $$nef)"; \
cp "server/data/$$nef" "$$dest/$$nef"; \
echo " + $$nef"; \
done; \
copied=$$(find "$$dest" -name '*.nef' | wc -l | tr -d ' '); \
if [ "$$copied" != "$(words $(BUNDLED_NEFS))" ]; then \
echo "!! ERROR: 預期 $(words $(BUNDLED_NEFS)) 個 .nef,實際 $$copied 個 !!"; exit 1; \
fi; \
echo " models.json + $$copied 個 .nef 已就位"
endef
.PHONY: help \
vendor-sync vendor-python vendor-wheels vendor-ffmpeg vendor-ffmpeg-macos-build \
vendor-python-windows vendor-wheels-windows vendor-ffmpeg-windows \
vendor-python-linux vendor-wheels-linux vendor-ffmpeg-linux \
server build-server \
frontend build-frontend build-embed \
payload payload-macos payload-windows payload-linux \
stage-macos stage-windows \
wails-macos wails-windows wails-linux \
dmg exe exe-only _run-iscc appimage \
dev test lint fmt \
clean clean-all clean-build-exe clean-build-dmg clean-build-appimage
# ── 幫助 ───────────────────────────────────────────────────────────
help: ## 列出所有 make targets
@echo "visionA-local — available targets (M1-1 skeleton)"
@echo ""
@echo " 依賴同步:"
@echo " vendor-sync 下載 python-build-standalone / wheels / ffmpeg → vendor/"
@echo ""
@echo " Build單元"
@echo " server build Go server binary (→ dist/visiona-local-server)"
@echo " frontend pnpm build Next.js 靜態產物 (→ frontend/out)"
@echo ""
@echo " Payload 準備:"
@echo " payload-macos stage macOS payload → visiona-local/payload/"
@echo " payload-windows stage Windows payload"
@echo " payload-linux stage Linux payload"
@echo ""
@echo " Wails 應用 build"
@echo " wails-macos wails build darwin/amd64"
@echo " wails-windows wails build windows/amd64"
@echo " wails-linux wails build linux/amd64"
@echo ""
@echo " 安裝檔打包:"
@echo " dmg macOS .dmgdmgbuild"
@echo " exe Windows .exeInno Setup"
@echo " appimage Linux .AppImage"
@echo ""
@echo " 工具:"
@echo " clean 清除 dist/ payload/"
@echo ""
@echo "Note: 目前所有 target 都是 placeholder只印 TODO尚未實作。"
# ── 依賴同步 ───────────────────────────────────────────────────────
PYTHON_VERSION := 3.12.9
PBS_RELEASE := 20250317
PBS_URL_DARWIN := https://github.com/astral-sh/python-build-standalone/releases/download/$(PBS_RELEASE)/cpython-$(PYTHON_VERSION)+$(PBS_RELEASE)-x86_64-apple-darwin-install_only.tar.gz
# ── ffmpegLGPL v3方案 B 混合)──
# v2 TDD §2.2macOS 自 build decoder-only~20 MBcommit 到 vendor/ffmpeg/macos/
# Windows / Linux 用 BtbN 的 n7.1 LGPL build。
FFMPEG_VERSION := n7.1
FFMPEG_SRC_URL := https://github.com/FFmpeg/FFmpeg/archive/refs/tags/$(FFMPEG_VERSION).tar.gz
# sha256 於第一次 build 時由 `make vendor-ffmpeg-macos-build` 計算後填入 vendor/ffmpeg/macos/BUILD.md
# 之後每次 build 使用此值做 integrity check下方 Makefile 變數亦同步更新)。
FFMPEG_SRC_SHA256 := 7ddad2d992bd250a6c56053c26029f7e728bebf0f37f80cf3f8a0e6ec706431a
vendor-sync: vendor-python vendor-wheels vendor-ffmpeg ## 下載所有第三方依賴到 vendor/(不進 git第三輪決策 Q-D=D2
@echo "==> vendor-sync 完成"
vendor-python: ## 下載 python-build-standalone tarball → vendor/python/darwin/
@mkdir -p vendor/python/darwin
@if [ ! -f vendor/python/darwin/python.tar.gz ]; then \
echo "==> 下載 python-build-standalone $(PYTHON_VERSION)+$(PBS_RELEASE) (macOS x86_64 install_only)..."; \
curl -fL -o vendor/python/darwin/python.tar.gz "$(PBS_URL_DARWIN)"; \
echo "==> tarball 大小:$$(du -sh vendor/python/darwin/python.tar.gz | cut -f1)"; \
else \
echo "==> python tarball 已存在,跳過下載 ($$(du -sh vendor/python/darwin/python.tar.gz | cut -f1))"; \
fi
vendor-wheels: ## 同步 wheels → vendor/wheels/darwin/(內部 wheel 從 visiona-local/wheels 複製,公開相依用 pip download
@mkdir -p vendor/wheels/darwin
@echo "==> 同步內部 wheelsKneronPLUS 等)..."
@if [ -d visiona-local/wheels/macos ]; then \
cp visiona-local/wheels/macos/*.whl vendor/wheels/darwin/ 2>/dev/null || true; \
fi
@echo "==> 從 PyPI 下載公開相依 wheels (cp312, macosx_x86_64)..."
@pip3 download \
--only-binary=:all: \
--platform macosx_10_9_x86_64 \
--platform macosx_11_0_x86_64 \
--platform macosx_12_0_x86_64 \
--python-version 3.12 \
--implementation cp \
--dest vendor/wheels/darwin \
numpy opencv-python-headless pyusb requests || echo "WARN: pip download 部分失敗(詳見上方訊息)"
@echo "==> wheels 總覽:"
@ls -1 vendor/wheels/darwin/*.whl 2>/dev/null | wc -l | xargs -I{} echo " 共 {} 個 wheel"
@du -sh vendor/wheels/darwin
vendor-ffmpeg: ## macOSLGPL v3 ffmpeg + ffprobe 已 commit 到 vendor/ffmpeg/macos/,本 target 只驗證存在 + LGPL 合規
@if [ ! -f vendor/ffmpeg/macos/ffmpeg ]; then \
echo "❌ vendor/ffmpeg/macos/ffmpeg 不存在。"; \
echo " 第一次 build 請執行make vendor-ffmpeg-macos-build"; \
echo " (只需要在升級 ffmpeg 版本時跑一次;平常 clone repo 後 binary 已在 git 內)"; \
exit 1; \
fi
@if [ ! -f vendor/ffmpeg/macos/ffprobe ]; then \
echo "❌ vendor/ffmpeg/macos/ffprobe 不存在。"; \
echo " 請執行make vendor-ffmpeg-macos-build"; \
exit 1; \
fi
@echo "==> vendor/ffmpeg/macos/ffmpeg 存在:$$(du -h vendor/ffmpeg/macos/ffmpeg | cut -f1)"
@echo "==> vendor/ffmpeg/macos/ffprobe 存在:$$(du -h vendor/ffmpeg/macos/ffprobe | cut -f1)"
@if vendor/ffmpeg/macos/ffmpeg -version 2>&1 | grep -qE -- '--enable-gpl|libx264|libx265'; then \
echo "❌ LGPL 驗證失敗binary 含 --enable-gpl / libx264 / libx265"; \
exit 1; \
fi
@echo "==> LGPL 驗證通過(無 --enable-gpl / libx264 / libx265"
# 只有升級 ffmpeg 版本時才跑此 targetbinary 產出後 commit 到 gitv2 TDD R5-6b
# 需要的系統依賴macOS
# brew install pkg-config nasm # 或 yasm
vendor-ffmpeg-macos-build: ## macOS從源碼 build LGPL v3 decoder-only ffmpeg + ffprobe升級時才跑~15 分鐘)
@if [ "$$(uname -s)" != "Darwin" ]; then \
echo "❌ vendor-ffmpeg-macos-build 只能在 macOS 上跑"; exit 1; \
fi
@command -v pkg-config >/dev/null 2>&1 || { echo "❌ 需要 pkg-configbrew install pkg-config"; exit 1; }
@command -v nasm >/dev/null 2>&1 || command -v yasm >/dev/null 2>&1 || { echo "❌ 需要 nasm 或 yasmbrew install nasm"; exit 1; }
@mkdir -p vendor/ffmpeg/macos build/ffmpeg-macos
@echo "==> 下載 ffmpeg source $(FFMPEG_VERSION)..."
curl -fL -o build/ffmpeg-macos/source.tar.gz "$(FFMPEG_SRC_URL)"
@echo "==> 驗證 source tarball sha256..."
@echo "$(FFMPEG_SRC_SHA256) build/ffmpeg-macos/source.tar.gz" | shasum -a 256 -c || { \
echo "❌ sha256 不符,請更新 Makefile 中的 FFMPEG_SRC_SHA256 或檢查來源"; \
echo " 實際 sha256$$(shasum -a 256 build/ffmpeg-macos/source.tar.gz | awk '{print $$1}')"; \
exit 1; }
@rm -rf build/ffmpeg-macos/src build/ffmpeg-macos/install
@mkdir -p build/ffmpeg-macos/src
tar xzf build/ffmpeg-macos/source.tar.gz -C build/ffmpeg-macos/src --strip-components=1
@echo "==> configuredecoder-only LGPL v3..."
cd build/ffmpeg-macos/src && ./configure \
--prefix="$$(pwd)/../install" \
--enable-version3 \
--disable-debug \
--disable-doc \
--disable-ffplay \
--disable-network \
--disable-autodetect \
--disable-shared \
--enable-static \
--disable-everything \
--enable-small \
--enable-protocol=file,pipe \
--enable-demuxer=mov,avi,mpegps,mpegts,matroska,image2 \
--enable-decoder=h264,hevc,mpeg1video,mpeg2video,mpeg4,mjpeg,prores,vp8,vp9,aac,mp2,mp3,pcm_s16le,pcm_s16be \
--enable-parser=h264,hevc,mpeg4video,mpegaudio,aac \
--enable-filter=scale,format,fps,null,anull \
--enable-muxer=image2pipe,image2,null \
--enable-encoder=mjpeg \
--enable-swscale \
--enable-swresample \
--extra-cflags="-arch x86_64 -mmacosx-version-min=10.15" \
--extra-ldflags="-arch x86_64 -mmacosx-version-min=10.15 -Wl,-search_paths_first" \
--arch=x86_64 \
--target-os=darwin \
--cc="clang -arch x86_64"
cd build/ffmpeg-macos/src && make -j$$(sysctl -n hw.ncpu)
cd build/ffmpeg-macos/src && make install
@echo "==> 複製 ffmpeg + ffprobe 到 vendor/ffmpeg/macos/..."
cp build/ffmpeg-macos/install/bin/ffmpeg vendor/ffmpeg/macos/ffmpeg
cp build/ffmpeg-macos/install/bin/ffprobe vendor/ffmpeg/macos/ffprobe
@strip -S -x vendor/ffmpeg/macos/ffmpeg
@strip -S -x vendor/ffmpeg/macos/ffprobe
@chmod +x vendor/ffmpeg/macos/ffmpeg vendor/ffmpeg/macos/ffprobe
@echo "==> ad-hoc 簽章..."
codesign --force --sign - vendor/ffmpeg/macos/ffmpeg
codesign --force --sign - vendor/ffmpeg/macos/ffprobe
@echo "==> 驗證授權configuration line 不含 --enable-gpl / libx264 / libx265..."
@if vendor/ffmpeg/macos/ffmpeg -version 2>&1 | grep -E -- '--enable-gpl|libx264|libx265'; then \
echo "❌ LGPL 驗證失敗build 不該出現 gpl / x264 / x265"; exit 1; \
fi
@echo "==> 複製 COPYING.LGPLv3 到 vendor/ffmpeg/macos/..."
cp build/ffmpeg-macos/src/COPYING.LGPLv3 vendor/ffmpeg/macos/COPYING.LGPLv3
@echo "==> ffmpeg 大小:$$(du -h vendor/ffmpeg/macos/ffmpeg | cut -f1)"
@echo "==> ffprobe 大小:$$(du -h vendor/ffmpeg/macos/ffprobe | cut -f1)"
@echo ""
@echo "✅ macOS LGPL ffmpeg build 完成。接下來請:"
@echo " 1) 更新 vendor/ffmpeg/macos/BUILD.md 的 Binary sha256 區塊"
@echo " 2) git add vendor/ffmpeg/macos/{ffmpeg,ffprobe,COPYING.LGPLv3,BUILD.md}"
# ── Build單元 ──────────────────────────────────────────────────
server: build-server ## alias for build-server
build-server: build-embed ## build Go server binary → dist/visiona-local-server會先 build frontend + embed
@mkdir -p $(DIST)
cd server && go build -o ../$(DIST)/visiona-local-server .
@echo "built: $(DIST)/visiona-local-server"
build-server-windows: build-embed ## 交叉/原生 build Windows server → payload/windows/bin/visiona-local-server.exe
@mkdir -p payload/windows/bin
cd server && GOOS=windows GOARCH=amd64 go build -o ../payload/windows/bin/visiona-local-server.exe .
@echo "built: payload/windows/bin/visiona-local-server.exe"
build-server-linux: build-embed ## 交叉/原生 build Linux server → payload/linux/bin/visiona-local-server
@mkdir -p payload/linux/bin
cd server && GOOS=linux GOARCH=amd64 go build -o ../payload/linux/bin/visiona-local-server .
@echo "built: payload/linux/bin/visiona-local-server"
frontend: build-frontend ## alias for build-frontend
build-frontend: ## pnpm build → frontend/out/
@echo "==> pnpm build frontend..."
cd frontend && pnpm install --frozen-lockfile && pnpm build
@echo "==> frontend/out 完成:"
@du -sh frontend/out
build-embed: build-frontend ## 同步 frontend/out → server/web/out 供 go:embed
@echo "==> 同步 frontend/out → server/web/out..."
rm -rf server/web/out
mkdir -p server/web/out
cp -R frontend/out/. server/web/out/
@du -sh server/web/out
# ── Payload 準備 ───────────────────────────────────────────────────
payload: payload-$(OS) ## 依當前 OS 準備 payload
payload-macos: build-server vendor-python vendor-wheels vendor-ffmpeg ## 準備 macOS payload → payload/darwin/(含 python runtime + wheels + ffmpeg
@echo "==> 建立 macOS payload (binary + models + scripts + python + wheels + ffmpeg + ffprobe)..."
rm -rf payload/darwin
mkdir -p payload/darwin/bin payload/darwin/data payload/darwin/scripts payload/darwin/python payload/darwin/wheels
cp dist/visiona-local-server payload/darwin/bin/
cp vendor/ffmpeg/macos/ffmpeg payload/darwin/bin/
cp vendor/ffmpeg/macos/ffprobe payload/darwin/bin/
cp vendor/ffmpeg/macos/COPYING.LGPLv3 payload/darwin/bin/ffmpeg-COPYING.LGPLv3
chmod +x payload/darwin/bin/ffmpeg payload/darwin/bin/ffprobe
$(call copy_bundled_data,payload/darwin/data)
cp -R server/scripts/* payload/darwin/scripts/
cp vendor/python/darwin/python.tar.gz payload/darwin/python/
@cp vendor/wheels/darwin/*.whl payload/darwin/wheels/ 2>/dev/null || true
@echo "==> macOS payload 完成:"
@du -sh payload/darwin
@echo ""
@echo "payload/darwin 結構:"
@find payload/darwin -maxdepth 2 -type d | sed 's|^| |'
@echo ""
@echo "payload/darwin/python"
@ls -lh payload/darwin/python
@echo "payload/darwin/wheels"
@ls -1 payload/darwin/wheels | head -20
stage-macos: payload-macos ## 將 payload/darwin/ 放到 Wails build/darwin/Resources/
@echo "==> 放置 payload 到 Wails build/darwin/Resources..."
rm -rf visiona-local/build/darwin/Resources
mkdir -p visiona-local/build/darwin/Resources
cp -R payload/darwin/. visiona-local/build/darwin/Resources/
@echo "==> stage 完成:"
@du -sh visiona-local/build/darwin/Resources
# ── M4Windows vendor + payload + wails + exe ─────────────────────
# 注意wails-windows 與 exe 必須在 Windows runner 上跑;在 macOS 上會明確 fail。
# payload-windows / vendor-*-windows 是 curl 下載跨平台可跑server.exe 步驟除外)。
PBS_URL_WINDOWS := https://github.com/astral-sh/python-build-standalone/releases/download/$(PBS_RELEASE)/cpython-$(PYTHON_VERSION)+$(PBS_RELEASE)-x86_64-pc-windows-msvc-install_only.tar.gz
# LGPL v3 buildBtbN n7.1 穩定分支,含 LGPL-safe extra libs— v2 TDD §3
FFMPEG_URL_WINDOWS := https://github.com/BtbN/FFmpeg-Builds/releases/latest/download/ffmpeg-n7.1-latest-win64-lgpl-7.1.zip
vendor-python-windows: ## 下載 python-build-standalone Windows x86_64 → vendor/python/windows/
@mkdir -p vendor/python/windows
@if [ ! -f vendor/python/windows/python.tar.gz ]; then \
echo "==> 下載 python-build-standalone $(PYTHON_VERSION)+$(PBS_RELEASE) (Windows x86_64 install_only)..."; \
curl -fL -o vendor/python/windows/python.tar.gz "$(PBS_URL_WINDOWS)"; \
echo "==> tarball 大小:$$(du -sh vendor/python/windows/python.tar.gz | cut -f1)"; \
else \
echo "==> python (Windows) tarball 已存在,跳過 ($$(du -sh vendor/python/windows/python.tar.gz | cut -f1))"; \
fi
vendor-wheels-windows: ## 同步 Windows wheels → vendor/wheels/windows/
@mkdir -p vendor/wheels/windows
@echo "==> 同步內部 wheels (Windows, KneronPLUS 等)..."
@if [ -d visiona-local/wheels/windows ]; then \
cp visiona-local/wheels/windows/*.whl vendor/wheels/windows/ 2>/dev/null || true; \
fi
@echo "==> 從 PyPI 下載公開相依 wheels (cp312, win_amd64)..."
@PY=""; \
for candidate in "$$VISIONA_PYTHON" "py -3" python3 python; do \
[ -z "$$candidate" ] && continue; \
resolved=$$(command -v $${candidate%% *} 2>/dev/null || true); \
case "$$resolved" in *WindowsApps*) continue ;; esac; \
if $$candidate --version >/dev/null 2>&1; then PY="$$candidate"; break; fi; \
done; \
if [ -z "$$PY" ]; then \
echo "WARN: 找不到真實 pythonWindowsApps stub 不算),跳過 PyPI 下載(僅使用內部 wheels"; \
else \
echo "==> 使用 $$PY -m pip"; \
$$PY -m pip download \
--only-binary=:all: \
--platform win_amd64 \
--python-version 3.12 \
--implementation cp \
--dest vendor/wheels/windows \
numpy opencv-python-headless pyusb requests || echo "WARN: pip download 部分失敗(詳見上方訊息)"; \
fi
@echo "==> Windows wheels 總覽:"
@ls -1 vendor/wheels/windows/*.whl 2>/dev/null | wc -l | xargs -I{} echo " 共 {} 個 wheel"
@du -sh vendor/wheels/windows 2>/dev/null || true
vendor-ffmpeg-windows: ## 下載 ffmpeg Windows LGPL v3 build (n7.1) → vendor/ffmpeg/windows/
@mkdir -p vendor/ffmpeg/windows
@if [ -f vendor/ffmpeg/windows/ffmpeg.exe ] && [ -f vendor/ffmpeg/windows/ffprobe.exe ]; then \
echo "==> ffmpeg.exe + ffprobe.exe 已存在,跳過"; \
else \
echo "==> 下載 BtbN LGPL ffmpeg (Windows, n7.1)..."; \
curl -fL -o vendor/ffmpeg/windows/ffmpeg-win.zip "$(FFMPEG_URL_WINDOWS)"; \
PY=""; \
for candidate in "$$VISIONA_PYTHON" "py -3" python3 python; do \
[ -z "$$candidate" ] && continue; \
resolved=$$(command -v $${candidate%% *} 2>/dev/null || true); \
case "$$resolved" in *WindowsApps*) continue ;; esac; \
if $$candidate --version >/dev/null 2>&1; then PY="$$candidate"; break; fi; \
done; \
if [ -z "$$PY" ]; then echo "ERROR: 需要真實 python 來解壓 zipWindowsApps stub 無法使用)"; exit 1; fi; \
echo "==> 使用 $$PY 解壓 ffmpeg zip取出 ffmpeg.exe / ffprobe.exe / LICENSE.txt"; \
$$PY -c "import zipfile, os; z=zipfile.ZipFile('vendor/ffmpeg/windows/ffmpeg-win.zip'); \
wanted=['/bin/ffmpeg.exe','/bin/ffprobe.exe','/LICENSE.txt','/COPYING.LGPLv3']; \
members=[n for n in z.namelist() if any(n.endswith(w) for w in wanted)]; \
assert any(n.endswith('/bin/ffmpeg.exe') for n in members), 'ffmpeg.exe not found in zip'; \
assert any(n.endswith('/bin/ffprobe.exe') for n in members), 'ffprobe.exe not found in zip'; \
os.makedirs('vendor/ffmpeg/windows', exist_ok=True); \
[open('vendor/ffmpeg/windows/'+m.rsplit('/',1)[1],'wb').write(z.read(m)) for m in members]; \
print('extracted:', [m.rsplit('/',1)[1] for m in members])" || { echo "ERROR: python 解壓失敗"; exit 1; }; \
rm -f vendor/ffmpeg/windows/ffmpeg-win.zip; \
[ -f vendor/ffmpeg/windows/ffmpeg.exe ] || { echo "ERROR: ffmpeg.exe 沒被寫出"; exit 1; }; \
[ -f vendor/ffmpeg/windows/ffprobe.exe ] || { echo "ERROR: ffprobe.exe 沒被寫出"; exit 1; }; \
echo "==> ffmpeg.exe 大小:$$(du -h vendor/ffmpeg/windows/ffmpeg.exe | cut -f1)"; \
echo "==> ffprobe.exe 大小:$$(du -h vendor/ffmpeg/windows/ffprobe.exe | cut -f1)"; \
fi
payload-windows: build-server-windows vendor-python-windows vendor-wheels-windows vendor-ffmpeg-windows ## 準備 Windows payload → payload/windows/
@echo "==> 建立 Windows payload (binary + models + scripts + python + wheels + ffmpeg)..."
@# 注意:不 rm -rf payload/windows因為 build-server-windows 已先把 .exe 放進去
mkdir -p payload/windows/bin payload/windows/data payload/windows/scripts payload/windows/python payload/windows/wheels
@if [ ! -f payload/windows/bin/visiona-local-server.exe ]; then \
echo "!! ERROR: payload/windows/bin/visiona-local-server.exe 不存在build-server-windows 可能失敗 !!"; \
exit 1; \
fi
cp vendor/ffmpeg/windows/ffmpeg.exe payload/windows/bin/
cp vendor/ffmpeg/windows/ffprobe.exe payload/windows/bin/
@# LGPL 授權條款BtbN build 自帶 LICENSE.txtCOPYING.LGPLv3 不一定在壓縮檔內,失敗不致命)
@cp vendor/ffmpeg/windows/LICENSE.txt payload/windows/bin/ffmpeg-LICENSE.txt 2>/dev/null || true
@cp vendor/ffmpeg/windows/COPYING.LGPLv3 payload/windows/bin/ffmpeg-COPYING.LGPLv3 2>/dev/null || true
$(call copy_bundled_data,payload/windows/data)
cp -R server/scripts/. payload/windows/scripts/
cp vendor/python/windows/python.tar.gz payload/windows/python/
@cp vendor/wheels/windows/*.whl payload/windows/wheels/ 2>/dev/null || true
@echo "==> Windows payload 完成:"
@du -sh payload/windows
@echo ""
@echo "payload/windows 結構:"
@find payload/windows -maxdepth 2 -type d | sed 's|^| |'
stage-windows: payload-windows ## 將 payload/windows/ 放到 Wails build/windows/Resources/
@echo "==> 放置 payload 到 Wails build/windows/Resources..."
rm -rf visiona-local/build/windows/Resources
mkdir -p visiona-local/build/windows/Resources
cp -R payload/windows/. visiona-local/build/windows/Resources/
@echo "==> stage 完成:"
@du -sh visiona-local/build/windows/Resources
# ── M5Linux vendor + payload + wails + AppImage ──────────────────
# 注意wails-linux 與 appimage 必須在 Linux runner 上跑;在 macOS 上會明確 fail。
# payload-linux / vendor-*-linux 是 curl 下載跨平台可跑server 步驟除外)。
PBS_URL_LINUX := https://github.com/astral-sh/python-build-standalone/releases/download/$(PBS_RELEASE)/cpython-$(PYTHON_VERSION)+$(PBS_RELEASE)-x86_64-unknown-linux-gnu-install_only.tar.gz
# LGPL v3 buildBtbN n7.1 穩定分支)— v2 TDD §4
FFMPEG_URL_LINUX := https://github.com/BtbN/FFmpeg-Builds/releases/latest/download/ffmpeg-n7.1-latest-linux64-lgpl-7.1.tar.xz
vendor-python-linux: ## 下載 python-build-standalone Linux x86_64 → vendor/python/linux/
@mkdir -p vendor/python/linux
@if [ ! -f vendor/python/linux/python.tar.gz ]; then \
echo "==> 下載 python-build-standalone $(PYTHON_VERSION)+$(PBS_RELEASE) (Linux x86_64 install_only)..."; \
curl -fL -o vendor/python/linux/python.tar.gz "$(PBS_URL_LINUX)"; \
echo "==> tarball 大小:$$(du -sh vendor/python/linux/python.tar.gz | cut -f1)"; \
else \
echo "==> python (Linux) tarball 已存在,跳過 ($$(du -sh vendor/python/linux/python.tar.gz | cut -f1))"; \
fi
vendor-wheels-linux: ## 同步 Linux wheels → vendor/wheels/linux/
@mkdir -p vendor/wheels/linux
@echo "==> 同步內部 wheels (Linux, KneronPLUS 等)..."
@if [ -d visiona-local/wheels/linux ]; then \
cp visiona-local/wheels/linux/*.whl vendor/wheels/linux/ 2>/dev/null || true; \
fi
@echo "==> 從 PyPI 下載公開相依 wheels (cp312, manylinux2014_x86_64)..."
@pip3 download \
--only-binary=:all: \
--platform manylinux2014_x86_64 \
--python-version 3.12 \
--implementation cp \
--dest vendor/wheels/linux \
numpy opencv-python-headless pyusb requests || echo "WARN: pip download 部分失敗(詳見上方訊息)"
@echo "==> Linux wheels 總覽:"
@ls -1 vendor/wheels/linux/*.whl 2>/dev/null | wc -l | xargs -I{} echo " 共 {} 個 wheel"
@du -sh vendor/wheels/linux 2>/dev/null || true
vendor-ffmpeg-linux: ## 下載 ffmpeg Linux LGPL v3 build (n7.1) → vendor/ffmpeg/linux/
@mkdir -p vendor/ffmpeg/linux
@if [ -f vendor/ffmpeg/linux/ffmpeg ] && [ -f vendor/ffmpeg/linux/ffprobe ]; then \
echo "==> ffmpeg + ffprobe (Linux) 已存在,跳過"; \
else \
echo "==> 下載 BtbN LGPL ffmpeg (Linux, n7.1)..."; \
curl -fL -o /tmp/ffmpeg-linux.tar.xz "$(FFMPEG_URL_LINUX)"; \
rm -rf /tmp/ffmpeg-linux-extract; \
mkdir -p /tmp/ffmpeg-linux-extract; \
tar xf /tmp/ffmpeg-linux.tar.xz -C /tmp/ffmpeg-linux-extract --strip-components=1; \
cp /tmp/ffmpeg-linux-extract/bin/ffmpeg vendor/ffmpeg/linux/; \
cp /tmp/ffmpeg-linux-extract/bin/ffprobe vendor/ffmpeg/linux/; \
cp /tmp/ffmpeg-linux-extract/LICENSE.txt vendor/ffmpeg/linux/LICENSE.txt 2>/dev/null || true; \
chmod +x vendor/ffmpeg/linux/ffmpeg vendor/ffmpeg/linux/ffprobe; \
rm -rf /tmp/ffmpeg-linux* ; \
echo "==> ffmpeg (Linux) 大小:$$(du -h vendor/ffmpeg/linux/ffmpeg | cut -f1)"; \
echo "==> ffprobe (Linux) 大小:$$(du -h vendor/ffmpeg/linux/ffprobe | cut -f1)"; \
fi
payload-linux: build-server-linux vendor-python-linux vendor-wheels-linux vendor-ffmpeg-linux ## 準備 Linux payload → payload/linux/
@echo "==> 建立 Linux payload (binary + models + scripts + python + wheels + ffmpeg)..."
mkdir -p payload/linux/bin payload/linux/data payload/linux/scripts payload/linux/python payload/linux/wheels
@if [ ! -f payload/linux/bin/visiona-local-server ]; then \
echo "!! ERROR: payload/linux/bin/visiona-local-server 不存在build-server-linux 可能失敗 !!"; \
exit 1; \
fi
@chmod +x payload/linux/bin/visiona-local-server
@cp vendor/ffmpeg/linux/ffmpeg payload/linux/bin/ 2>/dev/null && chmod +x payload/linux/bin/ffmpeg || echo "!! WARN: ffmpeg 缺失"
@cp vendor/ffmpeg/linux/ffprobe payload/linux/bin/ 2>/dev/null && chmod +x payload/linux/bin/ffprobe || echo "!! WARN: ffprobe 缺失"
@cp vendor/ffmpeg/linux/LICENSE.txt payload/linux/bin/ffmpeg-LICENSE.txt 2>/dev/null || true
$(call copy_bundled_data,payload/linux/data)
@if [ -d server/scripts ]; then cp -R server/scripts/. payload/linux/scripts/; fi
@if [ ! -f vendor/python/linux/python.tar.gz ]; then \
echo "!! ERROR: vendor/python/linux/python.tar.gz 不存在vendor-python-linux 應該已先跑過 !!"; \
exit 1; \
fi
@cp vendor/python/linux/python.tar.gz payload/linux/python/
@cp vendor/wheels/linux/*.whl payload/linux/wheels/ 2>/dev/null || true
@wheel_count=$$(ls -1 payload/linux/wheels/*.whl 2>/dev/null | wc -l); \
if [ "$$wheel_count" -lt 4 ]; then \
echo "!! ERROR: payload/linux/wheels 只找到 $$wheel_count 個 wheel預期至少 4 個numpy / opencv / pyusb / kp"; \
echo " 請檢查 vendor/wheels/linux/ 並重跑 make vendor-wheels-linux"; \
exit 1; \
fi
@echo "==> Linux payload 完成:包含 $$(ls payload/linux/wheels/*.whl 2>/dev/null | wc -l) 個 wheel + python tarball"
@du -sh payload/linux 2>/dev/null || true
@echo ""
@echo "payload/linux 結構:"
@find payload/linux -maxdepth 2 -type d 2>/dev/null | sed 's|^| |'
# ── Wails build ────────────────────────────────────────────────────
wails-macos: stage-macos ## wails build darwin/amd64 → visiona-local/build/bin/visiona-local.app
cd visiona-local && wails build -platform darwin/amd64 -clean
cp -R visiona-local/build/darwin/Resources/bin \
visiona-local/build/darwin/Resources/data \
visiona-local/build/darwin/Resources/scripts \
visiona-local/build/darwin/Resources/python \
visiona-local/build/darwin/Resources/wheels \
visiona-local/build/bin/visiona-local.app/Contents/Resources/
codesign --force --deep --sign - visiona-local/build/bin/visiona-local.app
codesign --verify --verbose visiona-local/build/bin/visiona-local.app
@du -sh visiona-local/build/bin/visiona-local.app
wails-windows: stage-windows ## ⚠️ 必須在 Windows runner 上執行wails build -platform windows/amd64
@case "$$(uname -s 2>/dev/null)" in \
MINGW*|CYGWIN*|MSYS*) : ;; \
*) \
echo ""; \
echo "❌ wails-windows 只能在 Windows runner 上 build偵測到 $$(uname -s)"; \
echo " 請在 Windows 機器上執行此 target或用 CI 的 Windows runner"; \
echo " 若只是要檢查前置步驟,可單獨跑 make stage-windows"; \
echo ""; \
exit 1 ;; \
esac
cd visiona-local && wails build -platform windows/amd64 -clean
@du -sh visiona-local/build/bin/visiona-local.exe
wails-linux: payload-linux ## ⚠️ 必須在 Linux runner 上執行wails build -platform linux/amd64
@if [ "$$(uname -s)" != "Linux" ]; then \
echo ""; \
echo "❌ wails-linux 只能在 Linux 上 build偵測到 $$(uname -s)"; \
echo " 請在 Linux x86_64 runnerGitHub Actions ubuntu-latest 即可)執行"; \
echo " 若只是要檢查前置步驟,可單獨跑 make payload-linux"; \
echo ""; \
exit 1; \
fi
# webkit2gtk-4.0 從 Ubuntu 22.10+ / Debian 12+ 起被 webkit2gtk-4.1 取代。
# 偵測 pkg-config 哪個存在:優先用 4.1(加 -tags webkit2_414.0 則
# 不加 tagWails 預設)。
@if pkg-config --exists webkit2gtk-4.1 2>/dev/null; then \
echo "==> webkit2gtk-4.1 detected → wails build with -tags webkit2_41"; \
cd visiona-local && wails build -platform linux/amd64 -clean -tags webkit2_41; \
elif pkg-config --exists webkit2gtk-4.0 2>/dev/null; then \
echo "==> webkit2gtk-4.0 detected → wails build (default)"; \
cd visiona-local && wails build -platform linux/amd64 -clean; \
else \
echo ""; \
echo "❌ 找不到 webkit2gtk-4.0 或 webkit2gtk-4.1 dev header"; \
echo " 請執行sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev"; \
echo " (舊版 Ubuntu 20.04 可改用 libwebkit2gtk-4.0-dev"; \
echo ""; \
exit 1; \
fi
@du -sh visiona-local/build/bin/visiona-local
# ── 安裝檔打包 ─────────────────────────────────────────────────────
dmg: wails-macos ## 美化 DMGcreate-dmg 有裝)或 plain DMGfallback→ dist/visiona-local.dmg
@mkdir -p $(DIST)
@rm -f $(DIST)/visiona-local.dmg
@if command -v create-dmg > /dev/null 2>&1; then \
$(MAKE) --no-print-directory dmg-fancy; \
else \
echo "⚠️ create-dmg 未安裝,使用 plain DMGhdiutil UDZO"; \
echo " 想要美化版本請執行brew install create-dmg"; \
$(MAKE) --no-print-directory dmg-plain; \
fi
@du -sh $(DIST)/visiona-local.dmg
@file $(DIST)/visiona-local.dmg
dmg-plain: ## hdiutil UDZO → dist/visiona-local.dmg無背景圖CI / fallback 用)
@mkdir -p $(DIST)
rm -f $(DIST)/visiona-local.dmg
hdiutil create -volname "visionA-local" \
-srcfolder visiona-local/build/bin/visiona-local.app \
-ov -format UDZO \
$(DIST)/visiona-local.dmg
dmg-fancy: ## create-dmg 美化版 → dist/visiona-local.dmg需 brew install create-dmg
@if [ ! -d visiona-local/build/bin/visiona-local.app ]; then \
echo "❌ visiona-local/build/bin/visiona-local.app 不存在,請先跑 make wails-macos"; exit 1; \
fi
@if ! command -v create-dmg > /dev/null 2>&1; then \
echo "❌ create-dmg 未安裝請執行brew install create-dmg"; exit 1; \
fi
@mkdir -p $(DIST)
rm -f $(DIST)/visiona-local.dmg
create-dmg \
--volname "visionA-local" \
--background installer/macos/background.png \
--window-pos 200 120 \
--window-size 640 400 \
--icon-size 128 \
--icon "visiona-local.app" 180 200 \
--app-drop-link 460 200 \
--hide-extension "visiona-local.app" \
--no-internet-enable \
$(DIST)/visiona-local.dmg \
visiona-local/build/bin/visiona-local.app
exe-only: ## 只跑 iscc 打包 installer前置產物必須已存在不重 build wails/payload
@if [ ! -f visiona-local/build/bin/visiona-local.exe ]; then \
echo "❌ visiona-local/build/bin/visiona-local.exe 不存在,請先跑 make wails-windows"; exit 1; \
fi
@if [ ! -f payload/windows/bin/visiona-local-server.exe ]; then \
echo "❌ payload/windows/bin/visiona-local-server.exe 不存在,請先跑 make payload-windows"; exit 1; \
fi
@$(MAKE) --no-print-directory _run-iscc
exe: wails-windows _run-iscc ## ⚠️ 必須在 Windows 上跑Inno Setup → dist/visiona-local-*-windows-x64.exe
_run-iscc:
@echo "DEBUG _run-iscc: ISCC='$$ISCC'"
@echo "DEBUG _run-iscc: PATH first entries: $$(echo $$PATH | tr ':' '\n' | head -5)"
@ISCC_BIN="$$ISCC"; \
if [ -z "$$ISCC_BIN" ]; then \
if command -v iscc > /dev/null 2>&1; then ISCC_BIN=iscc; \
elif command -v iscc.exe > /dev/null 2>&1; then ISCC_BIN=iscc.exe; \
else \
for p in "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" \
"/c/Program Files/Inno Setup 6/ISCC.exe" \
"$$LOCALAPPDATA/Programs/Inno Setup 6/ISCC.exe" \
"$$USERPROFILE/AppData/Local/Programs/Inno Setup 6/ISCC.exe" \
"/c/Program Files (x86)/Inno Setup 5/ISCC.exe"; do \
if [ -f "$$p" ]; then ISCC_BIN="$$p"; break; fi; \
done; \
fi; \
fi; \
ISCC_OK=0; \
if [ -n "$$ISCC_BIN" ]; then \
if [ -e "$$ISCC_BIN" ] || command -v "$$ISCC_BIN" > /dev/null 2>&1; then ISCC_OK=1; fi; \
fi; \
if [ "$$ISCC_OK" = "0" ]; then \
echo ""; \
echo "❌ Inno Setup Compiler (iscc) 未安裝 / 找不到"; \
echo " 已嘗試偵測的路徑:"; \
echo " - \$$ISCC 環境變數"; \
echo " - PATH 上的 iscc / iscc.exe"; \
echo " - /c/Program Files (x86)/Inno Setup 6/ISCC.exe"; \
echo " - /c/Program Files/Inno Setup 6/ISCC.exe"; \
echo " 請從 https://jrsoftware.org/isdl.php 下載並安裝 Inno Setup 6"; \
echo " 或設定 ISCC 環境變數指向 ISCC.exe 絕對路徑"; \
echo ""; \
exit 1; \
fi; \
echo "==> 使用 ISCC: $$ISCC_BIN"; \
mkdir -p $(DIST); \
echo "==> 執行 iscccwd: $$(pwd)..."; \
"$$ISCC_BIN" installer/windows/visiona-local.iss; \
ISCC_RC=$$?; \
echo "==> iscc exit code: $$ISCC_RC"; \
if [ $$ISCC_RC -ne 0 ]; then exit $$ISCC_RC; fi
@echo "==> 產出:"
@ls -lh $(DIST)/ 2>/dev/null || echo "dist 目錄不存在"
@ls -lh $(DIST)/visiona-local-*-windows-x64.exe 2>/dev/null || echo "未找到 .exe 產出檔"
appimage: wails-linux ## ⚠️ 必須在 Linux 上跑build-appimage.sh → dist/visiona-local-*-linux-x64.AppImage
@if [ "$$(uname -s)" != "Linux" ]; then \
echo ""; \
echo "❌ appimage 只能在 Linux 上 build偵測到 $$(uname -s)"; \
echo " 需要 appimagetool請在 Linux x86_64 runner 執行"; \
echo ""; \
exit 1; \
fi
@mkdir -p $(DIST)
VERSION=$(VERSION) bash installer/linux/build-appimage.sh
@echo "==> 產出:"
@ls -lh $(DIST)/visiona-local-*-linux-x64.AppImage 2>/dev/null || echo "未找到產出檔"
# ── 開發placeholder ────────────────────────────────────────────
dev:
@echo "TODO: make -j2 dev-server dev-frontend開發模式"
test:
@echo "TODO: go test + pnpm test"
lint:
@echo "TODO: go vet + pnpm lint"
fmt:
@echo "TODO: go fmt"
# ── 清理 ───────────────────────────────────────────────────────────
clean: ## 清除 dist/ 與 payload/ 產物
@echo "Cleaning dist/ and payload artifacts..."
rm -rf $(DIST)
rm -rf $(PAYLOAD)
@mkdir -p $(DIST) $(PAYLOAD)
@touch $(DIST)/.gitkeep $(PAYLOAD)/.gitkeep
@echo "Done."
clean-all: clean ## 完整清除dist/ payload/ wails build/ frontend build/ server embed
@echo "==> 清除 Wails build 產物..."
rm -rf visiona-local/build/bin
rm -rf visiona-local/build/darwin/Resources
rm -rf visiona-local/build/windows/Resources
@echo "==> 清除 frontend build 產物..."
rm -rf frontend/out
rm -rf frontend/.next
@echo "==> 清除 server 內嵌前端..."
rm -rf server/web/out
@echo "==> clean-all 完成"
clean-build-exe: clean-all exe ## Windows完整 clean + 從頭 build installer .exe最乾淨的 build
clean-build-dmg: clean-all dmg ## macOS完整 clean + 從頭 build installer .dmg
clean-build-appimage: clean-all appimage ## Linux完整 clean + 從頭 build installer .AppImage