返回 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 [canonical|legacy-dual] [PAYLOAD_LAUNCHER]}"
9 layout="${2:-canonical}"
10 payload_launcher="${3:-}"
11 case "$layout" in
12 canonical|legacy-dual) ;;
13 *) echo "Unknown Windows portable layout: $layout" >&2; exit 1 ;;
14 esac
15 [ -d "$staging" ] || { echo "Windows portable staging directory is missing: $staging" >&2; exit 1; }
16
17 # Root entry points for versioned-v1 (no Guard, no flat desktop/helper).
18 required_root=(
19 "Reasonix.exe"
20 "reasonix-cli.exe"
21 "current.json"
22 )
23 if [ "$layout" = "legacy-dual" ]; then
24 required_root+=("reasonix-launcher.exe")
25 fi
26
27 for expected in "${required_root[@]}"; do
28 [ -f "$staging/$expected" ] && [ ! -L "$staging/$expected" ] || {
29 echo "Windows portable root entry is missing: $expected" >&2
30 exit 1
31 }
32 done
33
34 # Exactly one active version directory under versions/v*.
35 version_dirs=()
36 if [ -d "$staging/versions" ]; then
37 while IFS= read -r -d '' d; do
38 version_dirs+=("$d")
39 done < <(find "$staging/versions" -mindepth 1 -maxdepth 1 -type d -name 'v*' -print0 2>/dev/null || true)
40 fi
41 if [ "${#version_dirs[@]}" -ne 1 ]; then
42 echo "Windows portable must contain exactly one active version directory" >&2
43 exit 1
44 fi
45
46 # current.json must point at a real version directory.
47 active_dir=$(node -e '
48 const fs = require("node:fs");
49 const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
50 if (p.schemaVersion !== 1 || !/^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?$/.test(p.activeVersion) ||
51 p.activeDir !== `versions/${p.activeVersion}` ||
52 Object.keys(p).some(k => !["schemaVersion", "activeVersion", "activeDir"].includes(k))) {
53 throw new Error("Invalid portable current.json");
54 }
55 console.log(p.activeDir);
56 ' "$staging/current.json")
57 [ -n "$active_dir" ] || {
58 echo "Windows portable current.json activeDir is empty or unreadable" >&2
59 exit 1
60 }
61 version_path="$staging/$active_dir"
62 [ -d "$version_path" ] || {
63 echo "Windows portable activeDir does not exist: $active_dir" >&2
64 exit 1
65 }
66 for name in reasonix-desktop.exe reasonix-cli.exe reasonix-update-helper.exe; do
67 [ -f "$version_path/$name" ] || {
68 echo "Windows portable version member is missing: $active_dir/$name" >&2
69 exit 1
70 }
71 done
72
73 # The Electron bundle is the app/ tree member of the active version.
74 for name in "app/Reasonix.exe" "app/resources/app.asar" "app/resources/build.json" "app/resources/app/index.html" "app/resources/bin/reasonix-cli-launcher.exe"; do
75 [ -f "$version_path/$name" ] || {
76 echo "Windows portable app tree member is missing: $active_dir/$name" >&2
77 exit 1
78 }
79 done
80
81 # Guard must not persist in a normal portable layout.
82 if [ -e "$staging/reasonix-guard.exe" ] || [ -e "$staging/reasonix-desktop.exe" ]; then
83 echo "Windows portable must not ship flat reasonix-guard.exe or reasonix-desktop.exe at InstallRoot" >&2
84 exit 1
85 fi
86
87 # Historical packages must contain identical launcher entries. New artifacts
88 # must not regain the compatibility entry merely because it exists in staging.
89 if [ "$layout" = "legacy-dual" ]; then
90 cmp -s "$staging/Reasonix.exe" "$staging/reasonix-launcher.exe" || {
91 echo "Reasonix.exe differs from the legacy GUI entry" >&2; exit 1
92 }
93 fi
94 if [ -n "$payload_launcher" ]; then
95 cmp -s "$staging/Reasonix.exe" "$payload_launcher" || {
96 echo "Reasonix.exe does not match the payload launcher" >&2; exit 1
97 }
98 fi
99 if cmp -s "$staging/Reasonix.exe" "$staging/reasonix-cli.exe"; then
100 echo "Reasonix.exe was overwritten by the CLI sidecar" >&2
101 exit 1
102 fi
103 cmp -s "$staging/reasonix-cli.exe" "$version_path/app/resources/bin/reasonix-cli-launcher.exe" || {
104 echo "reasonix-cli.exe is not the packaged CLI forwarding entry" >&2
105 exit 1
106 }
107 if cmp -s "$staging/reasonix-cli.exe" "$version_path/reasonix-cli.exe"; then
108 echo "Windows portable duplicated the full CLI at InstallRoot" >&2
109 exit 1
110 fi
111
112 # Case-insensitive collision check among root exes.
113 actual=()
114 while IFS= read -r -d '' path; do
115 [ -f "$path" ] && [ ! -L "$path" ] || { echo "Invalid Windows root entry: $path" >&2; exit 1; }
116 name="${path##*/}"
117 case "$name" in
118 Reasonix.exe|reasonix-cli.exe) ;;
119 reasonix-launcher.exe) [ "$layout" = "legacy-dual" ] || { echo "Unexpected legacy launcher" >&2; exit 1; } ;;
120 *) echo "Unexpected Windows root executable: $name" >&2; exit 1 ;;
121 esac
122 folded=$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')
123 if [ "${#actual[@]}" -gt 0 ]; then
124 for previous in "${actual[@]}"; do
125 previous_folded=$(printf '%s' "$previous" | tr '[:upper:]' '[:lower:]')
126 if [ "$folded" = "$previous_folded" ]; then
127 echo "Windows portable names collide case-insensitively: $previous and $name" >&2
128 exit 1
129 fi
130 done
131 fi
132 actual+=("$name")
133 done < <(find "$staging" -maxdepth 1 \( -iname '*.exe' -o -iname '*.dll' \) -print0)
134
134 lines BASH