react-native-bspatch 3.1.0 → 3.1.2
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/package.json +1 -1
- package/tools/build_ota.sh +82 -57
- package/tools/bump_version.js +1 -13
- package/tools/update_registry.js +4 -2
- package/tools/upload_ota.sh +32 -17
package/package.json
CHANGED
package/tools/build_ota.sh
CHANGED
|
@@ -65,6 +65,23 @@ FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
|
65
65
|
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
66
66
|
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
67
67
|
|
|
68
|
+
# ── Output (exported: upload_ota.sh prints in the same layout) ────────────────
|
|
69
|
+
if [ -t 1 ]; then
|
|
70
|
+
BOLD=$'\033[1m'; DIM=$'\033[2m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; RESET=$'\033[0m'
|
|
71
|
+
else
|
|
72
|
+
BOLD=""; DIM=""; GREEN=""; YELLOW=""; RESET=""
|
|
73
|
+
fi
|
|
74
|
+
# Pad to N characters, not bytes ("→" is 3 bytes, printf %-Ns counts bytes)
|
|
75
|
+
padr() { local LC_ALL=en_US.UTF-8; printf "%s%*s" "$1" $(( $2 > ${#1} ? $2 - ${#1} : 0 )) ""; }
|
|
76
|
+
info() { printf " ${DIM}%s${RESET} %s\n" "$(padr "$1" 9)" "$2"; } # " storage R2 bucket"
|
|
77
|
+
sub() { printf " %s %s\n" "$(padr "$1" 18)" "$2"; } # detail under a step
|
|
78
|
+
export BOLD DIM GREEN YELLOW RESET
|
|
79
|
+
export -f padr info sub
|
|
80
|
+
if [ "$ACTION" != "setup" ]; then
|
|
81
|
+
if [ "$ACTION" = "ota" ]; then TITLE="patch ${PLATFORM_ARG:-android + ios}"; else TITLE="$ACTION"; fi
|
|
82
|
+
printf "\n${BOLD}react-native-bspatch${RESET} ${DIM}%s · %s${RESET}\n" "$TITLE" "$FLAVOR_LOWER"
|
|
83
|
+
fi
|
|
84
|
+
|
|
68
85
|
# ── Environment ──────────────────────────────────────────────────────────────
|
|
69
86
|
# .env.<flavor>: OTA_BASE_URL / OTA_FOLDER, the same values react-native-config bakes into the app.
|
|
70
87
|
# .env.ota: upload secrets (OTA_R2_*). Never put them in .env.<flavor>: react-native-config
|
|
@@ -72,7 +89,7 @@ if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
|
72
89
|
# Parsed like react-native-config (KEY=VALUE, optional quotes), never executed:
|
|
73
90
|
# `APP_NAME=Baseform Dev` is valid dotenv but breaks `source`.
|
|
74
91
|
load_env() {
|
|
75
|
-
|
|
92
|
+
ENV_LOADED="${ENV_LOADED:+$ENV_LOADED + }$(basename "$1")"
|
|
76
93
|
local line k v
|
|
77
94
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
78
95
|
[[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=(.*)$ ]] || continue
|
|
@@ -93,6 +110,7 @@ if [ "$ACTION" != "setup" ]; then
|
|
|
93
110
|
fi
|
|
94
111
|
load_env "$ROOT/.env.$FLAVOR_LOWER"
|
|
95
112
|
if [ -f "$ROOT/.env.ota" ]; then load_env "$ROOT/.env.ota"; fi
|
|
113
|
+
info env "$ENV_LOADED"
|
|
96
114
|
fi
|
|
97
115
|
|
|
98
116
|
# ── Setup (project wiring only, no build env needed) ─────────────────────────
|
|
@@ -104,7 +122,6 @@ fi
|
|
|
104
122
|
|
|
105
123
|
# ── Build environment ──────────────────────────────────────────────────────
|
|
106
124
|
if [ "$ACTION" = "ota" ]; then
|
|
107
|
-
echo "Preparing build environment..."
|
|
108
125
|
# Kill any existing Metro processes to prevent file locks
|
|
109
126
|
pkill -f "react-native/local-cli/cli.js" || true
|
|
110
127
|
pkill -f "metro-config/src/index.js" || true
|
|
@@ -112,8 +129,10 @@ if [ "$ACTION" = "ota" ]; then
|
|
|
112
129
|
# Manually clear metro cache if it exists (robust vs ENOTEMPTY error)
|
|
113
130
|
METRO_CACHE_DIR=$(node -e "console.log(require('os').tmpdir())")/metro-cache
|
|
114
131
|
if [ -d "$METRO_CACHE_DIR" ]; then
|
|
115
|
-
echo " Cleaning Metro cache..."
|
|
116
132
|
rm -rf "$METRO_CACHE_DIR"
|
|
133
|
+
info metro "stopped · cache cleared"
|
|
134
|
+
else
|
|
135
|
+
info metro "stopped"
|
|
117
136
|
fi
|
|
118
137
|
fi
|
|
119
138
|
|
|
@@ -125,12 +144,10 @@ REGISTRY_SHARED="$ROOT/ota_registry.json"
|
|
|
125
144
|
# ── Registry Sync ──────────────────────────────────────────────────────────
|
|
126
145
|
# Reset syncs too: it edits the latest remote registry instead of overwriting it with a stale local copy
|
|
127
146
|
if [ "$ACTION" != "setup" ]; then
|
|
128
|
-
echo "Syncing remote registry..."
|
|
129
|
-
|
|
130
147
|
BASE_URL="${OTA_BASE_URL:-}"
|
|
131
148
|
BASE_FOLDER="${OTA_FOLDER:-ota}"
|
|
132
149
|
|
|
133
|
-
|
|
150
|
+
info storage "${OTA_R2_BUCKET:+R2 $OTA_R2_BUCKET → }$BASE_URL/$BASE_FOLDER"
|
|
134
151
|
|
|
135
152
|
if [[ -n "${OTA_R2_BUCKET:-}" ]]; then
|
|
136
153
|
R2_URL="https://${OTA_R2_ACCOUNT_ID:?set OTA_R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${OTA_R2_BUCKET}"
|
|
@@ -162,13 +179,13 @@ if [ "$ACTION" != "setup" ]; then
|
|
|
162
179
|
|
|
163
180
|
if [[ "$API_HTTP" == "200" ]] && node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$REGISTRY_SHARED.tmp" 2>/dev/null; then
|
|
164
181
|
mv "$REGISTRY_SHARED.tmp" "$REGISTRY_SHARED"
|
|
165
|
-
|
|
182
|
+
info registry "synced"
|
|
166
183
|
elif [[ "$API_HTTP" == "404" ]]; then
|
|
167
184
|
rm -f "$REGISTRY_SHARED.tmp"
|
|
168
185
|
# Remote is the source of truth: a local registry from another storage would publish
|
|
169
186
|
# entries for files that don't exist here
|
|
170
187
|
echo '{"apps":{}}' > "$REGISTRY_SHARED"
|
|
171
|
-
|
|
188
|
+
info registry "new (nothing on storage yet)"
|
|
172
189
|
else
|
|
173
190
|
# Continuing would later push a stale local registry over the remote one
|
|
174
191
|
rm -f "$REGISTRY_SHARED.tmp"
|
|
@@ -209,8 +226,19 @@ human_size() {
|
|
|
209
226
|
file_size() { wc -c < "$1" | tr -d ' '; }
|
|
210
227
|
STEP=0
|
|
211
228
|
STEP_TOTAL=5
|
|
212
|
-
|
|
213
|
-
|
|
229
|
+
# One line per step: " [3/5] Assets 59 files · 1s". In a terminal the label shows
|
|
230
|
+
# while the step runs and step_progress updates it in place.
|
|
231
|
+
step_line() { printf " [%d/%d] %-16s %s" "$STEP" "$STEP_TOTAL" "$STEP_LABEL" "$1"; }
|
|
232
|
+
step() {
|
|
233
|
+
STEP=$((STEP + 1)); STEP_START=$SECONDS; STEP_LABEL="$1"
|
|
234
|
+
if [ -t 1 ]; then step_line "${DIM}…${RESET}"; fi
|
|
235
|
+
}
|
|
236
|
+
step_progress() { if [ -t 1 ]; then printf "\r\033[K"; step_line "${DIM}$1${RESET}"; fi; }
|
|
237
|
+
step_done() {
|
|
238
|
+
if [ -t 1 ]; then printf "\r\033[K"; fi
|
|
239
|
+
step_line "$1 ${DIM}· $((SECONDS - STEP_START))s${RESET}"; printf "\n"
|
|
240
|
+
}
|
|
241
|
+
step_fail() { if [ -t 1 ]; then printf "\n"; fi; } # leave the step line before error output
|
|
214
242
|
export -f human_size file_size
|
|
215
243
|
|
|
216
244
|
# ── Release version (native app version the OTA targets) ─────────────────────
|
|
@@ -239,10 +267,9 @@ fi
|
|
|
239
267
|
|
|
240
268
|
# Confirm in a terminal (Enter keeps the detected version); CI / -y / --release-version skip it
|
|
241
269
|
if [ -t 0 ] && [ "$CONFIRM_VERSION" = true ] && [ -z "$APP_VERSION_ARG" ]; then
|
|
242
|
-
echo ""
|
|
243
270
|
if [ "$ACTION" = "reset" ]; then PROMPT_WHAT="Release version to RESET ($FLAVOR_LOWER, android + ios)"
|
|
244
271
|
else PROMPT_WHAT="Release version for ${PLATFORM_ARG:-android + ios} ($FLAVOR_LOWER)"; fi
|
|
245
|
-
read -r -p " $PROMPT_WHAT [${APP_VERSION:-none detected}]: " INPUT_VERSION
|
|
272
|
+
read -r -p " ${BOLD}?${RESET} $PROMPT_WHAT [${APP_VERSION:-none detected}]: " INPUT_VERSION
|
|
246
273
|
INPUT_VERSION="${INPUT_VERSION// /}"
|
|
247
274
|
APP_VERSION="${INPUT_VERSION:-$APP_VERSION}"
|
|
248
275
|
fi
|
|
@@ -251,15 +278,14 @@ if [ -z "$APP_VERSION" ]; then
|
|
|
251
278
|
echo "❌ Could not read the native app version. Pass --release-version <x.y.z>."
|
|
252
279
|
exit 1
|
|
253
280
|
fi
|
|
254
|
-
|
|
281
|
+
info release "$APP_VERSION"
|
|
255
282
|
|
|
256
283
|
# ── Reset ─────────────────────────────────────────────────────────────────────
|
|
257
284
|
if [ "$ACTION" = "reset" ]; then
|
|
258
|
-
echo "Resetting OTA registry and output for flavor '$FLAVOR_LOWER'..."
|
|
259
|
-
|
|
260
285
|
# Reset only the specific flavor in registry JSON if registry exists
|
|
286
|
+
LATEST=1
|
|
261
287
|
if [ -f "$REGISTRY_SHARED" ]; then
|
|
262
|
-
node -e "
|
|
288
|
+
LATEST=$(node -e "
|
|
263
289
|
try {
|
|
264
290
|
const fs = require('fs');
|
|
265
291
|
const reg = JSON.parse(fs.readFileSync('$REGISTRY_SHARED', 'utf8'));
|
|
@@ -270,8 +296,9 @@ if [ "$ACTION" = "reset" ]; then
|
|
|
270
296
|
flavors['$FLAVOR_LOWER'] = { latestOtaVersion: flavors['$FLAVOR_LOWER'].latestOtaVersion || 1, patches: [] };
|
|
271
297
|
}
|
|
272
298
|
fs.writeFileSync('$REGISTRY_SHARED', JSON.stringify(reg, null, 4));
|
|
273
|
-
|
|
274
|
-
|
|
299
|
+
console.log(flavors?.['$FLAVOR_LOWER']?.latestOtaVersion || 1);
|
|
300
|
+
} catch(e) { console.log(1); }
|
|
301
|
+
")
|
|
275
302
|
else
|
|
276
303
|
echo '{"apps":{}}' > "$REGISTRY_SHARED"
|
|
277
304
|
fi
|
|
@@ -282,20 +309,20 @@ if [ "$ACTION" = "reset" ]; then
|
|
|
282
309
|
find "$D" -maxdepth 1 -type f -name "asset-manifest-v*_${FLAVOR_LOWER}.json" -delete 2>/dev/null || true # legacy names
|
|
283
310
|
done
|
|
284
311
|
|
|
285
|
-
|
|
312
|
+
info local "cleared $APP_VERSION/$FLAVOR_LOWER · next OTA is v$((LATEST + 1))"
|
|
286
313
|
|
|
287
314
|
if [ "$PUSH_REMOTE" = true ]; then
|
|
288
|
-
echo " Syncing reset state for flavor '$FLAVOR_LOWER' to remote storage..."
|
|
289
315
|
bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "0" "" "ios" "--clean-remote"
|
|
316
|
+
else
|
|
317
|
+
info remote "unchanged ${DIM}(add --push to delete on storage)${RESET}"
|
|
290
318
|
fi
|
|
291
319
|
exit 0
|
|
292
320
|
fi
|
|
293
321
|
|
|
294
322
|
# ── Ensure bsdiff compiled ───────────────────────────────────────────────────
|
|
295
323
|
if [ ! -f "$BSDIFF" ]; then
|
|
296
|
-
echo "Compiling bsdiff..."
|
|
297
324
|
clang -O2 -D BSDIFF_EXECUTABLE -o "$BSDIFF" "$SCRIPT_DIR/bsdiff.c" -lbz2
|
|
298
|
-
|
|
325
|
+
info bsdiff "compiled"
|
|
299
326
|
fi
|
|
300
327
|
|
|
301
328
|
find_hermesc() {
|
|
@@ -328,8 +355,7 @@ build_platform() {
|
|
|
328
355
|
if [ "$PLATFORM" == "ios" ]; then OUTPUT="$ROOT/ios/output"; fi
|
|
329
356
|
local REGISTRY="$REGISTRY_SHARED"
|
|
330
357
|
|
|
331
|
-
|
|
332
|
-
echo ">>> Building $PLATFORM | Version: $APP_VERSION | OTA: v$CURRENT → v$NEXT"
|
|
358
|
+
printf "\n${BOLD}%s${RESET} · %s · ${BOLD}v%s${RESET} ${DIM}(previous v%s)${RESET}\n" "$PLATFORM" "$APP_VERSION" "$NEXT" "$CURRENT"
|
|
333
359
|
STEP=0
|
|
334
360
|
local PLATFORM_START=$SECONDS
|
|
335
361
|
|
|
@@ -341,17 +367,17 @@ build_platform() {
|
|
|
341
367
|
# Determine source bundle
|
|
342
368
|
local BUILD_LOG="$WORKSPACE/bundle.log"
|
|
343
369
|
if [ "$PLATFORM" == "android" ]; then
|
|
344
|
-
step "
|
|
370
|
+
step "Bundle JS"
|
|
345
371
|
# Gradle's config noise goes to a log; shown only on failure
|
|
346
372
|
if ! (cd "$ROOT/android" && ./gradlew ":app:createBundle${FLAVOR_CAP}ReleaseJsAndAssets" -Pkotlin.incremental=false --quiet) > "$BUILD_LOG" 2>&1; then
|
|
347
|
-
tail -n 40 "$BUILD_LOG"; echo " ❌ Gradle bundle failed (full log: $BUILD_LOG)"; exit 1
|
|
373
|
+
step_fail; tail -n 40 "$BUILD_LOG"; echo " ❌ Gradle bundle failed (full log: $BUILD_LOG)"; exit 1
|
|
348
374
|
fi
|
|
349
375
|
BUNDLE_SOURCE="$ROOT/android/app/build/generated/assets/react/${FLAVOR_LOWER}Release/index.android.bundle"
|
|
350
376
|
else
|
|
351
|
-
step "
|
|
377
|
+
step "Bundle JS"
|
|
352
378
|
if ! npx react-native bundle --platform ios --dev false --entry-file index.js \
|
|
353
379
|
--bundle-output "$WORKSPACE/source.bundle" --assets-dest "$OUTPUT/assets" > "$BUILD_LOG" 2>&1; then
|
|
354
|
-
tail -n 40 "$BUILD_LOG"; echo " ❌ Bundle failed (full log: $BUILD_LOG)"; exit 1
|
|
380
|
+
step_fail; tail -n 40 "$BUILD_LOG"; echo " ❌ Bundle failed (full log: $BUILD_LOG)"; exit 1
|
|
355
381
|
fi
|
|
356
382
|
BUNDLE_SOURCE="$WORKSPACE/source.bundle"
|
|
357
383
|
fi
|
|
@@ -366,12 +392,12 @@ build_platform() {
|
|
|
366
392
|
if [ "$FIRST_BYTE" == "c6" ]; then IS_HBC=true; fi
|
|
367
393
|
fi
|
|
368
394
|
|
|
369
|
-
step "Hermes
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
395
|
+
step "Hermes"
|
|
396
|
+
local HERMES_NOTE="already bytecode"
|
|
397
|
+
if [ "$IS_HBC" != true ]; then
|
|
398
|
+
HERMES_NOTE="compiled"
|
|
373
399
|
if [ -z "$HERMESC_PATH" ] || [ ! -f "$HERMESC_PATH" ]; then
|
|
374
|
-
echo " ❌ ERROR: hermesc not found"; exit 1
|
|
400
|
+
step_fail; echo " ❌ ERROR: hermesc not found"; exit 1
|
|
375
401
|
fi
|
|
376
402
|
# -O matches what Xcode/Gradle use for release
|
|
377
403
|
"$HERMESC_PATH" -emit-binary -O -out "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
|
|
@@ -382,7 +408,7 @@ build_platform() {
|
|
|
382
408
|
local NEW_BUNDLE_PATH="$OUTPUT/$BUNDLE_NAME"
|
|
383
409
|
|
|
384
410
|
cp "$BUNDLE_SOURCE" "$NEW_BUNDLE_PATH"
|
|
385
|
-
step_done "$
|
|
411
|
+
step_done "$HERMES_NOTE"
|
|
386
412
|
|
|
387
413
|
# ── Assets ──
|
|
388
414
|
local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
@@ -394,7 +420,7 @@ build_platform() {
|
|
|
394
420
|
local ASSET_JSON_LIST="["
|
|
395
421
|
local FIRST=true
|
|
396
422
|
local ASSET_COUNT=0
|
|
397
|
-
step "
|
|
423
|
+
step "Assets"
|
|
398
424
|
if [ -d "$ASSETS_SRC_DIR" ]; then
|
|
399
425
|
while IFS= read -r -d '' ASSET_FILE; do
|
|
400
426
|
local HASH=$(sha256 "$ASSET_FILE")
|
|
@@ -409,12 +435,11 @@ build_platform() {
|
|
|
409
435
|
if [ "$FIRST" = true ]; then FIRST=false; else ASSET_JSON_LIST+=", "; fi
|
|
410
436
|
ASSET_JSON_LIST+="{\"hash\": \"$HASH\", \"ext\": \"$EX\", \"originalPath\": \"$REL_PATH\"}"
|
|
411
437
|
ASSET_COUNT=$((ASSET_COUNT + 1))
|
|
412
|
-
|
|
438
|
+
step_progress "$ASSET_COUNT files"
|
|
413
439
|
done < <(find "$ASSETS_SRC_DIR" -type f -print0 2>/dev/null)
|
|
414
|
-
if [ -t 1 ]; then printf "\r\033[K"; fi
|
|
415
440
|
fi
|
|
416
441
|
ASSET_JSON_LIST+="]"
|
|
417
|
-
step_done "$ASSET_COUNT
|
|
442
|
+
step_done "$ASSET_COUNT files"
|
|
418
443
|
|
|
419
444
|
cat > "$ASSET_MANIFEST_FILE" << EOF
|
|
420
445
|
{ "version": $NEXT, "appVersion": "$APP_VERSION", "platform": "$PLATFORM", "flavor": "$FLAVOR_LOWER", "assets": $ASSET_JSON_LIST }
|
|
@@ -426,48 +451,49 @@ EOF
|
|
|
426
451
|
local NEW_HASH=$(sha256 "$NEW_BUNDLE_PATH")
|
|
427
452
|
local PATCHES_JSON="["
|
|
428
453
|
local FIRST_FROM=$((NEXT - 5)); if [ $FIRST_FROM -lt 2 ]; then FIRST_FROM=2; fi
|
|
429
|
-
local PATCH_TOTAL=$((NEXT - FIRST_FROM))
|
|
430
|
-
|
|
431
|
-
step "Creating $PATCH_TOTAL patch(es) from v$FIRST_FROM..v$((NEXT - 1))..."
|
|
432
|
-
else
|
|
433
|
-
step "Patches: none (first OTA of this release — clients take the full bundle)"
|
|
434
|
-
fi
|
|
454
|
+
local PATCH_TOTAL=$((NEXT - FIRST_FROM)) PATCH_MADE=0 PATCH_LINES=() PATCH_MISSING=""
|
|
455
|
+
step "Patches"
|
|
435
456
|
for (( i=FIRST_FROM; i<NEXT; i++ )); do
|
|
436
|
-
PATCH_N=$((PATCH_N + 1))
|
|
437
457
|
local OLD_B="bundle_${APP_VERSION}_v${i}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
438
458
|
local OLD_P="$OUTPUT/$OLD_B"
|
|
439
459
|
if [ ! -f "$OLD_P" ]; then
|
|
440
|
-
|
|
460
|
+
step_progress "v$i: fetching bundle…"
|
|
441
461
|
local REMOTE_BUNDLE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$OLD_B"
|
|
442
462
|
download_remote "$REMOTE_BUNDLE_PATH" "$OLD_P" || true
|
|
443
463
|
fi
|
|
444
464
|
if [ ! -f "$OLD_P" ]; then
|
|
445
|
-
|
|
465
|
+
PATCH_MISSING="${PATCH_MISSING:+$PATCH_MISSING, }v$i"
|
|
446
466
|
continue
|
|
447
467
|
fi
|
|
448
468
|
local PNAME="patch_${APP_VERSION}_v${i}_to_v${NEXT}_${FLAVOR_LOWER}_${PLATFORM}.patch"
|
|
449
469
|
local PATCH_START=$SECONDS
|
|
450
|
-
|
|
470
|
+
step_progress "v$i → v$NEXT…"
|
|
451
471
|
"$BSDIFF" "$OLD_P" "$NEW_BUNDLE_PATH" "$OUTPUT/$PNAME"
|
|
452
|
-
|
|
453
|
-
echo " ($PATCH_N/$PATCH_TOTAL) v$i → v$NEXT: $(human_size "$(file_size "$OUTPUT/$PNAME")") ($((SECONDS - PATCH_START))s)"
|
|
472
|
+
PATCH_LINES+=("v$i → v$NEXT|$(human_size "$(file_size "$OUTPUT/$PNAME")") ${DIM}· $((SECONDS - PATCH_START))s${RESET}")
|
|
454
473
|
PATCH_MADE=$((PATCH_MADE + 1))
|
|
455
474
|
if [ "$PATCHES_JSON" != "[" ]; then PATCHES_JSON+=", "; fi
|
|
456
475
|
PATCHES_JSON+="{\"from\": $i, \"to\": $NEXT, \"file\": \"$PNAME\", \"baseHash\": \"$(sha256 "$OLD_P")\", \"bundleHash\": \"$NEW_HASH\"}"
|
|
457
476
|
done
|
|
458
477
|
PATCHES_JSON+="]"
|
|
459
|
-
if [ $PATCH_TOTAL -
|
|
478
|
+
if [ $PATCH_TOTAL -eq 0 ]; then
|
|
479
|
+
step_done "none ${DIM}(first OTA of this release: full bundle only)${RESET}"
|
|
480
|
+
else
|
|
481
|
+
step_done "$PATCH_MADE created"
|
|
482
|
+
fi
|
|
483
|
+
local L
|
|
484
|
+
for L in "${PATCH_LINES[@]}"; do sub "${L%%|*}" "${L#*|}"; done
|
|
485
|
+
if [ -n "$PATCH_MISSING" ]; then sub "$PATCH_MISSING" "${DIM}no bundle on storage → full download${RESET}"; fi
|
|
460
486
|
|
|
461
487
|
# ── Finalize ──
|
|
462
|
-
step "
|
|
488
|
+
step "Publish"
|
|
489
|
+
if [ -t 1 ]; then printf "\r\033[K"; fi
|
|
490
|
+
step_line ""; printf "\n"
|
|
463
491
|
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "$NEXT" "$PATCHES_JSON" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
|
|
464
492
|
bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "$NEXT" "$NEW_BUNDLE_PATH" "$PLATFORM"
|
|
465
|
-
|
|
466
|
-
echo " ✓ $PLATFORM v$NEXT published in $((SECONDS - PLATFORM_START))s"
|
|
493
|
+
printf "${GREEN}✓${RESET} ${BOLD}%s v%s${RESET} published ${DIM}in %ss${RESET}\n" "$PLATFORM" "$NEXT" "$((SECONDS - PLATFORM_START))"
|
|
467
494
|
}
|
|
468
495
|
|
|
469
496
|
# ── Main ───────────────────────────────────────────────────────────────────
|
|
470
|
-
echo "=== OTA Build Pipeline | App: $APP_VERSION | Flavor: $FLAVOR ==="
|
|
471
497
|
|
|
472
498
|
FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
473
499
|
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
@@ -496,8 +522,7 @@ else
|
|
|
496
522
|
build_platform "ios" "$FLAVOR" "$CURRENT_TOTAL" "$TARGET_OTA"
|
|
497
523
|
fi
|
|
498
524
|
|
|
499
|
-
|
|
500
|
-
echo "=== All Builds Complete ==="
|
|
525
|
+
|
|
501
526
|
|
|
502
527
|
|
|
503
528
|
|
package/tools/bump_version.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// - package.json ("version")
|
|
7
7
|
// - android/app/build.gradle (versionName & increments versionCode)
|
|
8
8
|
// - ios/*.xcodeproj/project.pbxproj (MARKETING_VERSION & increments CURRENT_PROJECT_VERSION)
|
|
9
|
-
//
|
|
9
|
+
// JS reads the native version at runtime (getAppVersion); never hardcode it in source.
|
|
10
10
|
//
|
|
11
11
|
// Usage:
|
|
12
12
|
// node bump_version.js <new_version> [--root <path>]
|
|
@@ -118,16 +118,4 @@ if (fs.existsSync(iosDir)) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
// 4. src/ota/otaConfig.ts (if exists)
|
|
122
|
-
const otaConfigPath = path.join(rootDir, 'src/ota/otaConfig.ts');
|
|
123
|
-
if (fs.existsSync(otaConfigPath)) {
|
|
124
|
-
try {
|
|
125
|
-
let content = fs.readFileSync(otaConfigPath, 'utf8');
|
|
126
|
-
content = content.replace(/export const APP_VERSION = ['"][^'"]+['"];/, `export const APP_VERSION = '${newVersion}';`);
|
|
127
|
-
fs.writeFileSync(otaConfigPath, content, 'utf8');
|
|
128
|
-
console.log(` ✓ src/ota/otaConfig.ts: APP_VERSION → '${newVersion}'`);
|
|
129
|
-
changedCount++;
|
|
130
|
-
} catch (e) {}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
121
|
console.log(`\n✓ Done! Updated ${changedCount} file(s) to version ${newVersion}.\n`);
|
package/tools/update_registry.js
CHANGED
|
@@ -95,7 +95,6 @@ if (!signingKey) {
|
|
|
95
95
|
}
|
|
96
96
|
const signer = crypto.createSign('SHA256');
|
|
97
97
|
signer.update(fs.readFileSync(filePath));
|
|
98
|
-
console.log(` ✓ Signed ${file}`);
|
|
99
98
|
return signer.sign(signingKey, 'base64');
|
|
100
99
|
};
|
|
101
100
|
|
|
@@ -138,6 +137,9 @@ if (!signingKey) {
|
|
|
138
137
|
// ── Write updated registry ───────────────────────────────────────────────────
|
|
139
138
|
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
140
139
|
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
140
|
+
// Same layout as build_ota.sh step details
|
|
141
|
+
const patchCount = newPatches.length;
|
|
141
142
|
console.log(
|
|
142
|
-
`
|
|
143
|
+
` ${'entries'.padEnd(18)} full bundle${patchCount ? ` + ${patchCount} patch${patchCount > 1 ? 'es' : ''}` : ''}` +
|
|
144
|
+
(signingKey ? ' · signed' : ' · unsigned (no ota_private_key.pem)'),
|
|
143
145
|
);
|
package/tools/upload_ota.sh
CHANGED
|
@@ -53,39 +53,49 @@ if [[ -n "${OTA_R2_BUCKET:-}" ]]; then
|
|
|
53
53
|
: "${OTA_R2_ACCOUNT_ID:?set OTA_R2_ACCOUNT_ID}" "${OTA_R2_ACCESS_KEY_ID:?set OTA_R2_ACCESS_KEY_ID}" "${OTA_R2_SECRET_ACCESS_KEY:?set OTA_R2_SECRET_ACCESS_KEY}"
|
|
54
54
|
R2_URL="https://${OTA_R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${OTA_R2_BUCKET}"
|
|
55
55
|
r2() { curl -sS --aws-sigv4 "aws:amz:auto:s3" --user "$OTA_R2_ACCESS_KEY_ID:$OTA_R2_SECRET_ACCESS_KEY" "$@"; }
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
# "patch_1.0.4_v4_to_v7_…" → "patch v4 → v7", "bundle_…_v7_…" → "bundle v7"
|
|
57
|
+
pretty() {
|
|
58
|
+
case "$1" in
|
|
59
|
+
patch_*) echo "$1" | sed -E 's/.*_v([0-9]+)_to_v([0-9]+)_.*/patch v\1 → v\2/' ;;
|
|
60
|
+
bundle_*) echo "$1" | sed -E 's/.*_v([0-9]+)_.*/bundle v\1/' ;;
|
|
61
|
+
asset-manifest_*) echo "asset manifest" ;;
|
|
62
|
+
*) echo "$1" ;;
|
|
63
|
+
esac
|
|
64
|
+
}
|
|
65
|
+
r2_put() { # <file> <key> <cache-control> <label>
|
|
66
|
+
local SIZE PROGRESS=(-s) BAR=false
|
|
58
67
|
SIZE=$(wc -c < "$1" | tr -d ' ')
|
|
59
|
-
# Progress bar for big files (bundles) in a terminal
|
|
60
|
-
if [ -t
|
|
61
|
-
curl -S "${PROGRESS[@]}" -f --aws-sigv4 "aws:amz:auto:s3" --user "$OTA_R2_ACCESS_KEY_ID:$OTA_R2_SECRET_ACCESS_KEY" \
|
|
68
|
+
# Progress bar for big files (bundles) in a terminal; erased once done
|
|
69
|
+
if [ -t 1 ] && [ "$SIZE" -gt 1048576 ]; then printf " ↑ %s\n" "$4"; PROGRESS=(-#); BAR=true; fi
|
|
70
|
+
COLUMNS=60 curl -S "${PROGRESS[@]}" -f --aws-sigv4 "aws:amz:auto:s3" --user "$OTA_R2_ACCESS_KEY_ID:$OTA_R2_SECRET_ACCESS_KEY" \
|
|
62
71
|
-X PUT -T "$1" -H "Cache-Control: $3" "$R2_URL/$2" -o /dev/null || { echo " ❌ Upload failed: $2"; exit 1; }
|
|
63
|
-
|
|
72
|
+
if [ "$BAR" = true ]; then printf "\033[2A\033[K\n\033[K\033[1A"; fi
|
|
73
|
+
printf " ${GREEN}✓${RESET} %s %s\n" "$(padr "$4" 16)" "$(human_size "$SIZE")"
|
|
64
74
|
}
|
|
65
75
|
RELEASE_PREFIX="$BASE_FOLDER/$APP_VERSION/$CHANNEL"
|
|
66
76
|
IMMUTABLE="public, max-age=31536000, immutable"
|
|
67
77
|
|
|
68
|
-
echo " → Cloudflare R2: $OTA_R2_BUCKET/$RELEASE_PREFIX"
|
|
69
78
|
if [ "$CLEAN_REMOTE" = true ]; then
|
|
70
|
-
echo " Deleting $RELEASE_PREFIX/ ..."
|
|
71
79
|
# ponytail: one ListObjectsV2 page (1000 keys); a release+channel holds far fewer
|
|
72
80
|
LISTING=$(r2 -f "$R2_URL?list-type=2&prefix=$RELEASE_PREFIX/") || { echo " ❌ Listing $RELEASE_PREFIX/ failed"; exit 1; }
|
|
73
|
-
DELETED=0
|
|
81
|
+
DELETED=0; BUNDLES=0; PATCHES=0; MANIFESTS=0
|
|
74
82
|
for KEY in $(echo "$LISTING" | grep -o '<Key>[^<]*</Key>' | sed 's/<[^>]*>//g'); do
|
|
75
83
|
r2 -f -X DELETE "$R2_URL/$KEY" -o /dev/null || { echo " ❌ Delete failed: $KEY"; exit 1; }
|
|
76
|
-
|
|
84
|
+
DELETED=$((DELETED + 1))
|
|
85
|
+
case "$KEY" in *.hbc) BUNDLES=$((BUNDLES + 1)) ;; *.patch) PATCHES=$((PATCHES + 1)) ;; *.json) MANIFESTS=$((MANIFESTS + 1)) ;; esac
|
|
86
|
+
if [ -t 1 ]; then printf "\r\033[K ${DIM}%-9s${RESET} deleting… %d" "remote" "$DELETED"; fi
|
|
77
87
|
done
|
|
78
|
-
|
|
88
|
+
if [ -t 1 ]; then printf "\r\033[K"; fi
|
|
89
|
+
info remote "$DELETED object(s) deleted ${DIM}($BUNDLES bundles · $PATCHES patches · $MANIFESTS manifests)${RESET}"
|
|
79
90
|
else
|
|
80
91
|
# Only this version's files: earlier bundles/patches are already uploaded and never change
|
|
81
92
|
RELEASE_FILES=()
|
|
82
93
|
for FILE in "$OUTPUT_DIR"/*_v"${OTA_VERSION}"_*.{patch,hbc,json}; do
|
|
83
94
|
[[ -f "$FILE" ]] && is_release_file "$(basename "$FILE")" && RELEASE_FILES+=("$FILE")
|
|
84
95
|
done
|
|
85
|
-
N=0
|
|
86
96
|
for FILE in "${RELEASE_FILES[@]}"; do
|
|
87
|
-
|
|
88
|
-
r2_put "$FILE" "$RELEASE_PREFIX/$FNAME" "$IMMUTABLE" "(
|
|
97
|
+
FNAME=$(basename "$FILE")
|
|
98
|
+
r2_put "$FILE" "$RELEASE_PREFIX/$FNAME" "$IMMUTABLE" "$(pretty "$FNAME")"
|
|
89
99
|
done
|
|
90
100
|
# Content-addressed assets: upload only hashes the bucket doesn't have yet
|
|
91
101
|
ASSETS=("$OUTPUT_DIR"/asset-store/*)
|
|
@@ -98,13 +108,18 @@ if [[ -n "${OTA_R2_BUCKET:-}" ]]; then
|
|
|
98
108
|
r2 -f -X PUT -T "$FILE" -H "Cache-Control: $IMMUTABLE" "$R2_URL/$KEY" -o /dev/null || { echo " ❌ Upload failed: $KEY"; exit 1; }
|
|
99
109
|
NEW=$((NEW + 1)); NEW_BYTES=$((NEW_BYTES + $(wc -c < "$FILE")))
|
|
100
110
|
fi
|
|
101
|
-
if [ -t 1 ]; then printf "\r
|
|
111
|
+
if [ -t 1 ]; then printf "\r\033[K ${DIM}assets %d/%d checked${RESET}" "$N" "${#ASSETS[@]}"; fi
|
|
102
112
|
done
|
|
103
113
|
if [ -t 1 ] && [ ${#ASSETS[@]} -gt 0 ]; then printf "\r\033[K"; fi
|
|
104
|
-
|
|
114
|
+
printf " ${GREEN}✓${RESET} %s %s\n" "$(padr assets 16)" "${#ASSETS[@]} · $NEW new ($(human_size "$NEW_BYTES"))"
|
|
105
115
|
fi
|
|
106
116
|
# Registry last: clients never see a version whose files aren't uploaded yet
|
|
107
|
-
|
|
117
|
+
if [ "$CLEAN_REMOTE" = true ]; then
|
|
118
|
+
r2 -f -X PUT -T "$REGISTRY_FILE" -H "Cache-Control: no-cache" "$R2_URL/$BASE_FOLDER/ota_registry.json" -o /dev/null || { echo " ❌ Upload failed: ota_registry.json"; exit 1; }
|
|
119
|
+
info registry "uploaded"
|
|
120
|
+
else
|
|
121
|
+
r2_put "$REGISTRY_FILE" "$BASE_FOLDER/ota_registry.json" "no-cache" "registry"
|
|
122
|
+
fi
|
|
108
123
|
exit 0
|
|
109
124
|
fi
|
|
110
125
|
|