#!/usr/bin/env sh # build-test.sh — 偵測並執行專案的建置與測試,先建置再測試。 # 用法: # build-test.sh 偵測後實際執行 # build-test.sh --detect 只偵測,唯讀,不執行任何建置或測試 # ecosystem:nodejs | python | dotnet # 偵測方式: # nodejs → package.json 的 scripts.build 與 scripts.test(npm init 產生的佔位 test 視為沒有) # python → pytest 設定(pytest.ini、setup.cfg、tox.ini、pyproject.toml)或 tests 目錄與 test_*.py # dotnet → *.sln 或 *.csproj,建置與測試都用 dotnet # 為什麼要有 --detect:推不出指令(5)本來要等所有版本都改寫完才發現,使用者又答不出指令時, # 前面全數作廢還要走還原。--detect 讓呼叫方在動任何檔案之前先問出結果,工作區完全沒被動過。 # --detect 只做偵測與前置檢查,一個檔案都不寫。 # 輸出:底層指令的原始輸出;--detect 模式輸出偵測到的指令名稱,一行一個(nodejs 是 build、test, # python 是 pytest,dotnet 是 dotnet build、dotnet test)。 # 結束碼(慣例見 README「工具」章的結束碼總表): # 0 建置與測試都通過;--detect 模式是推得出指令 # 1 參數個數不對 # 2 ecosystem 不認識 # 4 需要的指令不存在(npm、python3、pytest、dotnet) # 5 推不出任何建置或測試指令——呼叫方要改問使用者 # 6 找不到專案目錄——呼叫方停手 # 其他 建置或測試失敗,帶回失敗指令的結束碼;--detect 模式不會回這一類 # 本工具不用 3:3 保留給「該 ecosystem 沒有來源檔案」,這支改用 5 表達推不出指令。 set -u DETECT=0 if [ "${1:-}" = "--detect" ]; then DETECT=1 shift fi if [ "$#" -ne 2 ]; then echo "用法:build-test.sh [--detect] " >&2 exit 1 fi eco="$1" DIR="$2" NO_COMMAND=5 [ -d "$DIR" ] || { echo "project dir not found: $DIR" >&2; exit 6; } case "$eco" in nodejs) [ -f "$DIR/package.json" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } # package.json 靠 python3 解析。少了 python3 會解出空字串,被誤判成推不出指令,所以先擋。 command -v python3 >/dev/null 2>&1 || { echo "python3 not found" >&2; exit 4; } scripts=$(python3 - "$DIR/package.json" <<'EOF' import json,sys try: d=json.load(open(sys.argv[1],encoding="utf-8")) except Exception: sys.exit(0) s=(d.get("scripts") or {}) if s.get("build"): print("build") t=s.get("test") or "" if t and "no test specified" not in t: print("test") EOF ) [ -n "$scripts" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; exit 4; } if [ "$DETECT" -eq 1 ]; then printf '%s\n' "$scripts" exit 0 fi for s in $scripts; do ( cd "$DIR" && npm run --if-present "$s" ) || exit $? done exit 0 ;; python) has_tests=0 for f in pytest.ini setup.cfg tox.ini pyproject.toml; do if [ -f "$DIR/$f" ] && grep -q 'pytest' "$DIR/$f" 2>/dev/null; then has_tests=1; fi done [ -d "$DIR/tests" ] && has_tests=1 if [ "$has_tests" -eq 0 ]; then found=$(find "$DIR" -maxdepth 3 -name 'test_*.py' 2>/dev/null | head -n 1) [ -n "$found" ] && has_tests=1 fi [ "$has_tests" -eq 1 ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } if command -v pytest >/dev/null 2>&1; then if [ "$DETECT" -eq 1 ]; then echo "pytest" exit 0 fi ( cd "$DIR" && pytest ) exit $? elif command -v python3 >/dev/null 2>&1 && python3 -m pytest --version >/dev/null 2>&1; then if [ "$DETECT" -eq 1 ]; then echo "python3 -m pytest" exit 0 fi ( cd "$DIR" && python3 -m pytest ) exit $? else echo "pytest not found" >&2 exit 4 fi ;; dotnet) projs=$(find "$DIR" -maxdepth 3 \( -name '*.sln' -o -name '*.csproj' \) 2>/dev/null) [ -n "$projs" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } command -v dotnet >/dev/null 2>&1 || { echo "dotnet not found" >&2; exit 4; } if [ "$DETECT" -eq 1 ]; then echo "dotnet build" echo "dotnet test" exit 0 fi ( cd "$DIR" && dotnet build ) || exit $? ( cd "$DIR" && dotnet test ) || exit $? exit 0 ;; *) echo "unknown ecosystem: $eco" >&2; exit 2 ;; esac