# Build Pipeline — visionA-local > Makefile、build 腳本、CI(選配)、跨平台建置策略。**無簽章。** --- ## 1. Makefile 骨架 ```makefile # visionA-local - Makefile .PHONY: help dev dev-mock build build-server build-frontend build-embed \ payload payload-macos payload-windows payload-linux \ installer installer-macos installer-windows installer-linux \ clean test lint fmt VERSION ?= v0.1.0 BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) OS := $(shell uname -s | tr A-Z a-z) DIST := dist # ── 幫助 ─────────────────────────────────────────────────── help: @echo "visionA-local - Available targets:" @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' # ── 開發 ─────────────────────────────────────────────────── dev: ## 開發模式(真實硬體 + 前後端 hot reload) @$(MAKE) -j2 dev-server dev-frontend dev-server: cd server && go run main.go --dev dev-mock: ## 開發模式(Mock) @$(MAKE) -j2 dev-mock-server dev-frontend dev-mock-server: cd server && go run main.go --dev --mock --mock-devices=3 --mock-camera dev-frontend: cd frontend && pnpm dev # ── Build(單元) ─────────────────────────────────────────── build: build-frontend build-embed build-server ## Build Go server binary (含 embedded Next.js) build-frontend: ## 編譯 Next.js 靜態檔 cd frontend && pnpm build build-embed: build-frontend @rm -rf server/web/out @mkdir -p server/web/out cp -r frontend/out/. server/web/out/ build-server: @mkdir -p $(DIST) cd server && go build \ -ldflags="-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)" \ -o ../$(DIST)/visiona-local-server main.go # ── Payload 準備 ──────────────────────────────────────────── payload: payload-$(OS) ## 依當前 OS 準備 payload payload-macos: build ## 準備 macOS 的 Wails payload @echo "Staging macOS payload..." @rm -rf visiona-local/payload @mkdir -p visiona-local/payload/{bin,data/nef/kl520,data/nef/kl720,scripts/wheels,python} cp $(DIST)/visiona-local-server visiona-local/payload/bin/ cp server/data/models.json visiona-local/payload/data/ cp server/data/nef/kl520/*.nef visiona-local/payload/data/nef/kl520/ cp server/data/nef/kl720/*.nef visiona-local/payload/data/nef/kl720/ cp server/scripts/kneron_bridge.py visiona-local/payload/scripts/ cp server/scripts/requirements.txt visiona-local/payload/scripts/ # Python runtime(python-build-standalone) cp vendor/python/cpython-3.12-macos-x64.tar.gz visiona-local/payload/python/ # Wheels cp vendor/wheels/macos-x64/*.whl visiona-local/payload/scripts/wheels/ # ffmpeg (LGPL static) cp vendor/ffmpeg/macos-x64/ffmpeg visiona-local/payload/bin/ chmod +x visiona-local/payload/bin/ffmpeg # yt-dlp cp vendor/yt-dlp/yt-dlp_macos visiona-local/payload/bin/yt-dlp chmod +x visiona-local/payload/bin/yt-dlp payload-windows: build @echo "Staging Windows payload..." @rm -rf visiona-local/payload @mkdir -p visiona-local/payload/{bin,data/nef/kl520,data/nef/kl720,scripts/wheels,python,drivers/amd64} cp $(DIST)/visiona-local-server.exe visiona-local/payload/bin/ cp server/data/models.json visiona-local/payload/data/ cp -r server/data/nef/* visiona-local/payload/data/nef/ cp server/scripts/kneron_bridge.py visiona-local/payload/scripts/ cp server/scripts/requirements.txt visiona-local/payload/scripts/ cp vendor/python/cpython-3.12-windows-x64.tar.gz visiona-local/payload/python/ cp vendor/wheels/windows-x64/*.whl visiona-local/payload/scripts/wheels/ cp vendor/ffmpeg/windows-x64/*.{exe,dll} visiona-local/payload/bin/ cp vendor/yt-dlp/yt-dlp.exe visiona-local/payload/bin/ # WinUSB driver cp vendor/drivers/kneron_winusb.inf visiona-local/payload/drivers/ cp vendor/drivers/amd64/*.dll visiona-local/payload/drivers/amd64/ cp vendor/drivers/libusb-1.0.dll visiona-local/payload/bin/ payload-linux: build @echo "Staging Linux payload..." @rm -rf visiona-local/payload @mkdir -p visiona-local/payload/{bin,data/nef/kl520,data/nef/kl720,scripts/wheels,python} cp $(DIST)/visiona-local-server visiona-local/payload/bin/ cp server/data/models.json visiona-local/payload/data/ cp -r server/data/nef/* visiona-local/payload/data/nef/ cp server/scripts/kneron_bridge.py visiona-local/payload/scripts/ cp server/scripts/requirements.txt visiona-local/payload/scripts/ cp vendor/python/cpython-3.12-linux-x64.tar.gz visiona-local/payload/python/ cp vendor/wheels/linux-x64/*.whl visiona-local/payload/scripts/wheels/ cp vendor/ffmpeg/linux-x64/ffmpeg visiona-local/payload/bin/ chmod +x visiona-local/payload/bin/ffmpeg cp vendor/yt-dlp/yt-dlp visiona-local/payload/bin/ chmod +x visiona-local/payload/bin/yt-dlp # 內帶 libusb 供 AppImage 使用 cp /usr/lib/x86_64-linux-gnu/libusb-1.0.so.0 visiona-local/payload/bin/ # ── Installer 建置 ────────────────────────────────────────── installer: installer-$(OS) ## 依當前 OS 建 installer installer-macos: payload-macos cd visiona-local && wails build -platform darwin/amd64 -clean codesign --force --deep --sign - visiona-local/build/bin/visiona-local.app mkdir -p $(DIST) dmgbuild -s dmg-config.py "visionA-local" \ $(DIST)/visiona-local-$(VERSION)-macos-x64.dmg installer-windows: payload-windows cd visiona-local && wails build -platform windows/amd64 -clean -webview2 embed mkdir -p $(DIST) iscc /DVERSION=$(VERSION) visiona-local-installer.iss installer-linux: payload-linux cd visiona-local && wails build -platform linux/amd64 -clean bash scripts/build-appimage.sh $(VERSION) # ── 測試 / Lint ───────────────────────────────────────────── test: test-server test-frontend test-server: cd server && go test -v ./... test-frontend: cd frontend && pnpm test lint: cd server && go vet ./... cd frontend && pnpm lint fmt: cd server && go fmt ./... # ── 清理 ─────────────────────────────────────────────────── clean: rm -rf $(DIST) rm -rf frontend/.next frontend/out rm -rf server/web/out @mkdir -p server/web/out && touch server/web/out/.gitkeep rm -rf visiona-local/build visiona-local/payload @mkdir -p visiona-local/payload && touch visiona-local/payload/.gitkeep ``` --- ## 2. Vendor 目錄結構(離線打包依賴) **關鍵:所有第三方二進位都放進 `vendor/`,由建置腳本首次執行時透過 `make vendor-sync` 下載。一旦下載就 cache,之後 offline build。** ``` /Users/jimchen/visionA/local-tool/vendor/ ├── python/ ← python-build-standalone tarballs │ ├── cpython-3.12-macos-x64.tar.gz │ ├── cpython-3.12-windows-x64.tar.gz │ └── cpython-3.12-linux-x64.tar.gz ├── wheels/ ← 預先下載的 Python wheels │ ├── macos-x64/ │ │ ├── numpy-*.whl │ │ ├── opencv_python_headless-*.whl │ │ ├── pyusb-*.whl │ │ └── KneronPLUS-*.whl │ ├── windows-x64/ │ └── linux-x64/ ├── ffmpeg/ ← LGPL static builds │ ├── macos-x64/ffmpeg │ ├── windows-x64/{ffmpeg.exe, *.dll} │ └── linux-x64/ffmpeg ├── yt-dlp/ │ ├── yt-dlp_macos │ ├── yt-dlp.exe │ └── yt-dlp └── drivers/ ← Windows WinUSB driver ├── kneron_winusb.inf ├── libusb-1.0.dll └── amd64/ ``` ### 2.1 `make vendor-sync` 腳本(首次 setup 用) ```bash #!/usr/bin/env bash # scripts/vendor-sync.sh - 下載所有第三方依賴到 vendor/ set -e VENDOR=vendor mkdir -p $VENDOR/{python,wheels,ffmpeg,yt-dlp,drivers} # 1. python-build-standalone PBS_VERSION="20250317" PBS_PY="3.12.9" PBS_BASE="https://github.com/astral-sh/python-build-standalone/releases/download/$PBS_VERSION" curl -L -o $VENDOR/python/cpython-3.12-macos-x64.tar.gz \ "$PBS_BASE/cpython-$PBS_PY+$PBS_VERSION-x86_64-apple-darwin-install_only.tar.gz" curl -L -o $VENDOR/python/cpython-3.12-windows-x64.tar.gz \ "$PBS_BASE/cpython-$PBS_PY+$PBS_VERSION-x86_64-pc-windows-msvc-install_only.tar.gz" curl -L -o $VENDOR/python/cpython-3.12-linux-x64.tar.gz \ "$PBS_BASE/cpython-$PBS_PY+$PBS_VERSION-x86_64-unknown-linux-gnu-install_only.tar.gz" # 2. Python wheels(用每個平台的 Python 預先下載) # 注意:必須在對應平台上跑 pip download 才能拿到正確的 wheel tag # 這裡假設使用者已在 macOS 機器上跑過: # pip download -d vendor/wheels/macos-x64 \ # --platform macosx_11_0_x86_64 --python-version 3.12 --only-binary=:all: \ # numpy opencv-python-headless pyusb # KneronPLUS wheel 另外手動放進去(從 edge-ai-platform/installer/wheels/ 複製) cp -n ../edge-ai-platform/installer/wheels/macos/KneronPLUS*.whl $VENDOR/wheels/macos-x64/ 2>/dev/null || true cp -n ../edge-ai-platform/installer/wheels/linux/KneronPLUS*.whl $VENDOR/wheels/linux-x64/ 2>/dev/null || true cp -n ../edge-ai-platform/installer/wheels/windows/KneronPLUS*.whl $VENDOR/wheels/windows-x64/ 2>/dev/null || true # 3. ffmpeg LGPL FFMPEG_VER="7.1" # macOS curl -L -o /tmp/ffmpeg-mac.zip "https://evermeet.cx/ffmpeg/ffmpeg-$FFMPEG_VER.zip" unzip -o /tmp/ffmpeg-mac.zip -d $VENDOR/ffmpeg/macos-x64/ # Windows(BtbN LGPL build) curl -L -o /tmp/ffmpeg-win.zip \ "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n$FFMPEG_VER-latest-win64-lgpl-shared-$FFMPEG_VER.zip" # 解壓 ffmpeg.exe 與需要的 DLLs 到 vendor/ffmpeg/windows-x64/ # Linux curl -L -o /tmp/ffmpeg-linux.tar.xz \ "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" tar -xf /tmp/ffmpeg-linux.tar.xz -C /tmp/ cp /tmp/ffmpeg-*-amd64-static/ffmpeg $VENDOR/ffmpeg/linux-x64/ # 4. yt-dlp YTDLP_BASE="https://github.com/yt-dlp/yt-dlp/releases/latest/download" curl -L -o $VENDOR/yt-dlp/yt-dlp_macos "$YTDLP_BASE/yt-dlp_macos" curl -L -o $VENDOR/yt-dlp/yt-dlp.exe "$YTDLP_BASE/yt-dlp.exe" curl -L -o $VENDOR/yt-dlp/yt-dlp "$YTDLP_BASE/yt-dlp" chmod +x $VENDOR/yt-dlp/yt-dlp_macos $VENDOR/yt-dlp/yt-dlp # 5. WinUSB driver(從 edge-ai-platform 複製) cp -r ../edge-ai-platform/installer/drivers/* $VENDOR/drivers/ echo "✅ Vendor sync complete." ``` **vendor/ 是否進 git?(第三輪使用者決策 Q-D=D2:不進 git)** - **整個 `vendor/` 目錄加入 `.gitignore`**,不進版本控制 - 所有第三方二進位(Python tarball、ffmpeg、yt-dlp、KneronPLUS wheel、WinUSB driver 等)都由 `make vendor-sync` 從來源下載 - 開發者第一次 clone 後執行 `make vendor-sync` 即可取得完整 vendor tree - CI 亦在每次 build 前跑 `make vendor-sync`(可加 cache 加速) - 保留 `vendor/.gitkeep`(或 `vendor/README.md`)讓空目錄存在並說明取得方式 - 優點:repo 保持小;缺點:依賴外部下載來源可用性(由 R8 風險緩解處理 — pbs 下載 URL pin 版本 + 備援 mirror) **`.gitignore` 片段:** ```gitignore # 第三方依賴(由 make vendor-sync 下載,不進 git) /vendor/** !/vendor/.gitkeep !/vendor/README.md # 建置產出 /dist/ /visiona-local/build/ /visiona-local/payload/ ``` --- ## 3. CI 策略(選配,第一版可純手動) ### 3.1 建議:GitHub Actions matrix build ```yaml # .github/workflows/release.yml name: Release on: push: tags: - 'v*' jobs: build-macos: runs-on: macos-13 # Intel runner (x86_64) steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.22' - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'pnpm' cache-dependency-path: 'frontend/pnpm-lock.yaml' - name: Install Wails run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - name: Install dmgbuild run: pip install dmgbuild - name: Vendor sync run: make vendor-sync - name: Build installer run: make installer-macos VERSION=${{ github.ref_name }} - uses: actions/upload-artifact@v4 with: name: macos-installer path: dist/*.dmg build-windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 - name: Install Wails run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - name: Install Inno Setup run: choco install innosetup -y - run: make vendor-sync - run: make installer-windows VERSION=${{ github.ref_name }} - uses: actions/upload-artifact@v4 with: name: windows-installer path: dist/*.exe build-linux: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 - name: Install Wails deps run: | sudo apt-get update sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev \ libusb-1.0-0-dev - name: Install Wails run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - name: Install appimagetool run: | wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool-x86_64.AppImage sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool - run: make vendor-sync - run: make installer-linux VERSION=${{ github.ref_name }} - uses: actions/upload-artifact@v4 with: name: linux-installer path: dist/*.AppImage release: needs: [build-macos, build-windows, build-linux] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 - uses: softprops/action-gh-release@v2 with: files: | macos-installer/*.dmg windows-installer/*.exe linux-installer/*.AppImage ``` ### 3.2 第一版可以不用 CI MVP 階段可以**純手動**: - 在自己的 Mac 上 `make installer-macos` - 用 UTM / Parallels VM 跑 Windows 11 `make installer-windows` - 用 Docker / multipass VM 跑 Ubuntu 22.04 `make installer-linux` CI 可以等 M5 之後再做。 --- ## 4. 版本號管理 單一來源:Git tag `v1.0.0` → 透過 Makefile `VERSION` 變數傳入: - Go binary:`-ldflags "-X main.Version=$(VERSION)"` - Wails: 讀取 `wails.json` 的 `info.productVersion` - Installer 檔名:`visiona-local-v1.0.0-*` **建議版本策略:** - `v0.x.x`:MVP / alpha - `v1.0.0`:第一個對內部釋出的穩定版 - 每個 milestone 對應一個 minor(M1→v0.1.0、M2→v0.2.0、M6→v1.0.0) --- ## 5. 開發迭代速度優化 - **dev 模式不用 payload**:`make dev` 直接 `go run` server + `pnpm dev` 前端,不過 Wails app - **dev 模式用系統 Python**:`--python-mode=system` 避免每次都要解壓內嵌 runtime - **前端改動不用重 build server**:`go run main.go --dev` 時 `web/out` 不啟用,前端直接連 3000 port