From a05d206c07585ed377a1c3381272e2c0a663db98 Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Wed, 22 Jul 2026 22:15:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(build):=20copy=5Fbundled=5Fdata=20=E6=94=B9?= =?UTF-8?q?=E7=82=BA=E5=86=AA=E7=AD=89=EF=BC=8C=E6=B8=85=E6=8E=89=E5=89=8D?= =?UTF-8?q?=E6=AC=A1=20build=20=E7=9A=84=E6=AE=98=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- local-tool/Makefile | 54 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/local-tool/Makefile b/local-tool/Makefile index a89e930..9ea717c 100644 --- a/local-tool/Makefile +++ b/local-tool/Makefile @@ -39,9 +39,34 @@ BUNDLED_NEFS := \ # copy_bundled_data:把 server/data/ 複製到 $(1),但 nef/ 只帶 BUNDLED_NEFS 白名單。 # $(1) = 目標 data 目錄(例:payload/darwin/data) # -# 步驟:(a) 複製 server/data/ 下除了 nef/ 以外的所有東西(models.json 等) -# (b) 再逐一複製白名單內的 .nef -# (c) 白名單檔案不存在就直接 fail,避免安靜地產出缺 model 的安裝包 +# 步驟:(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//(因為 +# 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//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 Bash(windows-2022 + # shell: bash),該環境沒有 rsync。 @@ -49,8 +74,27 @@ 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; \ - mkdir -p "$(1)"; \ - dest="$$(cd "$(1)" && pwd)"; \ + 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//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 ); \