react-native-bspatch 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -151
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +0 -25
- package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +14 -6
- package/ios/OtaBundleResolver.swift +14 -6
- package/ios/RNBsPatch.mm +1 -28
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +29 -16
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/otaAssetResolver.js +0 -20
- package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
- package/lib/commonjs/ota/otaCore.js +200 -330
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/otaMetadata.js +21 -21
- package/lib/commonjs/ota/otaMetadata.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js +13 -15
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +30 -17
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/otaAssetResolver.js +1 -21
- package/lib/module/ota/otaAssetResolver.js.map +1 -1
- package/lib/module/ota/otaCore.js +197 -329
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/otaMetadata.js +19 -19
- package/lib/module/ota/otaMetadata.js.map +1 -1
- package/lib/module/ota/storage/GitHubRawStorage.js +13 -15
- package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +0 -1
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
- package/lib/typescript/ota/otaCore.d.ts +13 -3
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/otaMetadata.d.ts +9 -7
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
- package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
- package/lib/typescript/ota/types.d.ts +5 -19
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeBsPatch.ts +0 -5
- package/src/ota/OtaProvider.tsx +30 -23
- package/src/ota/otaAssetResolver.ts +1 -17
- package/src/ota/otaCore.ts +203 -457
- package/src/ota/otaMetadata.ts +30 -20
- package/src/ota/storage/GitHubRawStorage.ts +13 -21
- package/src/ota/types.ts +5 -20
- package/tools/build_ota.sh +222 -525
- package/tools/setup_project.js +17 -115
- package/tools/update_registry.js +31 -32
- package/tools/upload_ota.sh +61 -14
package/tools/build_ota.sh
CHANGED
|
@@ -3,36 +3,34 @@
|
|
|
3
3
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
4
4
|
# OTA Build Script — incremental, one version per run
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# v1 is the store build. Every OTA version ships a full bundle; patches are built
|
|
7
|
+
# only from earlier OTA versions (v2+), whose bundles are byte-identical on device.
|
|
8
|
+
# Clients on the store build download the full bundle once, then patch.
|
|
8
9
|
#
|
|
9
|
-
# Usage:
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# Usage (npx react-native-bspatch ...):
|
|
11
|
+
# patch [android|ios] [--flavor staging] [--release-version 1.0.4] [-y]
|
|
12
|
+
# → build + publish the next OTA (both platforms if omitted)
|
|
13
|
+
# reset [--flavor ...] [--push] → wipe this release's OTAs
|
|
14
|
+
# setup → wire OtaBundleResolver into AppDelegate / MainApplication
|
|
15
|
+
# version <x.y.z> → set the native app version (package.json, Gradle, Xcode)
|
|
16
|
+
# The release version is read from the native project and confirmed by a prompt (Enter keeps it).
|
|
13
17
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
14
18
|
|
|
15
19
|
set -e
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
# Resolve symlinks: npx runs us via node_modules/.bin/react-native-bspatch
|
|
21
|
+
SOURCE="$0"
|
|
22
|
+
while [ -L "$SOURCE" ]; do
|
|
23
|
+
LINK_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
24
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
25
|
+
case "$SOURCE" in /*) ;; *) SOURCE="$LINK_DIR/$SOURCE" ;; esac
|
|
26
|
+
done
|
|
27
|
+
SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
18
28
|
ROOT="$PWD"
|
|
19
|
-
BUNDLE_XCODE_IOS="$ROOT/ios/output/main.jsbundle"
|
|
20
29
|
BSDIFF="$SCRIPT_DIR/bsdiff"
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if [ -f "$ROOT/.env" ]; then
|
|
26
|
-
ENV_FILE="$ROOT/.env"
|
|
27
|
-
elif [ -f "$MODULE_DIR/.env" ]; then
|
|
28
|
-
ENV_FILE="$MODULE_DIR/.env"
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
if [ -n "$ENV_FILE" ]; then
|
|
32
|
-
echo "Loading environment variables from $(basename "$ENV_FILE")..."
|
|
33
|
-
set -a; . "$ENV_FILE"; set +a
|
|
34
|
-
else
|
|
35
|
-
echo " No .env file found in $ROOT or $MODULE_DIR"
|
|
31
|
+
if [ "$1" = "version" ]; then
|
|
32
|
+
shift
|
|
33
|
+
exec node "$SCRIPT_DIR/bump_version.js" "$@" --root "$ROOT"
|
|
36
34
|
fi
|
|
37
35
|
|
|
38
36
|
# ── Parameters ─────────────────────────────────────────────────────────────
|
|
@@ -40,43 +38,25 @@ FLAVOR="dev"
|
|
|
40
38
|
ACTION="ota"
|
|
41
39
|
PLATFORM_ARG=""
|
|
42
40
|
APP_VERSION_ARG=""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
SEED_APK=""
|
|
46
|
-
SEED_IPA=""
|
|
47
|
-
SEED_BUNDLE=""
|
|
48
|
-
FORCE_BASE=false
|
|
41
|
+
CONFIRM_VERSION=true
|
|
49
42
|
|
|
50
43
|
# Arg parsing
|
|
51
44
|
while [[ "$#" -gt 0 ]]; do
|
|
52
45
|
case $1 in
|
|
53
46
|
--platform) PLATFORM_ARG="$2"; shift ;;
|
|
54
47
|
--flavor) FLAVOR="$2"; shift ;;
|
|
55
|
-
--app-version) APP_VERSION_ARG="$2"; shift ;;
|
|
56
|
-
|
|
57
|
-
--ipa) SEED_IPA="$2"; ACTION="seed"; shift ;;
|
|
58
|
-
--bundle) SEED_BUNDLE="$2"; ACTION="seed"; shift ;;
|
|
59
|
-
--force) FORCE_BASE=true ;;
|
|
60
|
-
-y|--yes|--non-interactive) NON_INTERACTIVE=true ;;
|
|
48
|
+
--release-version|--app-version) APP_VERSION_ARG="$2"; shift ;;
|
|
49
|
+
-y|--yes|--non-interactive) CONFIRM_VERSION=false ;;
|
|
61
50
|
--push) PUSH_REMOTE=true ;;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
apk|ipa)
|
|
51
|
+
patch) ACTION="ota" ;;
|
|
52
|
+
android|ios) PLATFORM_ARG="$1" ;;
|
|
53
|
+
reset|setup) ACTION="$1" ;;
|
|
54
|
+
base|seed|apk|ipa|--apk|--ipa|*.apk|*.aab|*.ipa)
|
|
55
|
+
# Removed in 3.0: falling through would silently build and publish an OTA
|
|
56
|
+
echo "❌ '$1' was removed: the first OTA after a store release is a full bundle, no base/seed needed."
|
|
57
|
+
exit 1 ;;
|
|
66
58
|
dev|staging|production) FLAVOR="$1" ;; # Support positional flavor
|
|
67
|
-
|
|
68
|
-
*)
|
|
69
|
-
if [[ "$1" == *.apk ]] || [[ "$1" == *.aab ]]; then
|
|
70
|
-
SEED_APK="$1"
|
|
71
|
-
ACTION="seed"
|
|
72
|
-
elif [[ "$1" == *.ipa ]]; then
|
|
73
|
-
SEED_IPA="$1"
|
|
74
|
-
ACTION="seed"
|
|
75
|
-
elif [[ "$1" == *.hbc ]] || [[ "$1" == *.bundle ]] || [[ "$1" == *.jsbundle ]]; then
|
|
76
|
-
SEED_BUNDLE="$1"
|
|
77
|
-
ACTION="seed"
|
|
78
|
-
fi
|
|
79
|
-
;;
|
|
59
|
+
*) echo "❌ Unknown argument: $1"; exit 1 ;;
|
|
80
60
|
esac
|
|
81
61
|
shift
|
|
82
62
|
done
|
|
@@ -85,7 +65,39 @@ FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
|
85
65
|
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
86
66
|
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
87
67
|
|
|
88
|
-
# ──
|
|
68
|
+
# ── Environment ──────────────────────────────────────────────────────────────
|
|
69
|
+
# .env.<flavor>: OTA_BASE_URL / OTA_FOLDER, the same values react-native-config bakes into the app.
|
|
70
|
+
# .env.ota: upload secrets (OTA_R2_*). Never put them in .env.<flavor>: react-native-config
|
|
71
|
+
# ships every key of that file inside the APK/IPA.
|
|
72
|
+
# Parsed like react-native-config (KEY=VALUE, optional quotes), never executed:
|
|
73
|
+
# `APP_NAME=Baseform Dev` is valid dotenv but breaks `source`.
|
|
74
|
+
load_env() {
|
|
75
|
+
echo "Loading $(basename "$1")..."
|
|
76
|
+
local line k v
|
|
77
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
78
|
+
[[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=(.*)$ ]] || continue
|
|
79
|
+
k="${BASH_REMATCH[1]}"; v="${BASH_REMATCH[2]%$'\r'}"; v="${v#"${v%%[![:space:]]*}"}"
|
|
80
|
+
if [[ "$v" =~ ^\"(.*)\"$ ]] || [[ "$v" =~ ^\'(.*)\'$ ]]; then v="${BASH_REMATCH[1]}"; fi
|
|
81
|
+
export "$k=$v"
|
|
82
|
+
done < "$1"
|
|
83
|
+
}
|
|
84
|
+
if [ "$ACTION" != "setup" ]; then
|
|
85
|
+
if [ ! -f "$ROOT/.env.$FLAVOR_LOWER" ]; then
|
|
86
|
+
echo "❌ $ROOT/.env.$FLAVOR_LOWER not found (OTA_BASE_URL / OTA_FOLDER for flavor '$FLAVOR_LOWER')"
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
load_env "$ROOT/.env.$FLAVOR_LOWER"
|
|
90
|
+
if [ -f "$ROOT/.env.ota" ]; then load_env "$ROOT/.env.ota"; fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# ── Setup (project wiring only, no build env needed) ─────────────────────────
|
|
94
|
+
if [ "$ACTION" = "setup" ]; then
|
|
95
|
+
echo "Setting up react-native-bspatch in this project..."
|
|
96
|
+
node "$SCRIPT_DIR/setup_project.js" --root "$ROOT"
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# ── Build environment ──────────────────────────────────────────────────────
|
|
89
101
|
echo "Preparing build environment..."
|
|
90
102
|
# Kill any existing Metro processes to prevent file locks
|
|
91
103
|
pkill -f "react-native/local-cli/cli.js" || true
|
|
@@ -114,40 +126,47 @@ if [ "$ACTION" != "setup" ]; then
|
|
|
114
126
|
echo " Using URL: $BASE_URL"
|
|
115
127
|
echo " Using Folder: $BASE_FOLDER"
|
|
116
128
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
if [[ -n "${OTA_R2_BUCKET:-}" ]]; then
|
|
130
|
+
R2_URL="https://${OTA_R2_ACCOUNT_ID:?set OTA_R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${OTA_R2_BUCKET}"
|
|
131
|
+
r2() { curl -sS --aws-sigv4 "aws:amz:auto:s3" --user "${OTA_R2_ACCESS_KEY_ID:?}:${OTA_R2_SECRET_ACCESS_KEY:?}" "$@"; }
|
|
132
|
+
# S3 API, not the public URL: always the latest object, no CDN cache
|
|
133
|
+
API_HTTP=$(r2 -o "$REGISTRY_SHARED.tmp" -w "%{http_code}" "$R2_URL/$BASE_FOLDER/ota_registry.json" || echo 0)
|
|
134
|
+
else
|
|
135
|
+
if [[ "$BASE_URL" != "https://raw.githubusercontent.com/"* ]]; then
|
|
136
|
+
echo " ❌ Set OTA_R2_* (Cloudflare R2) or OTA_BASE_URL=https://raw.githubusercontent.com/<owner>/<repo>/<branch>"
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
CLEAN_URL="${BASE_URL#https://raw.githubusercontent.com/}"
|
|
140
|
+
GH_OWNER=$(echo "$CLEAN_URL" | cut -d'/' -f1)
|
|
141
|
+
GH_REPO=$(echo "$CLEAN_URL" | cut -d'/' -f2)
|
|
142
|
+
GH_BRANCH=$(echo "$CLEAN_URL" | cut -d'/' -f3)
|
|
143
|
+
if [[ -z "$GH_BRANCH" ]]; then GH_BRANCH="main"; fi
|
|
144
|
+
|
|
145
|
+
# Contents API (not the raw CDN) always returns the latest commit; the raw
|
|
146
|
+
# media type has no 1 MB limit and needs no base64 decoding.
|
|
147
|
+
GH_API_TOKEN="$(gh auth token 2>/dev/null || true)"
|
|
148
|
+
GH_API_TOKEN="${GH_API_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
|
|
149
|
+
GH_API_AUTH=()
|
|
150
|
+
if [[ -n "$GH_API_TOKEN" ]]; then GH_API_AUTH=(-H "Authorization: Bearer $GH_API_TOKEN"); fi
|
|
151
|
+
|
|
152
|
+
API_HTTP=$(curl -s -o "$REGISTRY_SHARED.tmp" -w "%{http_code}" "${GH_API_AUTH[@]}" \
|
|
153
|
+
-H "Accept: application/vnd.github.raw" \
|
|
154
|
+
"https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/contents/${BASE_FOLDER}/ota_registry.json?ref=${GH_BRANCH}" || echo 0)
|
|
121
155
|
fi
|
|
122
|
-
echo " Downloading remote registry from GitHub..."
|
|
123
|
-
|
|
124
|
-
CLEAN_URL="${BASE_URL#https://raw.githubusercontent.com/}"
|
|
125
|
-
GH_OWNER=$(echo "$CLEAN_URL" | cut -d'/' -f1)
|
|
126
|
-
GH_REPO=$(echo "$CLEAN_URL" | cut -d'/' -f2)
|
|
127
|
-
GH_BRANCH=$(echo "$CLEAN_URL" | cut -d'/' -f3)
|
|
128
|
-
if [[ -z "$GH_BRANCH" ]]; then GH_BRANCH="main"; fi
|
|
129
|
-
|
|
130
|
-
# Contents API (not the raw CDN) always returns the latest commit; the raw
|
|
131
|
-
# media type has no 1 MB limit and needs no base64 decoding.
|
|
132
|
-
GH_API_TOKEN="$(gh auth token 2>/dev/null || true)"
|
|
133
|
-
GH_API_TOKEN="${GH_API_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
|
|
134
|
-
GH_API_AUTH=()
|
|
135
|
-
if [[ -n "$GH_API_TOKEN" ]]; then GH_API_AUTH=(-H "Authorization: Bearer $GH_API_TOKEN"); fi
|
|
136
|
-
|
|
137
|
-
API_HTTP=$(curl -s -o "$REGISTRY_SHARED.tmp" -w "%{http_code}" "${GH_API_AUTH[@]}" \
|
|
138
|
-
-H "Accept: application/vnd.github.raw" \
|
|
139
|
-
"https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/contents/${BASE_FOLDER}/ota_registry.json?ref=${GH_BRANCH}" || echo 0)
|
|
140
156
|
|
|
141
157
|
if [[ "$API_HTTP" == "200" ]] && node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$REGISTRY_SHARED.tmp" 2>/dev/null; then
|
|
142
158
|
mv "$REGISTRY_SHARED.tmp" "$REGISTRY_SHARED"
|
|
143
|
-
echo " ✓ Remote registry synced
|
|
159
|
+
echo " ✓ Remote registry synced"
|
|
144
160
|
elif [[ "$API_HTTP" == "404" ]]; then
|
|
145
161
|
rm -f "$REGISTRY_SHARED.tmp"
|
|
146
|
-
|
|
162
|
+
# Remote is the source of truth: a local registry from another storage would publish
|
|
163
|
+
# entries for files that don't exist here
|
|
164
|
+
echo '{"apps":{}}' > "$REGISTRY_SHARED"
|
|
165
|
+
echo " Notice: No remote registry yet (HTTP 404) — starting a new one."
|
|
147
166
|
else
|
|
148
167
|
# Continuing would later push a stale local registry over the remote one
|
|
149
168
|
rm -f "$REGISTRY_SHARED.tmp"
|
|
150
|
-
echo " ❌ Registry sync failed (HTTP $API_HTTP). Check network /
|
|
169
|
+
echo " ❌ Registry sync failed (HTTP $API_HTTP). Check network / credentials and retry."
|
|
151
170
|
exit 1
|
|
152
171
|
fi
|
|
153
172
|
fi
|
|
@@ -156,8 +175,14 @@ fi
|
|
|
156
175
|
download_remote() {
|
|
157
176
|
local REMOTE_PATH="$1"
|
|
158
177
|
local DEST="$2"
|
|
159
|
-
|
|
160
|
-
if
|
|
178
|
+
local OK=0
|
|
179
|
+
if [[ -n "${OTA_R2_BUCKET:-}" ]]; then
|
|
180
|
+
r2 -f -o "$DEST.tmp" "$R2_URL/$REMOTE_PATH" 2>/dev/null || OK=1
|
|
181
|
+
else
|
|
182
|
+
# no-cache + timestamp query bypasses the GitHub CDN 5-min cache
|
|
183
|
+
curl -s -f -H "Cache-Control: no-cache" -o "$DEST.tmp" "$BASE_URL/$REMOTE_PATH?cb=$(date +%s)" || OK=1
|
|
184
|
+
fi
|
|
185
|
+
if [ $OK -eq 0 ]; then
|
|
161
186
|
mv "$DEST.tmp" "$DEST"
|
|
162
187
|
else
|
|
163
188
|
rm -f "$DEST.tmp"
|
|
@@ -170,110 +195,55 @@ sha256() {
|
|
|
170
195
|
shasum -a 256 "$1" | awk '{print $1}'
|
|
171
196
|
}
|
|
172
197
|
|
|
173
|
-
# ──
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.log(JSON.stringify({ version: match[1], source: 'android/app/build.gradle' }));
|
|
193
|
-
process.exit(0);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} catch (e) {}
|
|
197
|
-
|
|
198
|
-
// 2. Try ios project.pbxproj (most reliable for iOS release)
|
|
199
|
-
try {
|
|
200
|
-
const iosDir = path.join(root, 'ios');
|
|
201
|
-
if (fs.existsSync(iosDir)) {
|
|
202
|
-
const files = fs.readdirSync(iosDir);
|
|
203
|
-
for (const f of files) {
|
|
204
|
-
if (f.endsWith('.xcodeproj')) {
|
|
205
|
-
const pbxPath = path.join(iosDir, f, 'project.pbxproj');
|
|
206
|
-
if (fs.existsSync(pbxPath)) {
|
|
207
|
-
const content = fs.readFileSync(pbxPath, 'utf8');
|
|
208
|
-
const match = content.match(/MARKETING_VERSION\\s*=\\s*([^;]+);/);
|
|
209
|
-
if (match && match[1].trim() !== '0.0.0' && match[1].trim() !== '0.0.1') {
|
|
210
|
-
console.log(JSON.stringify({ version: match[1].trim(), source: 'ios/' + f }));
|
|
211
|
-
process.exit(0);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} catch (e) {}
|
|
218
|
-
|
|
219
|
-
// 3. Try package.json
|
|
220
|
-
try {
|
|
221
|
-
const pkgPath = path.join(root, 'package.json');
|
|
222
|
-
if (fs.existsSync(pkgPath)) {
|
|
223
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
224
|
-
if (pkg.version && pkg.version !== '0.0.0' && pkg.version !== '0.0.1') {
|
|
225
|
-
console.log(JSON.stringify({ version: pkg.version, source: 'package.json' }));
|
|
226
|
-
process.exit(0);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
} catch (e) {}
|
|
230
|
-
|
|
231
|
-
// 4. Fallback: src/ota/otaConfig.ts
|
|
232
|
-
try {
|
|
233
|
-
const otaConfigPath = path.join(root, 'src/ota/otaConfig.ts');
|
|
234
|
-
if (fs.existsSync(otaConfigPath)) {
|
|
235
|
-
const content = fs.readFileSync(otaConfigPath, 'utf8');
|
|
236
|
-
const match = content.match(/export const APP_VERSION = ['\"](.+?)['\"]/);
|
|
237
|
-
if (match) {
|
|
238
|
-
console.log(JSON.stringify({ version: match[1], source: 'src/ota/otaConfig.ts' }));
|
|
239
|
-
process.exit(0);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
} catch (e) {}
|
|
243
|
-
|
|
244
|
-
console.log(JSON.stringify({ version: 'unknown', source: 'none' }));
|
|
245
|
-
")
|
|
246
|
-
|
|
247
|
-
APP_VERSION=$(node -e "try { console.log(JSON.parse('$APP_VERSION_INFO').version); } catch(e) { console.log('unknown'); }")
|
|
248
|
-
APP_VERSION_SOURCE=$(node -e "try { console.log(JSON.parse('$APP_VERSION_INFO').source); } catch(e) { console.log('none'); }")
|
|
249
|
-
|
|
250
|
-
# Interactive confirmation / prompt:
|
|
251
|
-
# Prompt if running in an interactive terminal, not in reset mode, and no explicit flags passed
|
|
252
|
-
if [ -t 0 ] && [ -z "$APP_VERSION_ARG" ] && [ "$NON_INTERACTIVE" = false ] && [ "$ACTION" != "reset" ]; then
|
|
253
|
-
echo ""
|
|
254
|
-
echo "────────────────────────────────────────────────────────"
|
|
255
|
-
if [ "$APP_VERSION" != "unknown" ]; then
|
|
256
|
-
echo " Detected Native App Version: $APP_VERSION ($APP_VERSION_SOURCE)"
|
|
257
|
-
read -p " Enter App Version to build [Press Enter for '$APP_VERSION']: " USER_INPUT_VER
|
|
258
|
-
else
|
|
259
|
-
read -p " Enter Native App Version to build (e.g. 1.0.0): " USER_INPUT_VER
|
|
260
|
-
fi
|
|
261
|
-
echo "────────────────────────────────────────────────────────"
|
|
262
|
-
echo ""
|
|
198
|
+
# ── Progress helpers (exported: upload_ota.sh uses them too) ──────────────────
|
|
199
|
+
human_size() {
|
|
200
|
+
awk -v b="$1" 'BEGIN { split("B KB MB GB", u); i = 1; while (b >= 1024 && i < 4) { b /= 1024; i++ }
|
|
201
|
+
printf (i == 1 ? "%d %s" : "%.1f %s"), b, u[i] }'
|
|
202
|
+
}
|
|
203
|
+
file_size() { wc -c < "$1" | tr -d ' '; }
|
|
204
|
+
STEP=0
|
|
205
|
+
STEP_TOTAL=5
|
|
206
|
+
step() { STEP=$((STEP + 1)); STEP_START=$SECONDS; echo ""; echo " [$STEP/$STEP_TOTAL] $*"; }
|
|
207
|
+
step_done() { echo " ✓ $* ($((SECONDS - STEP_START))s)"; }
|
|
208
|
+
export -f human_size file_size
|
|
209
|
+
|
|
210
|
+
# ── Release version (native app version the OTA targets) ─────────────────────
|
|
211
|
+
android_version() {
|
|
212
|
+
sed -nE "s/^[[:space:]]*versionName[[:space:]]+[\"']([^\"']+)[\"'].*/\1/p" "$ROOT/android/app/build.gradle" 2>/dev/null | head -n 1
|
|
213
|
+
}
|
|
214
|
+
ios_version() {
|
|
215
|
+
grep -ho 'MARKETING_VERSION = [^;]*' "$ROOT"/ios/*.xcodeproj/project.pbxproj 2>/dev/null | head -n 1 | sed 's/.*= //'
|
|
216
|
+
}
|
|
263
217
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
218
|
+
if [ -n "$APP_VERSION_ARG" ]; then
|
|
219
|
+
APP_VERSION="$APP_VERSION_ARG"
|
|
220
|
+
elif [ "$PLATFORM_ARG" = "android" ]; then
|
|
221
|
+
APP_VERSION=$(android_version)
|
|
222
|
+
elif [ "$PLATFORM_ARG" = "ios" ]; then
|
|
223
|
+
APP_VERSION=$(ios_version)
|
|
224
|
+
else
|
|
225
|
+
AV=$(android_version); IV=$(ios_version)
|
|
226
|
+
if [ -n "$AV" ] && [ -n "$IV" ] && [ "$AV" != "$IV" ]; then
|
|
227
|
+
# One registry key per release version: publishing both under one would strand one platform
|
|
228
|
+
echo "❌ Android is $AV but iOS is $IV. Run 'patch android' and 'patch ios' separately."
|
|
229
|
+
exit 1
|
|
267
230
|
fi
|
|
231
|
+
APP_VERSION="${AV:-$IV}"
|
|
268
232
|
fi
|
|
269
233
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
echo "
|
|
273
|
-
|
|
234
|
+
# Confirm in a terminal (Enter keeps the detected version); CI / -y / --release-version skip it
|
|
235
|
+
if [ -t 0 ] && [ "$CONFIRM_VERSION" = true ] && [ -z "$APP_VERSION_ARG" ]; then
|
|
236
|
+
echo ""
|
|
237
|
+
read -r -p " Release version for ${PLATFORM_ARG:-android + ios} [${APP_VERSION:-none detected}]: " INPUT_VERSION
|
|
238
|
+
INPUT_VERSION="${INPUT_VERSION// /}"
|
|
239
|
+
APP_VERSION="${INPUT_VERSION:-$APP_VERSION}"
|
|
274
240
|
fi
|
|
275
241
|
|
|
276
|
-
|
|
242
|
+
if [ -z "$APP_VERSION" ]; then
|
|
243
|
+
echo "❌ Could not read the native app version. Pass --release-version <x.y.z>."
|
|
244
|
+
exit 1
|
|
245
|
+
fi
|
|
246
|
+
echo " ✓ Release version: $APP_VERSION"
|
|
277
247
|
|
|
278
248
|
# ── Reset ─────────────────────────────────────────────────────────────────────
|
|
279
249
|
if [ "$ACTION" = "reset" ]; then
|
|
@@ -285,8 +255,11 @@ if [ "$ACTION" = "reset" ]; then
|
|
|
285
255
|
try {
|
|
286
256
|
const fs = require('fs');
|
|
287
257
|
const reg = JSON.parse(fs.readFileSync('$REGISTRY_SHARED', 'utf8'));
|
|
288
|
-
|
|
289
|
-
|
|
258
|
+
const flavors = reg.apps?.['$APP_VERSION']?.flavors;
|
|
259
|
+
if (flavors?.['$FLAVOR_LOWER']) {
|
|
260
|
+
// Keep the counter: reused numbers would make clients on an old vN skip the new vN,
|
|
261
|
+
// and reused file names would serve stale cached bundles
|
|
262
|
+
flavors['$FLAVOR_LOWER'] = { latestOtaVersion: flavors['$FLAVOR_LOWER'].latestOtaVersion || 1, patches: [] };
|
|
290
263
|
}
|
|
291
264
|
fs.writeFileSync('$REGISTRY_SHARED', JSON.stringify(reg, null, 4));
|
|
292
265
|
} catch(e) {}
|
|
@@ -295,15 +268,13 @@ if [ "$ACTION" = "reset" ]; then
|
|
|
295
268
|
echo '{"apps":{}}' > "$REGISTRY_SHARED"
|
|
296
269
|
fi
|
|
297
270
|
|
|
298
|
-
# Delete only this app version + flavor
|
|
299
|
-
# the next build re-registers them automatically. Delete them by hand to re-seed.
|
|
271
|
+
# Delete only this app version + flavor
|
|
300
272
|
for D in "$ROOT/android/output" "$ROOT/ios/output"; do
|
|
301
|
-
find "$D" -maxdepth 1 -type f -name "*_${APP_VERSION}_*" -name "*_${FLAVOR_LOWER}[._]*"
|
|
302
|
-
-not -name "bundle_${APP_VERSION}_v1_*" -delete 2>/dev/null || true
|
|
273
|
+
find "$D" -maxdepth 1 -type f -name "*_${APP_VERSION}_*" -name "*_${FLAVOR_LOWER}[._]*" -delete 2>/dev/null || true
|
|
303
274
|
find "$D" -maxdepth 1 -type f -name "asset-manifest-v*_${FLAVOR_LOWER}.json" -delete 2>/dev/null || true # legacy names
|
|
304
275
|
done
|
|
305
276
|
|
|
306
|
-
echo " ✓ Registry and output cleared for $APP_VERSION/$FLAVOR_LOWER
|
|
277
|
+
echo " ✓ Registry and output cleared for $APP_VERSION/$FLAVOR_LOWER "
|
|
307
278
|
|
|
308
279
|
if [ "$PUSH_REMOTE" = true ]; then
|
|
309
280
|
echo " Syncing reset state for flavor '$FLAVOR_LOWER' to remote storage..."
|
|
@@ -312,13 +283,6 @@ if [ "$ACTION" = "reset" ]; then
|
|
|
312
283
|
exit 0
|
|
313
284
|
fi
|
|
314
285
|
|
|
315
|
-
# ── Setup ─────────────────────────────────────────────────────────────────────
|
|
316
|
-
if [ "$ACTION" = "setup" ]; then
|
|
317
|
-
echo "Setting up react-native-bspatch in this project..."
|
|
318
|
-
node "$SCRIPT_DIR/setup_project.js" --root "$ROOT"
|
|
319
|
-
exit 0
|
|
320
|
-
fi
|
|
321
|
-
|
|
322
286
|
# ── Ensure bsdiff compiled ───────────────────────────────────────────────────
|
|
323
287
|
if [ ! -f "$BSDIFF" ]; then
|
|
324
288
|
echo "Compiling bsdiff..."
|
|
@@ -358,12 +322,8 @@ build_platform() {
|
|
|
358
322
|
|
|
359
323
|
echo ""
|
|
360
324
|
echo ">>> Building $PLATFORM | Version: $APP_VERSION | OTA: v$CURRENT → v$NEXT"
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if [ -n "$IOS_VER" ] && [ "$IOS_VER" != "$APP_VERSION" ]; then
|
|
364
|
-
echo " ⚠ iOS MARKETING_VERSION is $IOS_VER but building for $APP_VERSION — iOS clients on $IOS_VER won't see this OTA"
|
|
365
|
-
fi
|
|
366
|
-
fi
|
|
325
|
+
STEP=0
|
|
326
|
+
local PLATFORM_START=$SECONDS
|
|
367
327
|
|
|
368
328
|
# ── Bundle ──
|
|
369
329
|
# Workspace for building
|
|
@@ -371,37 +331,23 @@ build_platform() {
|
|
|
371
331
|
mkdir -p "$WORKSPACE"
|
|
372
332
|
|
|
373
333
|
# Determine source bundle
|
|
334
|
+
local BUILD_LOG="$WORKSPACE/bundle.log"
|
|
374
335
|
if [ "$PLATFORM" == "android" ]; then
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
cd "$ROOT"
|
|
336
|
+
step "Bundling JS (Gradle createBundle${FLAVOR_CAP}ReleaseJsAndAssets)..."
|
|
337
|
+
# Gradle's config noise goes to a log; shown only on failure
|
|
338
|
+
if ! (cd "$ROOT/android" && ./gradlew ":app:createBundle${FLAVOR_CAP}ReleaseJsAndAssets" -Pkotlin.incremental=false --quiet) > "$BUILD_LOG" 2>&1; then
|
|
339
|
+
tail -n 40 "$BUILD_LOG"; echo " ❌ Gradle bundle failed (full log: $BUILD_LOG)"; exit 1
|
|
340
|
+
fi
|
|
378
341
|
BUNDLE_SOURCE="$ROOT/android/app/build/generated/assets/react/${FLAVOR_LOWER}Release/index.android.bundle"
|
|
379
342
|
else
|
|
380
|
-
|
|
381
|
-
if
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if [ -n "$XCODE_STALE" ] && [ "$ACTION" = "base" ]; then
|
|
385
|
-
echo " ⚠ Xcode bundle ($(stat -f '%Sm' "$BUNDLE_XCODE_IOS")) is older than $XCODE_STALE — ignoring it"
|
|
386
|
-
fi
|
|
387
|
-
fi
|
|
388
|
-
if [ -f "$BUNDLE_XCODE_IOS" ] && [ -z "$XCODE_STALE" ] && [ "$ACTION" = "base" ] && [ $CURRENT -eq 0 ]; then
|
|
389
|
-
# Base action: use Xcode-compiled bundle (already Hermes-compiled, matches IPA exactly)
|
|
390
|
-
echo " Using Xcode bundle as base source..."
|
|
391
|
-
cp "$BUNDLE_XCODE_IOS" "$WORKSPACE/source.bundle"
|
|
392
|
-
# Also discover assets for base manifest
|
|
393
|
-
echo " Discovering assets from project (shadow bundle)..."
|
|
394
|
-
npx react-native bundle --platform ios --dev false --entry-file index.js \
|
|
395
|
-
--bundle-output "$WORKSPACE/shadow.bundle.tmp" --assets-dest "$OUTPUT/assets"
|
|
396
|
-
rm -f "$WORKSPACE/shadow.bundle.tmp"
|
|
397
|
-
else
|
|
398
|
-
# OTA update: always generate fresh bundle from current code + discover assets
|
|
399
|
-
echo " Generating fresh iOS bundle from current code..."
|
|
400
|
-
npx react-native bundle --platform ios --dev false --entry-file index.js \
|
|
401
|
-
--bundle-output "$WORKSPACE/source.bundle" --assets-dest "$OUTPUT/assets"
|
|
343
|
+
step "Bundling JS (react-native bundle)..."
|
|
344
|
+
if ! npx react-native bundle --platform ios --dev false --entry-file index.js \
|
|
345
|
+
--bundle-output "$WORKSPACE/source.bundle" --assets-dest "$OUTPUT/assets" > "$BUILD_LOG" 2>&1; then
|
|
346
|
+
tail -n 40 "$BUILD_LOG"; echo " ❌ Bundle failed (full log: $BUILD_LOG)"; exit 1
|
|
402
347
|
fi
|
|
403
348
|
BUNDLE_SOURCE="$WORKSPACE/source.bundle"
|
|
404
349
|
fi
|
|
350
|
+
step_done "$(human_size "$(file_size "$BUNDLE_SOURCE")")"
|
|
405
351
|
|
|
406
352
|
# Ensure Hermes bytecode format (for both iOS and Android OTA bundles)
|
|
407
353
|
find_hermesc
|
|
@@ -412,14 +358,14 @@ build_platform() {
|
|
|
412
358
|
if [ "$FIRST_BYTE" == "c6" ]; then IS_HBC=true; fi
|
|
413
359
|
fi
|
|
414
360
|
|
|
361
|
+
step "Hermes bytecode..."
|
|
415
362
|
if [ "$IS_HBC" = true ]; then
|
|
416
|
-
echo "
|
|
363
|
+
echo " already Hermes bytecode"
|
|
417
364
|
else
|
|
418
|
-
echo " Compiling to Hermes bytecode..."
|
|
419
365
|
if [ -z "$HERMESC_PATH" ] || [ ! -f "$HERMESC_PATH" ]; then
|
|
420
366
|
echo " ❌ ERROR: hermesc not found"; exit 1
|
|
421
367
|
fi
|
|
422
|
-
# -O matches what Xcode/Gradle use for release
|
|
368
|
+
# -O matches what Xcode/Gradle use for release
|
|
423
369
|
"$HERMESC_PATH" -emit-binary -O -out "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
|
|
424
370
|
mv "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
|
|
425
371
|
fi
|
|
@@ -427,47 +373,20 @@ build_platform() {
|
|
|
427
373
|
local BUNDLE_NAME="bundle_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
428
374
|
local NEW_BUNDLE_PATH="$OUTPUT/$BUNDLE_NAME"
|
|
429
375
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
cd "$ROOT/android"
|
|
433
|
-
./gradlew "assemble${FLAVOR_CAP}Release" -Pkotlin.incremental=false --quiet
|
|
434
|
-
cd "$ROOT"
|
|
435
|
-
# Locate the APK dynamically as flavor names can vary in path
|
|
436
|
-
APK_PATH=$(find "$ROOT/android/app/build/outputs/apk" -name "app-${FLAVOR_LOWER}-release.apk" | head -n 1)
|
|
437
|
-
if [ -f "$APK_PATH" ]; then
|
|
438
|
-
unzip -p "$APK_PATH" assets/index.android.bundle > "$NEW_BUNDLE_PATH"
|
|
439
|
-
else
|
|
440
|
-
echo " ⚠ Warning: Could not find base APK at $APK_PATH. Using generated bundle directly."
|
|
441
|
-
cp "$BUNDLE_SOURCE" "$NEW_BUNDLE_PATH"
|
|
442
|
-
fi
|
|
443
|
-
else
|
|
444
|
-
cp "$BUNDLE_SOURCE" "$NEW_BUNDLE_PATH"
|
|
445
|
-
fi
|
|
376
|
+
cp "$BUNDLE_SOURCE" "$NEW_BUNDLE_PATH"
|
|
377
|
+
step_done "$BUNDLE_NAME, $(human_size "$(file_size "$NEW_BUNDLE_PATH")")"
|
|
446
378
|
|
|
447
379
|
# ── Assets ──
|
|
448
380
|
local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
449
|
-
local BASE_ASSET_MANIFEST="$OUTPUT/base-assets_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
450
381
|
local ASSET_STORE_DIR="$OUTPUT/asset-store"
|
|
451
382
|
mkdir -p "$ASSET_STORE_DIR"
|
|
452
383
|
local ASSETS_SRC_DIR="$ROOT/android/app/build/generated/res/react/${FLAVOR_LOWER}Release"
|
|
453
384
|
if [ "$PLATFORM" == "ios" ]; then ASSETS_SRC_DIR="$OUTPUT/assets"; fi
|
|
454
385
|
|
|
455
|
-
# Load base assets — try server if missing locally (e.g. after ota:clean)
|
|
456
|
-
if [ ! -f "$BASE_ASSET_MANIFEST" ]; then
|
|
457
|
-
local REMOTE_BASE_ASSET_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$(basename "$BASE_ASSET_MANIFEST")"
|
|
458
|
-
echo " Base asset manifest missing locally, downloading from server..."
|
|
459
|
-
download_remote "$REMOTE_BASE_ASSET_PATH" "$BASE_ASSET_MANIFEST" || true
|
|
460
|
-
if [ -f "$BASE_ASSET_MANIFEST" ]; then
|
|
461
|
-
echo " ✓ base-assets manifest restored from server"
|
|
462
|
-
else
|
|
463
|
-
echo " ⚠ base-assets manifest not found on server — all assets will be treated as new"
|
|
464
|
-
fi
|
|
465
|
-
fi
|
|
466
|
-
|
|
467
386
|
local ASSET_JSON_LIST="["
|
|
468
387
|
local FIRST=true
|
|
469
|
-
local
|
|
470
|
-
|
|
388
|
+
local ASSET_COUNT=0
|
|
389
|
+
step "Hashing assets..."
|
|
471
390
|
if [ -d "$ASSETS_SRC_DIR" ]; then
|
|
472
391
|
while IFS= read -r -d '' ASSET_FILE; do
|
|
473
392
|
local HASH=$(sha256 "$ASSET_FILE")
|
|
@@ -475,200 +394,68 @@ build_platform() {
|
|
|
475
394
|
local EX="${FNAME##*.}"
|
|
476
395
|
# Capture relative path for resolver matching
|
|
477
396
|
local REL_PATH="${ASSET_FILE#$ASSETS_SRC_DIR/}"
|
|
478
|
-
|
|
479
|
-
#
|
|
480
|
-
|
|
481
|
-
NEW_BASE_ASSETS+="\"$REL_PATH\": \"$HASH\""
|
|
482
|
-
|
|
483
|
-
# Optimization: Skip if hash matches the base APK version (client already has it)
|
|
484
|
-
local IS_IN_BASE=false
|
|
485
|
-
if [ -f "$BASE_ASSET_MANIFEST" ] && grep -qF "\"$REL_PATH\": \"$HASH\"" "$BASE_ASSET_MANIFEST"; then
|
|
486
|
-
IS_IN_BASE=true
|
|
487
|
-
fi
|
|
488
|
-
# Always add ALL assets to the manifest so the resolver can find them at runtime.
|
|
489
|
-
# Only skip the file copy if the asset is unchanged from base (avoids redundant uploads).
|
|
490
|
-
local ASSET_IS_BASE="false"
|
|
491
|
-
if [ "$IS_IN_BASE" != "true" ] || [ "$ACTION" = "base" ]; then
|
|
492
|
-
cp "$ASSET_FILE" "$ASSET_STORE_DIR/${HASH}.${EX}"
|
|
493
|
-
else
|
|
494
|
-
ASSET_IS_BASE="true"
|
|
495
|
-
fi
|
|
397
|
+
# ponytail: every asset ships via the content-addressed store (clients download each hash once);
|
|
398
|
+
# skipping assets already in the store binary needs a manifest of that binary
|
|
399
|
+
cp "$ASSET_FILE" "$ASSET_STORE_DIR/${HASH}.${EX}"
|
|
496
400
|
|
|
497
401
|
if [ "$FIRST" = true ]; then FIRST=false; else ASSET_JSON_LIST+=", "; fi
|
|
498
|
-
ASSET_JSON_LIST+="{\"hash\": \"$HASH\", \"ext\": \"$EX\", \"originalPath\": \"$REL_PATH\"
|
|
402
|
+
ASSET_JSON_LIST+="{\"hash\": \"$HASH\", \"ext\": \"$EX\", \"originalPath\": \"$REL_PATH\"}"
|
|
403
|
+
ASSET_COUNT=$((ASSET_COUNT + 1))
|
|
404
|
+
if [ -t 1 ]; then printf "\r %d assets" "$ASSET_COUNT"; fi
|
|
499
405
|
done < <(find "$ASSETS_SRC_DIR" -type f -print0 2>/dev/null)
|
|
406
|
+
if [ -t 1 ]; then printf "\r\033[K"; fi
|
|
500
407
|
fi
|
|
501
408
|
ASSET_JSON_LIST+="]"
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
# If base action, save the manifest to local disk for future comparisons
|
|
505
|
-
if [ "$ACTION" = "base" ]; then
|
|
506
|
-
echo "$NEW_BASE_ASSETS" > "$BASE_ASSET_MANIFEST"
|
|
507
|
-
echo " ✓ Base asset manifest saved locally"
|
|
508
|
-
fi
|
|
409
|
+
step_done "$ASSET_COUNT assets"
|
|
509
410
|
|
|
510
411
|
cat > "$ASSET_MANIFEST_FILE" << EOF
|
|
511
412
|
{ "version": $NEXT, "appVersion": "$APP_VERSION", "platform": "$PLATFORM", "flavor": "$FLAVOR_LOWER", "assets": $ASSET_JSON_LIST }
|
|
512
413
|
EOF
|
|
513
414
|
|
|
514
415
|
# ── Patches ──
|
|
515
|
-
|
|
416
|
+
# From the last 5 OTA versions only (never v1 = store build). Clients on the
|
|
417
|
+
# store build or further behind download the full bundle.
|
|
516
418
|
local NEW_HASH=$(sha256 "$NEW_BUNDLE_PATH")
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
local OLD_P="$OUTPUT/$OLD_B"
|
|
525
|
-
if [ ! -f "$OLD_P" ]; then
|
|
526
|
-
echo " Missing local bundle for v$i. Attempting to download from server..."
|
|
527
|
-
local REMOTE_BUNDLE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$OLD_B"
|
|
528
|
-
download_remote "$REMOTE_BUNDLE_PATH" "$OLD_P" || true
|
|
529
|
-
fi
|
|
530
|
-
if [ ! -f "$OLD_P" ]; then
|
|
531
|
-
echo " ⚠ Warning: Could not find or download bundle v$i. Skipping patch generation from v$i."
|
|
532
|
-
continue
|
|
533
|
-
fi
|
|
534
|
-
local PNAME="patch_${APP_VERSION}_v${i}_to_v${NEXT}_${FLAVOR_LOWER}_${PLATFORM}.patch"
|
|
535
|
-
"$BSDIFF" "$OLD_P" "$NEW_BUNDLE_PATH" "$OUTPUT/$PNAME"
|
|
536
|
-
if [ "$PATCHES_JSON" != "[" ]; then PATCHES_JSON+=", "; fi
|
|
537
|
-
PATCHES_JSON+="{\"from\": $i, \"to\": $NEXT, \"file\": \"$PNAME\", \"baseHash\": \"$(sha256 "$OLD_P")\", \"bundleHash\": \"$NEW_HASH\"}"
|
|
538
|
-
done
|
|
539
|
-
PATCHES_JSON+="]"
|
|
419
|
+
local PATCHES_JSON="["
|
|
420
|
+
local FIRST_FROM=$((NEXT - 5)); if [ $FIRST_FROM -lt 2 ]; then FIRST_FROM=2; fi
|
|
421
|
+
local PATCH_TOTAL=$((NEXT - FIRST_FROM)) PATCH_N=0 PATCH_MADE=0
|
|
422
|
+
if [ $PATCH_TOTAL -gt 0 ]; then
|
|
423
|
+
step "Creating $PATCH_TOTAL patch(es) from v$FIRST_FROM..v$((NEXT - 1))..."
|
|
424
|
+
else
|
|
425
|
+
step "Patches: none (first OTA of this release — clients take the full bundle)"
|
|
540
426
|
fi
|
|
427
|
+
for (( i=FIRST_FROM; i<NEXT; i++ )); do
|
|
428
|
+
PATCH_N=$((PATCH_N + 1))
|
|
429
|
+
local OLD_B="bundle_${APP_VERSION}_v${i}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
430
|
+
local OLD_P="$OUTPUT/$OLD_B"
|
|
431
|
+
if [ ! -f "$OLD_P" ]; then
|
|
432
|
+
echo " ($PATCH_N/$PATCH_TOTAL) v$i: downloading base bundle..."
|
|
433
|
+
local REMOTE_BUNDLE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$OLD_B"
|
|
434
|
+
download_remote "$REMOTE_BUNDLE_PATH" "$OLD_P" || true
|
|
435
|
+
fi
|
|
436
|
+
if [ ! -f "$OLD_P" ]; then
|
|
437
|
+
echo " ($PATCH_N/$PATCH_TOTAL) v$i: ⚠ bundle not found, skipped (v$i clients take the full bundle)"
|
|
438
|
+
continue
|
|
439
|
+
fi
|
|
440
|
+
local PNAME="patch_${APP_VERSION}_v${i}_to_v${NEXT}_${FLAVOR_LOWER}_${PLATFORM}.patch"
|
|
441
|
+
local PATCH_START=$SECONDS
|
|
442
|
+
if [ -t 1 ]; then printf " (%d/%d) v%d → v%d ..." "$PATCH_N" "$PATCH_TOTAL" "$i" "$NEXT"; fi
|
|
443
|
+
"$BSDIFF" "$OLD_P" "$NEW_BUNDLE_PATH" "$OUTPUT/$PNAME"
|
|
444
|
+
if [ -t 1 ]; then printf "\r\033[K"; fi
|
|
445
|
+
echo " ($PATCH_N/$PATCH_TOTAL) v$i → v$NEXT: $(human_size "$(file_size "$OUTPUT/$PNAME")") ($((SECONDS - PATCH_START))s)"
|
|
446
|
+
PATCH_MADE=$((PATCH_MADE + 1))
|
|
447
|
+
if [ "$PATCHES_JSON" != "[" ]; then PATCHES_JSON+=", "; fi
|
|
448
|
+
PATCHES_JSON+="{\"from\": $i, \"to\": $NEXT, \"file\": \"$PNAME\", \"baseHash\": \"$(sha256 "$OLD_P")\", \"bundleHash\": \"$NEW_HASH\"}"
|
|
449
|
+
done
|
|
450
|
+
PATCHES_JSON+="]"
|
|
451
|
+
if [ $PATCH_TOTAL -gt 0 ]; then step_done "$PATCH_MADE patch(es)"; fi
|
|
541
452
|
|
|
542
453
|
# ── Finalize ──
|
|
454
|
+
step "Registry + upload..."
|
|
543
455
|
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "$NEXT" "$PATCHES_JSON" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
|
|
544
456
|
bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "$NEXT" "$NEW_BUNDLE_PATH" "$PLATFORM"
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
seed_platform() {
|
|
548
|
-
local PLATFORM="$1"
|
|
549
|
-
local FLAVOR="$2"
|
|
550
|
-
local BINARY_PATH="$3"
|
|
551
|
-
local FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
552
|
-
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
553
|
-
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
554
|
-
|
|
555
|
-
local OUTPUT="$ROOT/android/output"
|
|
556
|
-
if [ "$PLATFORM" == "ios" ]; then OUTPUT="$ROOT/ios/output"; fi
|
|
557
|
-
mkdir -p "$OUTPUT"
|
|
558
|
-
|
|
559
457
|
echo ""
|
|
560
|
-
echo "
|
|
561
|
-
echo " Source binary: $BINARY_PATH"
|
|
562
|
-
|
|
563
|
-
if [ ! -f "$BINARY_PATH" ]; then
|
|
564
|
-
echo " ❌ ERROR: Source binary file not found: $BINARY_PATH"
|
|
565
|
-
exit 1
|
|
566
|
-
fi
|
|
567
|
-
|
|
568
|
-
local TARGET_BUNDLE="$OUTPUT/bundle_${APP_VERSION}_v1_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
569
|
-
local BASE_ASSET_MANIFEST="$OUTPUT/base-assets_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
570
|
-
local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest_${APP_VERSION}_v1_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
571
|
-
local TMP_DIR=$(mktemp -d)
|
|
572
|
-
|
|
573
|
-
if [ "$PLATFORM" == "android" ]; then
|
|
574
|
-
echo " Extracting index.android.bundle from Android binary..."
|
|
575
|
-
if unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "assets/index.android.bundle"; then
|
|
576
|
-
unzip -p "$BINARY_PATH" assets/index.android.bundle > "$TMP_DIR/extracted.bundle"
|
|
577
|
-
elif unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "base/assets/index.android.bundle"; then
|
|
578
|
-
unzip -p "$BINARY_PATH" base/assets/index.android.bundle > "$TMP_DIR/extracted.bundle"
|
|
579
|
-
elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]]; then
|
|
580
|
-
cp "$BINARY_PATH" "$TMP_DIR/extracted.bundle"
|
|
581
|
-
else
|
|
582
|
-
echo " ❌ ERROR: Could not find 'assets/index.android.bundle' inside $BINARY_PATH"
|
|
583
|
-
rm -rf "$TMP_DIR"
|
|
584
|
-
exit 1
|
|
585
|
-
fi
|
|
586
|
-
else
|
|
587
|
-
echo " Extracting main.jsbundle from iOS binary..."
|
|
588
|
-
local IPA_ENTRY=$(unzip -l "$BINARY_PATH" 2>/dev/null | grep -o 'Payload/[^/]*\.app/main\.jsbundle' | head -n 1)
|
|
589
|
-
if [ -n "$IPA_ENTRY" ]; then
|
|
590
|
-
unzip -p "$BINARY_PATH" "$IPA_ENTRY" > "$TMP_DIR/extracted.bundle"
|
|
591
|
-
elif unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "main.jsbundle"; then
|
|
592
|
-
local ANY_BUNDLE=$(unzip -l "$BINARY_PATH" 2>/dev/null | grep -o '[^ ]*main\.jsbundle' | head -n 1)
|
|
593
|
-
unzip -p "$BINARY_PATH" "$ANY_BUNDLE" > "$TMP_DIR/extracted.bundle"
|
|
594
|
-
elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]] || [[ "$BINARY_PATH" == *.jsbundle ]]; then
|
|
595
|
-
cp "$BINARY_PATH" "$TMP_DIR/extracted.bundle"
|
|
596
|
-
else
|
|
597
|
-
echo " ❌ ERROR: Could not find 'main.jsbundle' inside $BINARY_PATH"
|
|
598
|
-
rm -rf "$TMP_DIR"
|
|
599
|
-
exit 1
|
|
600
|
-
fi
|
|
601
|
-
fi
|
|
602
|
-
|
|
603
|
-
# Check Hermes bytecode format (0xc6 magic number)
|
|
604
|
-
FIRST_BYTE=$(od -An -N1 -t x1 "$TMP_DIR/extracted.bundle" | tr -d ' ' || echo "")
|
|
605
|
-
if [ "$FIRST_BYTE" == "c6" ]; then
|
|
606
|
-
echo " ✓ Bundle is verified Hermes bytecode format."
|
|
607
|
-
cp "$TMP_DIR/extracted.bundle" "$TARGET_BUNDLE"
|
|
608
|
-
else
|
|
609
|
-
echo " Bundle is JavaScript text. Compiling to Hermes bytecode..."
|
|
610
|
-
find_hermesc
|
|
611
|
-
if [ -z "$HERMESC_PATH" ] || [ ! -f "$HERMESC_PATH" ]; then
|
|
612
|
-
echo " ❌ ERROR: hermesc compiler not found. Cannot compile bundle to bytecode."
|
|
613
|
-
rm -rf "$TMP_DIR"
|
|
614
|
-
exit 1
|
|
615
|
-
fi
|
|
616
|
-
"$HERMESC_PATH" -emit-binary -O -out "$TARGET_BUNDLE" "$TMP_DIR/extracted.bundle"
|
|
617
|
-
fi
|
|
618
|
-
|
|
619
|
-
# Extract base assets manifest
|
|
620
|
-
echo " Scanning assets from binary for base manifest..."
|
|
621
|
-
local NEW_BASE_ASSETS="{"
|
|
622
|
-
local ASSET_JSON_LIST="["
|
|
623
|
-
local FIRST=true
|
|
624
|
-
|
|
625
|
-
if [ "$PLATFORM" == "android" ]; then
|
|
626
|
-
unzip -q -o "$BINARY_PATH" "res/drawable*/*" "res/raw/*" "base/res/drawable*/*" "base/res/raw/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
|
|
627
|
-
else
|
|
628
|
-
unzip -q -o "$BINARY_PATH" "Payload/*.app/assets/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
|
|
629
|
-
fi
|
|
630
|
-
|
|
631
|
-
if [ -d "$TMP_DIR/assets_unzipped" ]; then
|
|
632
|
-
while IFS= read -r -d '' ASSET_FILE; do
|
|
633
|
-
local HASH=$(sha256 "$ASSET_FILE")
|
|
634
|
-
local FNAME=$(basename "$ASSET_FILE")
|
|
635
|
-
local EX="${FNAME##*.}"
|
|
636
|
-
local REL_PATH="${ASSET_FILE#$TMP_DIR/assets_unzipped/}"
|
|
637
|
-
# Same keys as build_platform: iOS "assets/...", Android "drawable-*/..." / "raw/..."
|
|
638
|
-
REL_PATH=$(echo "$REL_PATH" | sed -E 's#^Payload/[^/]+\.app/##; s#^(base/)?res/##')
|
|
639
|
-
|
|
640
|
-
if [ "$NEW_BASE_ASSETS" != "{" ]; then NEW_BASE_ASSETS+=", "; fi
|
|
641
|
-
NEW_BASE_ASSETS+="\"$REL_PATH\": \"$HASH\""
|
|
642
|
-
|
|
643
|
-
if [ "$FIRST" = true ]; then FIRST=false; else ASSET_JSON_LIST+=", "; fi
|
|
644
|
-
ASSET_JSON_LIST+="{\"hash\": \"$HASH\", \"ext\": \"$EX\", \"originalPath\": \"$REL_PATH\", \"isBase\": true}"
|
|
645
|
-
done < <(find "$TMP_DIR/assets_unzipped" -type f -print0 2>/dev/null)
|
|
646
|
-
fi
|
|
647
|
-
|
|
648
|
-
NEW_BASE_ASSETS+="}"
|
|
649
|
-
ASSET_JSON_LIST+="]"
|
|
650
|
-
|
|
651
|
-
echo "$NEW_BASE_ASSETS" > "$BASE_ASSET_MANIFEST"
|
|
652
|
-
cat > "$ASSET_MANIFEST_FILE" << EOF
|
|
653
|
-
{ "version": 1, "appVersion": "$APP_VERSION", "platform": "$PLATFORM", "flavor": "$FLAVOR_LOWER", "assets": $ASSET_JSON_LIST }
|
|
654
|
-
EOF
|
|
655
|
-
|
|
656
|
-
rm -rf "$TMP_DIR"
|
|
657
|
-
|
|
658
|
-
local BUNDLE_HASH=$(sha256 "$TARGET_BUNDLE")
|
|
659
|
-
echo " ✓ Base bundle v1 successfully created: $(basename "$TARGET_BUNDLE")"
|
|
660
|
-
echo " SHA-256: $BUNDLE_HASH"
|
|
661
|
-
|
|
662
|
-
# Update registry for v1
|
|
663
|
-
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "1" "[]" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
|
|
664
|
-
|
|
665
|
-
# Push if requested
|
|
666
|
-
if [ "$PUSH_REMOTE" = true ]; then
|
|
667
|
-
echo " Pushing seeded base bundle to remote..."
|
|
668
|
-
bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "1" "$TARGET_BUNDLE" "$PLATFORM"
|
|
669
|
-
fi
|
|
670
|
-
|
|
671
|
-
echo " ✓ Seeding complete for $PLATFORM!"
|
|
458
|
+
echo " ✓ $PLATFORM v$NEXT published in $((SECONDS - PLATFORM_START))s"
|
|
672
459
|
}
|
|
673
460
|
|
|
674
461
|
# ── Main ───────────────────────────────────────────────────────────────────
|
|
@@ -678,26 +465,6 @@ FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
|
678
465
|
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
679
466
|
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
680
467
|
|
|
681
|
-
# ── Seed Action ────────────────────────────────────────────────────────────
|
|
682
|
-
if [ "$ACTION" = "seed" ]; then
|
|
683
|
-
if [ -n "$SEED_APK" ]; then
|
|
684
|
-
seed_platform "android" "$FLAVOR_LOWER" "$SEED_APK"
|
|
685
|
-
elif [ -n "$SEED_IPA" ]; then
|
|
686
|
-
seed_platform "ios" "$FLAVOR_LOWER" "$SEED_IPA"
|
|
687
|
-
elif [ -n "$SEED_BUNDLE" ]; then
|
|
688
|
-
PLATFORM="${PLATFORM_ARG:-android}"
|
|
689
|
-
seed_platform "$PLATFORM" "$FLAVOR_LOWER" "$SEED_BUNDLE"
|
|
690
|
-
else
|
|
691
|
-
echo "❌ Please provide a binary file to seed:"
|
|
692
|
-
echo " ./build_ota.sh seed --apk <path-to-apk>"
|
|
693
|
-
echo " ./build_ota.sh seed --ipa <path-to-ipa>"
|
|
694
|
-
exit 1
|
|
695
|
-
fi
|
|
696
|
-
echo ""
|
|
697
|
-
echo "=== Base Seeding Complete ==="
|
|
698
|
-
exit 0
|
|
699
|
-
fi
|
|
700
|
-
|
|
701
468
|
# Read current version once to coordinate across platforms
|
|
702
469
|
CURRENT_TOTAL=0
|
|
703
470
|
if [ -f "$REGISTRY_SHARED" ]; then
|
|
@@ -709,78 +476,8 @@ if [ -f "$REGISTRY_SHARED" ]; then
|
|
|
709
476
|
")
|
|
710
477
|
fi
|
|
711
478
|
|
|
712
|
-
#
|
|
713
|
-
|
|
714
|
-
if [ "$ACTION" = "ota" ] && [ $CURRENT_TOTAL -eq 0 ]; then
|
|
715
|
-
echo "Checking for pre-existing Base Bundle v1..."
|
|
716
|
-
|
|
717
|
-
CHECK_PLATFORMS=()
|
|
718
|
-
if [ -n "$PLATFORM_ARG" ]; then
|
|
719
|
-
CHECK_PLATFORMS=("$PLATFORM_ARG")
|
|
720
|
-
else
|
|
721
|
-
CHECK_PLATFORMS=("android" "ios")
|
|
722
|
-
fi
|
|
723
|
-
|
|
724
|
-
ALL_FOUND=true
|
|
725
|
-
MISSING_PLATFORM=""
|
|
726
|
-
for P in "${CHECK_PLATFORMS[@]}"; do
|
|
727
|
-
P_OUTPUT="$ROOT/$P/output"
|
|
728
|
-
P_BASE_BUNDLE="$P_OUTPUT/bundle_${APP_VERSION}_v1_${P}_${FLAVOR_LOWER}.hbc"
|
|
729
|
-
|
|
730
|
-
# Check if local exists
|
|
731
|
-
if [ ! -f "$P_BASE_BUNDLE" ]; then
|
|
732
|
-
# Try downloading from remote if available
|
|
733
|
-
REMOTE_BASE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$(basename "$P_BASE_BUNDLE")"
|
|
734
|
-
download_remote "$REMOTE_BASE_PATH" "$P_BASE_BUNDLE" || true
|
|
735
|
-
fi
|
|
736
|
-
|
|
737
|
-
if [ -f "$P_BASE_BUNDLE" ]; then
|
|
738
|
-
echo " ✓ Found Base Bundle v1 for $P: $(basename "$P_BASE_BUNDLE")"
|
|
739
|
-
# Auto-adopt base bundle in registry
|
|
740
|
-
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "1" "[]" "$P" "" "$FLAVOR_LOWER"
|
|
741
|
-
else
|
|
742
|
-
ALL_FOUND=false
|
|
743
|
-
MISSING_PLATFORM="$P"
|
|
744
|
-
fi
|
|
745
|
-
done
|
|
746
|
-
|
|
747
|
-
if [ "$ALL_FOUND" = true ]; then
|
|
748
|
-
echo " ✓ Base bundle v1 verified and registered. Proceeding with incremental OTA update..."
|
|
749
|
-
CURRENT_TOTAL=1
|
|
750
|
-
else
|
|
751
|
-
if [ "$FORCE_BASE" = true ]; then
|
|
752
|
-
echo " ⚠ Warning: --force flag detected. Creating base bundle v1 from current working tree..."
|
|
753
|
-
ACTION="base"
|
|
754
|
-
else
|
|
755
|
-
echo ""
|
|
756
|
-
echo "╔══════════════════════════════════════════════════════════════════════════════════════════╗"
|
|
757
|
-
echo "║ ❌ CANNOT BUILD OTA: MISSING BASE BUNDLE (v1) ║"
|
|
758
|
-
echo "╠══════════════════════════════════════════════════════════════════════════════════════════╣"
|
|
759
|
-
echo "║ App Version: $APP_VERSION | Flavor: $FLAVOR_LOWER | Missing: $MISSING_PLATFORM"
|
|
760
|
-
echo "║"
|
|
761
|
-
echo "║ No base bundle (v1) was found for this app version (neither locally nor on remote). ║"
|
|
762
|
-
echo "║ If an OTA update is built now, it would generate a base bundle from your current ║"
|
|
763
|
-
echo "║ working tree, which will MISMATCH the version installed on user devices and force a ║"
|
|
764
|
-
echo "║ 100% full-bundle download (wasting bandwidth and bypassing patch benefits). ║"
|
|
765
|
-
echo "║"
|
|
766
|
-
echo "║ How to resolve: ║"
|
|
767
|
-
echo "║ 1. If you already have the release APK/IPA submitted to Google Play / App Store: ║"
|
|
768
|
-
echo "║ Seed the exact base bundle from your release binary: ║"
|
|
769
|
-
echo "║ ./build_ota.sh seed --apk <path-to-apk> ║"
|
|
770
|
-
echo "║ ./build_ota.sh seed --ipa <path-to-ipa> ║"
|
|
771
|
-
echo "║"
|
|
772
|
-
echo "║ 2. If you are preparing a brand new Store release right now from this commit: ║"
|
|
773
|
-
echo "║ ./build_ota.sh base ║"
|
|
774
|
-
echo "║"
|
|
775
|
-
echo "║ 3. If you really want to force create base v1 from current code: ║"
|
|
776
|
-
echo "║ ./build_ota.sh --force ║"
|
|
777
|
-
echo "╚══════════════════════════════════════════════════════════════════════════════════════════╝"
|
|
778
|
-
echo ""
|
|
779
|
-
exit 1
|
|
780
|
-
fi
|
|
781
|
-
fi
|
|
782
|
-
fi
|
|
783
|
-
|
|
479
|
+
# v1 is the store build: the first OTA build is v2
|
|
480
|
+
if [ "$CURRENT_TOTAL" -lt 1 ]; then CURRENT_TOTAL=1; fi
|
|
784
481
|
TARGET_OTA=$((CURRENT_TOTAL + 1))
|
|
785
482
|
|
|
786
483
|
if [ -n "$PLATFORM_ARG" ]; then
|