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>
This commit is contained in:
jim800121chen 2026-07-22 22:15:26 +08:00
parent 61593f004d
commit a05d206c07

View File

@ -39,9 +39,34 @@ BUNDLED_NEFS := \
# copy_bundled_data把 server/data/ 複製到 $(1),但 nef/ 只帶 BUNDLED_NEFS 白名單。 # copy_bundled_data把 server/data/ 複製到 $(1),但 nef/ 只帶 BUNDLED_NEFS 白名單。
# $(1) = 目標 data 目錄payload/darwin/data # $(1) = 目標 data 目錄payload/darwin/data
# #
# 步驟:(a) 複製 server/data/ 下除了 nef/ 以外的所有東西models.json 等) # 步驟:(a) 先清空 $(1),確保這個 helper 是冪等的(見下方「為什麼要清空」)
# (b) 再逐一複製白名單內的 .nef # (b) 複製 server/data/ 下除了 nef/ 以外的所有東西models.json 等)
# (c) 白名單檔案不存在就直接 fail避免安靜地產出缺 model 的安裝包 # (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 + # 只用 POSIX find / cp不用 rsync —— Windows CI 跑在 Git Bashwindows-2022 +
# shell: bash該環境沒有 rsync。 # shell: bash該環境沒有 rsync。
@ -49,8 +74,27 @@ define copy_bundled_data
@set -e; \ @set -e; \
echo "==> 複製 server/data → $(1).nef 白名單:$(words $(BUNDLED_NEFS)) 個)"; \ echo "==> 複製 server/data → $(1).nef 白名單:$(words $(BUNDLED_NEFS)) 個)"; \
if [ ! -d server/data ]; then echo "!! ERROR: server/data 不存在 !!"; exit 1; fi; \ if [ ! -d server/data ]; then echo "!! ERROR: server/data 不存在 !!"; exit 1; fi; \
mkdir -p "$(1)"; \ target='$(strip $(1))'; \
dest="$$(cd "$(1)" && pwd)"; \ 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 && \ ( 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 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 ); \ find . -path ./nef -prune -o -type f -print | while read -r f; do cp "$$f" "$$dest/$$f"; done ); \