#!/usr/bin/env sh # apply-version.sh — 改寫版本來源檔案,把套件釘選到指定版本。 # 用法:apply-version.sh # ecosystem:nodejs | python | dotnet # 對應檔案(與 list-packages.sh 相同的解析邏輯): # nodejs → package.json(dependencies / devDependencies) # python → requirements.txt(優先)或 pyproject.toml 的 [project] dependencies # dotnet → *.csproj 的 PackageReference # 結束碼(慣例見 README「工具」章的結束碼總表): # 0 改寫成功 # 1 參數個數不對 # 2 ecosystem 不認識 # 3 找不到對應的版本來源檔案——呼叫方跳過這個套件 # 4 檔案中找不到該套件——呼叫方跳過這個套件 # 5 需要的指令不存在(python3) # 6 找不到專案目錄——呼叫方停手,不要當成跳過 set -u if [ "$#" -ne 4 ]; then echo "用法:apply-version.sh " >&2 exit 1 fi eco="$1" DIR="$2" name="$3" version="$4" # 先擋找不到專案目錄。少了這道,壞路徑會被報成「沒有版本來源檔案」,然後被當成跳過藏起來。 [ -d "$DIR" ] || { echo "project dir not found: $DIR" >&2; exit 6; } # 三種來源檔案都靠 python3 改寫。少了 python3 會被誤判成檔案中找不到該套件,所以先擋。 command -v python3 >/dev/null 2>&1 || { echo "python3 not found" >&2; exit 5; } case "$eco" in nodejs) [ -f "$DIR/package.json" ] || { echo "package.json not found in $DIR" >&2; exit 3; } python3 - "$DIR/package.json" "$name" "$version" <<'EOF' import json,sys path,name,version=sys.argv[1],sys.argv[2],sys.argv[3] d=json.load(open(path,encoding="utf-8")) found=False for k in ("dependencies","devDependencies"): if name in (d.get(k) or {}): d[k][name]=version found=True if not found: sys.exit(4) with open(path,"w",encoding="utf-8") as f: json.dump(d,f,indent=2,ensure_ascii=False) f.write("\n") EOF rc=$? [ "$rc" -eq 0 ] || { echo "$name not found in $DIR/package.json" >&2; exit "$rc"; } ;; python) if [ -f "$DIR/requirements.txt" ]; then python3 - "$DIR/requirements.txt" "$name" "$version" <<'EOF' import re,sys path,name,version=sys.argv[1],sys.argv[2],sys.argv[3] pat=re.compile(r"([A-Za-z0-9_.\-\[\]]+)\s*(?:(?:==|>=|<=|~=|!=|>|<)\s*([^,;\s]+))?") lname=name.split("[")[0].lower() lines=open(path,encoding="utf-8").readlines() out=[] found=False for line in lines: body=line.split("#")[0].strip() m=pat.match(body) if m and m.group(1).split("[")[0].lower()==lname: out.append(f"{m.group(1)}=={version}\n") found=True else: out.append(line) if not found: sys.exit(4) open(path,"w",encoding="utf-8").writelines(out) EOF rc=$? [ "$rc" -eq 0 ] || { echo "$name not found in $DIR/requirements.txt" >&2; exit "$rc"; } elif [ -f "$DIR/pyproject.toml" ]; then python3 - "$DIR/pyproject.toml" "$name" "$version" <<'EOF' import re,sys path,name,version=sys.argv[1],sys.argv[2],sys.argv[3] pat=re.compile(r"([A-Za-z0-9_.\-\[\]]+)\s*(?:(?:==|>=|<=|~=|!=|>|<)\s*([^,;\s\"']+))?") lname=name.split("[")[0].lower() lines=open(path,encoding="utf-8").readlines() out=[] found=False for line in lines: m=re.search(r'"([^"]+)"', line) if m: dep=m.group(1) dm=pat.match(dep) if dm and dm.group(1).split("[")[0].lower()==lname: newdep=f"{dm.group(1)}=={version}" line=line.replace(dep,newdep,1) found=True out.append(line) if not found: sys.exit(4) open(path,"w",encoding="utf-8").writelines(out) EOF rc=$? [ "$rc" -eq 0 ] || { echo "$name not found in $DIR/pyproject.toml" >&2; exit "$rc"; } else echo "requirements.txt or pyproject.toml not found in $DIR" >&2 exit 3 fi ;; dotnet) projs=$(find "$DIR" -maxdepth 3 -name '*.csproj' 2>/dev/null) [ -n "$projs" ] || { echo "*.csproj not found in $DIR" >&2; exit 3; } found=0 for proj in $projs; do python3 - "$proj" "$name" "$version" <<'EOF' import re,sys path,name,version=sys.argv[1],sys.argv[2],sys.argv[3] text=open(path,encoding="utf-8").read() hit=[False] def repl(m): tag=m.group(0) inc=re.search(r'Include="([^"]+)"',tag) if inc and inc.group(1)==name: hit[0]=True if re.search(r'Version="[^"]*"',tag): tag=re.sub(r'Version="[^"]*"', 'Version="%s"'%version, tag) elif tag.endswith("/>"): tag=tag[:-2]+' Version="%s" />'%version else: tag=tag[:-1]+' Version="%s">'%version return tag new_text=re.sub(r"]*>", repl, text) if not hit[0]: sys.exit(4) open(path,"w",encoding="utf-8").write(new_text) EOF if [ $? -eq 0 ]; then found=1 break fi done [ "$found" -eq 1 ] || { echo "PackageReference $name not found under $DIR" >&2; exit 4; } ;; *) echo "unknown ecosystem: $eco" >&2; exit 2 ;; esac exit 0