返回 DeepSeek-Reasonix
verify-windows-portable.sh
根目录 / scripts / verify-windows-portable.sh
1 #!/usr/bin/env bash
2 # Verify the Windows portable release unit (versioned-v1) before packaging.
3 # This must run on the Windows staging directory: NTFS treats file names
4 # case-insensitively, so Reasonix.exe and reasonix.exe silently overwrite each
5 # other even though a source-level packaging test sees two different strings.
6 set -euo pipefail
7
8 staging="${1:?usage: verify-windows-portable.sh STAGING_DIR}"
9 [ -d "$staging" ] || { echo "Windows portable staging directory is missing: $staging" >&2; exit 1; }
10
11 # Root entry points for versioned-v1 (no Guard, no flat desktop/helper).
12 required_root=(
13 "Reasonix.exe"
14 "reasonix-cli.exe"
15 "reasonix-launcher.exe"
16 "current.json"
17 )
18
19 for expected in "${required_root[@]}"; do
20 [ -e "$staging/$expected" ] || {
21 echo "Windows portable root entry is missing: $expected" >&2
22 exit 1
23 }
24 done
25
26 # Exactly one active version directory under versions/v*.
27 version_dirs=()
28 if [ -d "$staging/versions" ]; then
29 while IFS= read -r -d '' d; do
30 version_dirs+=("$d")
31 done < <(find "$staging/versions" -mindepth 1 -maxdepth 1 -type d -name 'v*' -print0 2>/dev/null || true)
32 fi
33 if [ "${#version_dirs[@]}" -lt 1 ]; then
34 echo "Windows portable versions/ tree is missing an active version directory" >&2
35 exit 1
36 fi
37
38 # current.json must point at a real version directory.
39 active_dir=$(python3 - <<'PY' "$staging/current.json" 2>/dev/null || true
40 import json,sys
41 p=sys.argv[1]
42 with open(p) as f:
43 d=json.load(f)
44 print(d.get("activeDir",""))
45 PY
46 )
47 if [ -z "$active_dir" ]; then
48 # Fallback without python: require versions/v* with required members
49 active_dir=""
50 for d in "${version_dirs[@]}"; do
51 base=$(basename "$d")
52 if [ -f "$d/reasonix-desktop.exe" ] && [ -f "$d/reasonix-cli.exe" ] && [ -f "$d/reasonix-update-helper.exe" ]; then
53 active_dir="versions/$base"
54 break
55 fi
56 done
57 fi
58 [ -n "$active_dir" ] || {
59 echo "Windows portable current.json activeDir is empty or unreadable" >&2
60 exit 1
61 }
62 version_path="$staging/$active_dir"
63 [ -d "$version_path" ] || {
64 echo "Windows portable activeDir does not exist: $active_dir" >&2
65 exit 1
66 }
67 for name in reasonix-desktop.exe reasonix-cli.exe reasonix-update-helper.exe; do
68 [ -f "$version_path/$name" ] || {
69 echo "Windows portable version member is missing: $active_dir/$name" >&2
70 exit 1
71 }
72 done
73
74 # Guard must not persist in a normal portable layout.
75 if [ -e "$staging/reasonix-guard.exe" ] || [ -e "$staging/reasonix-desktop.exe" ]; then
76 echo "Windows portable must not ship flat reasonix-guard.exe or reasonix-desktop.exe at InstallRoot" >&2
77 exit 1
78 fi
79
80 # Reasonix.exe is the portable alias of the thin launcher.
81 cmp -s "$staging/Reasonix.exe" "$staging/reasonix-launcher.exe" || {
82 echo "Reasonix.exe is not the packaged GUI launcher" >&2
83 exit 1
84 }
85 if cmp -s "$staging/Reasonix.exe" "$staging/reasonix-cli.exe"; then
86 echo "Reasonix.exe was overwritten by the CLI sidecar" >&2
87 exit 1
88 fi
89
90 # Case-insensitive collision check among root exes.
91 actual=()
92 for path in "$staging"/*.exe; do
93 [ -f "$path" ] || continue
94 name="${path##*/}"
95 folded=$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')
96 if [ "${#actual[@]}" -gt 0 ]; then
97 for previous in "${actual[@]}"; do
98 previous_folded=$(printf '%s' "$previous" | tr '[:upper:]' '[:lower:]')
99 if [ "$folded" = "$previous_folded" ]; then
100 echo "Windows portable names collide case-insensitively: $previous and $name" >&2
101 exit 1
102 fi
103 done
104 fi
105 actual+=("$name")
106 done
107
107 lines BASH