react-native-bspatch 1.1.3 → 1.1.4
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 +25 -20
- package/package.json +1 -1
- package/tools/build_ota.sh +263 -11
- package/tools/setup_project.js +69 -4
package/README.md
CHANGED
|
@@ -363,6 +363,7 @@ Add these to your `package.json` (or use the existing ones in this project):
|
|
|
363
363
|
"react-native-bspatch": "bash ./src/native_modules/react-native-bspatch/tools/build_ota.sh",
|
|
364
364
|
"ota:setup": "yarn react-native-bspatch setup",
|
|
365
365
|
"ota:init": "yarn react-native-bspatch base",
|
|
366
|
+
"ota:seed": "yarn react-native-bspatch seed",
|
|
366
367
|
"ota:build": "yarn react-native-bspatch",
|
|
367
368
|
"ota:clean": "yarn react-native-bspatch reset"
|
|
368
369
|
}
|
|
@@ -372,22 +373,19 @@ Add these to your `package.json` (or use the existing ones in this project):
|
|
|
372
373
|
### Workflow
|
|
373
374
|
|
|
374
375
|
```
|
|
375
|
-
ota:setup ──►
|
|
376
|
-
│
|
|
377
|
-
│
|
|
378
|
-
│
|
|
379
|
-
│
|
|
380
|
-
│
|
|
381
|
-
│
|
|
382
|
-
│ │
|
|
383
|
-
│ └── First build only: creates base bundle from APK/IPA.
|
|
384
|
-
│ Stores base asset manifest so subsequent builds know
|
|
385
|
-
│ which assets are already in the APK and skip them.
|
|
376
|
+
ota:setup ──► (Build Release APK/IPA) ──► ota:build ──► (repeat for each OTA update)
|
|
377
|
+
│ │ │
|
|
378
|
+
│ │ └── Incremental patch from Base Bundle v1
|
|
379
|
+
│ │
|
|
380
|
+
│ └── Automatic Base Capture (Gradle & Xcode):
|
|
381
|
+
│ Each release build automatically saves `bundle_{appVersion}_v1_{platform}.hbc`.
|
|
382
|
+
│ No manual `ota:init` is required!
|
|
386
383
|
│
|
|
387
384
|
└── Patches native integration into project files:
|
|
388
385
|
• iOS: AppDelegate.swift — adds bundleURL() override
|
|
389
|
-
• iOS: project.pbxproj —
|
|
386
|
+
• iOS: project.pbxproj — syncs and auto-captures base bundle on Release archive
|
|
390
387
|
• Android: MainApplication.kt — adds OtaBundleResolver.resolve() call
|
|
388
|
+
• Android: app/build.gradle — auto-captures base bundle upon assembleRelease / bundleRelease
|
|
391
389
|
Idempotent: safe to run multiple times; skips already-patched files.
|
|
392
390
|
```
|
|
393
391
|
|
|
@@ -395,14 +393,22 @@ ota:setup ──► ota:init ──► ota:build ──► (ota:build ◄──
|
|
|
395
393
|
|
|
396
394
|
| Command | Action |
|
|
397
395
|
|---|---|
|
|
398
|
-
| `yarn ota:setup` | Inject OTA integration into AppDelegate.swift, MainApplication.kt, project.pbxproj
|
|
399
|
-
| `yarn ota:
|
|
400
|
-
| `yarn ota:
|
|
396
|
+
| `yarn ota:setup` | Inject OTA integration into AppDelegate.swift, MainApplication.kt, project.pbxproj, and app/build.gradle. |
|
|
397
|
+
| `yarn ota:build` | Generate incremental `.patch` from previous bundle, upload to server, update registry. |
|
|
398
|
+
| `yarn ota:seed --apk <path>` | **Zero Mismatch Recovery:** Seed base bundle directly from an existing release APK/AAB. |
|
|
399
|
+
| `yarn ota:seed --ipa <path>` | **Zero Mismatch Recovery:** Seed base bundle directly from an existing release IPA. |
|
|
400
|
+
| `yarn ota:init` | Manual first release: build base APK/IPA + bundle + base asset manifest. Creates `ota_registry.json`. |
|
|
401
401
|
| `yarn ota:build --platform android` | Build Android only |
|
|
402
402
|
| `yarn ota:build --platform ios` | Build iOS only |
|
|
403
403
|
| `yarn ota:build --flavor staging` | Build with flavor "staging" (default: "dev") |
|
|
404
404
|
| `yarn ota:clean` | Wipe `ota_registry.json` + output files. Preserves base asset manifests so `ota:build` still works after clean. |
|
|
405
405
|
|
|
406
|
+
> [!TIP]
|
|
407
|
+
> **Zero Mismatch & Fail-Safe Protection:**
|
|
408
|
+
> 1. **Auto-Capture:** When you build a Release APK/AAB or Archive on Xcode, the bundle is automatically captured into `output/` as Base Bundle v1.
|
|
409
|
+
> 2. **Seed Binary:** If you forgot to capture the base bundle and already submitted your APK/IPA to the Store, run `yarn ota:seed --apk ./app-release.apk`. It extracts the exact byte-for-byte bundle directly from the release binary, guaranteeing 100% hash match.
|
|
410
|
+
> 3. **Fail-Safe Guard:** `ota:build` will refuse to generate an update if no authentic Base Bundle v1 is found, preventing corrupt patches that trigger full-bundle fallback downloads.
|
|
411
|
+
|
|
406
412
|
### How It Works
|
|
407
413
|
|
|
408
414
|
1. **Bundle** — `npx react-native bundle` generates Hermes bytecode (`.hbc`). iOS base build uses the Xcode-compiled bundle to guarantee byte-exact match with the IPA.
|
|
@@ -417,11 +423,10 @@ ota:setup ──► ota:init ──► ota:build ──► (ota:build ◄──
|
|
|
417
423
|
# 1. Add the npm scripts to package.json (or run directly)
|
|
418
424
|
bash ./src/native_modules/react-native-bspatch/tools/build_ota.sh setup
|
|
419
425
|
|
|
420
|
-
# 2.
|
|
421
|
-
|
|
422
|
-
#
|
|
423
|
-
yarn ota:
|
|
424
|
-
yarn ota:init --platform ios # iOS only
|
|
426
|
+
# 2. Build your app normally for App Store / Google Play
|
|
427
|
+
# Gradle and Xcode will automatically capture the base bundle (v1)!
|
|
428
|
+
# OR seed from an existing release binary:
|
|
429
|
+
yarn ota:seed --apk ./release.apk
|
|
425
430
|
|
|
426
431
|
# 3. For each subsequent release:
|
|
427
432
|
yarn ota:build
|
package/package.json
CHANGED
package/tools/build_ota.sh
CHANGED
|
@@ -45,20 +45,41 @@ PLATFORM_ARG=""
|
|
|
45
45
|
APP_VERSION_ARG=""
|
|
46
46
|
NON_INTERACTIVE=false
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
SEED_APK=""
|
|
49
|
+
SEED_IPA=""
|
|
50
|
+
SEED_BUNDLE=""
|
|
51
|
+
FORCE_BASE=false
|
|
52
|
+
|
|
49
53
|
# Arg parsing
|
|
50
54
|
while [[ "$#" -gt 0 ]]; do
|
|
51
55
|
case $1 in
|
|
52
56
|
--platform) PLATFORM_ARG="$2"; shift ;;
|
|
53
57
|
--flavor) FLAVOR="$2"; shift ;;
|
|
54
58
|
--app-version) APP_VERSION_ARG="$2"; shift ;;
|
|
59
|
+
--apk) SEED_APK="$2"; ACTION="seed"; shift ;;
|
|
60
|
+
--ipa) SEED_IPA="$2"; ACTION="seed"; shift ;;
|
|
61
|
+
--bundle) SEED_BUNDLE="$2"; ACTION="seed"; shift ;;
|
|
62
|
+
--force) FORCE_BASE=true ;;
|
|
55
63
|
-y|--yes|--non-interactive) NON_INTERACTIVE=true ;;
|
|
56
64
|
--push) PUSH_REMOTE=true ;;
|
|
57
65
|
reset|base) ACTION="$1" ;;
|
|
58
66
|
setup) ACTION="$1" ;;
|
|
59
|
-
|
|
67
|
+
seed) ACTION="seed" ;;
|
|
68
|
+
apk|ipa) ACTION="base" ;; # Compatibility for legacy actions
|
|
60
69
|
dev|staging|production) FLAVOR="$1" ;; # Support positional flavor
|
|
61
70
|
[0-9]*) ;; # Ignore version if passed as $1 (pos 1 usually)
|
|
71
|
+
*)
|
|
72
|
+
if [[ "$1" == *.apk ]] || [[ "$1" == *.aab ]]; then
|
|
73
|
+
SEED_APK="$1"
|
|
74
|
+
ACTION="seed"
|
|
75
|
+
elif [[ "$1" == *.ipa ]]; then
|
|
76
|
+
SEED_IPA="$1"
|
|
77
|
+
ACTION="seed"
|
|
78
|
+
elif [[ "$1" == *.hbc ]] || [[ "$1" == *.bundle ]]; then
|
|
79
|
+
SEED_BUNDLE="$1"
|
|
80
|
+
ACTION="seed"
|
|
81
|
+
fi
|
|
82
|
+
;;
|
|
62
83
|
esac
|
|
63
84
|
shift
|
|
64
85
|
done
|
|
@@ -397,6 +418,22 @@ if [ ! -f "$BSDIFF" ]; then
|
|
|
397
418
|
echo " ✓ bsdiff compiled"
|
|
398
419
|
fi
|
|
399
420
|
|
|
421
|
+
find_hermesc() {
|
|
422
|
+
HERMESC_PATH="$ROOT/node_modules/hermes-compiler/hermesc/osx-bin/hermesc"
|
|
423
|
+
if [ ! -f "$HERMESC_PATH" ]; then
|
|
424
|
+
HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/osx-bin/hermesc"
|
|
425
|
+
fi
|
|
426
|
+
if [ ! -f "$HERMESC_PATH" ]; then
|
|
427
|
+
HERMESC_PATH="$ROOT/node_modules/hermes-compiler/hermesc/linux64-bin/hermesc"
|
|
428
|
+
fi
|
|
429
|
+
if [ ! -f "$HERMESC_PATH" ]; then
|
|
430
|
+
HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/linux64-bin/hermesc"
|
|
431
|
+
fi
|
|
432
|
+
if [ ! -f "$HERMESC_PATH" ]; then
|
|
433
|
+
HERMESC_PATH="$(which hermesc 2>/dev/null || echo "")"
|
|
434
|
+
fi
|
|
435
|
+
}
|
|
436
|
+
|
|
400
437
|
build_platform() {
|
|
401
438
|
local PLATFORM="$1"
|
|
402
439
|
local FLAVOR="$2"
|
|
@@ -412,9 +449,6 @@ build_platform() {
|
|
|
412
449
|
echo ""
|
|
413
450
|
echo ">>> Building $PLATFORM | Version: $APP_VERSION | OTA: v$CURRENT → v$NEXT"
|
|
414
451
|
|
|
415
|
-
# ── Bundle ──
|
|
416
|
-
# ... (rest of the function omitted for space, but I'll make sure to replace correctly)
|
|
417
|
-
|
|
418
452
|
# ── Bundle ──
|
|
419
453
|
# Workspace for building
|
|
420
454
|
local WORKSPACE="$OUTPUT/workspace"
|
|
@@ -446,10 +480,7 @@ build_platform() {
|
|
|
446
480
|
fi
|
|
447
481
|
|
|
448
482
|
# Ensure Hermes bytecode format (for both iOS and Android OTA bundles)
|
|
449
|
-
|
|
450
|
-
if [ ! -f "$HERMESC_PATH" ]; then
|
|
451
|
-
HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/osx-bin/hermesc"
|
|
452
|
-
fi
|
|
483
|
+
find_hermesc
|
|
453
484
|
|
|
454
485
|
IS_HBC=false
|
|
455
486
|
if [ -f "$BUNDLE_SOURCE" ]; then
|
|
@@ -588,11 +619,159 @@ EOF
|
|
|
588
619
|
bash "$SCRIPT_DIR/upload_ota_custom.sh" "$APP_VERSION" "$FLAVOR_LOWER" "$NEXT" "$NEW_BUNDLE_PATH" "$PLATFORM"
|
|
589
620
|
}
|
|
590
621
|
|
|
622
|
+
seed_platform() {
|
|
623
|
+
local PLATFORM="$1"
|
|
624
|
+
local FLAVOR="$2"
|
|
625
|
+
local BINARY_PATH="$3"
|
|
626
|
+
local FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
627
|
+
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
628
|
+
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
629
|
+
|
|
630
|
+
local OUTPUT="$ROOT/android/output"
|
|
631
|
+
if [ "$PLATFORM" == "ios" ]; then OUTPUT="$ROOT/ios/output"; fi
|
|
632
|
+
mkdir -p "$OUTPUT"
|
|
633
|
+
|
|
634
|
+
echo ""
|
|
635
|
+
echo ">>> Seeding Base Bundle (v1) | Platform: $PLATFORM | App: $APP_VERSION | Flavor: $FLAVOR_LOWER"
|
|
636
|
+
echo " Source binary: $BINARY_PATH"
|
|
637
|
+
|
|
638
|
+
if [ ! -f "$BINARY_PATH" ]; then
|
|
639
|
+
echo " ❌ ERROR: Source binary file not found: $BINARY_PATH"
|
|
640
|
+
exit 1
|
|
641
|
+
fi
|
|
642
|
+
|
|
643
|
+
local TARGET_BUNDLE="$OUTPUT/bundle_${APP_VERSION}_v1_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
644
|
+
local BASE_ASSET_MANIFEST="$OUTPUT/base-assets_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
645
|
+
local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest-v1_${FLAVOR_LOWER}.json"
|
|
646
|
+
local TMP_DIR=$(mktemp -d)
|
|
647
|
+
|
|
648
|
+
if [ "$PLATFORM" == "android" ]; then
|
|
649
|
+
echo " Extracting index.android.bundle from Android binary..."
|
|
650
|
+
if unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "assets/index.android.bundle"; then
|
|
651
|
+
unzip -p "$BINARY_PATH" assets/index.android.bundle > "$TMP_DIR/extracted.bundle"
|
|
652
|
+
elif unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "base/assets/index.android.bundle"; then
|
|
653
|
+
unzip -p "$BINARY_PATH" base/assets/index.android.bundle > "$TMP_DIR/extracted.bundle"
|
|
654
|
+
elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]]; then
|
|
655
|
+
cp "$BINARY_PATH" "$TMP_DIR/extracted.bundle"
|
|
656
|
+
else
|
|
657
|
+
echo " ❌ ERROR: Could not find 'assets/index.android.bundle' inside $BINARY_PATH"
|
|
658
|
+
rm -rf "$TMP_DIR"
|
|
659
|
+
exit 1
|
|
660
|
+
fi
|
|
661
|
+
else
|
|
662
|
+
echo " Extracting main.jsbundle from iOS binary..."
|
|
663
|
+
local IPA_ENTRY=$(unzip -l "$BINARY_PATH" 2>/dev/null | grep -o 'Payload/[^/]*\.app/main\.jsbundle' | head -n 1)
|
|
664
|
+
if [ -n "$IPA_ENTRY" ]; then
|
|
665
|
+
unzip -p "$BINARY_PATH" "$IPA_ENTRY" > "$TMP_DIR/extracted.bundle"
|
|
666
|
+
elif unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "main.jsbundle"; then
|
|
667
|
+
local ANY_BUNDLE=$(unzip -l "$BINARY_PATH" 2>/dev/null | grep -o '[^ ]*main\.jsbundle' | head -n 1)
|
|
668
|
+
unzip -p "$BINARY_PATH" "$ANY_BUNDLE" > "$TMP_DIR/extracted.bundle"
|
|
669
|
+
elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]]; then
|
|
670
|
+
cp "$BINARY_PATH" "$TMP_DIR/extracted.bundle"
|
|
671
|
+
else
|
|
672
|
+
echo " ❌ ERROR: Could not find 'main.jsbundle' inside $BINARY_PATH"
|
|
673
|
+
rm -rf "$TMP_DIR"
|
|
674
|
+
exit 1
|
|
675
|
+
fi
|
|
676
|
+
fi
|
|
677
|
+
|
|
678
|
+
# Check Hermes bytecode format (0xc6 magic number)
|
|
679
|
+
FIRST_BYTE=$(od -An -N1 -t x1 "$TMP_DIR/extracted.bundle" | tr -d ' ' || echo "")
|
|
680
|
+
if [ "$FIRST_BYTE" == "c6" ]; then
|
|
681
|
+
echo " ✓ Bundle is verified Hermes bytecode format."
|
|
682
|
+
cp "$TMP_DIR/extracted.bundle" "$TARGET_BUNDLE"
|
|
683
|
+
else
|
|
684
|
+
echo " Bundle is JavaScript text. Compiling to Hermes bytecode..."
|
|
685
|
+
find_hermesc
|
|
686
|
+
if [ -z "$HERMESC_PATH" ] || [ ! -f "$HERMESC_PATH" ]; then
|
|
687
|
+
echo " ❌ ERROR: hermesc compiler not found. Cannot compile bundle to bytecode."
|
|
688
|
+
rm -rf "$TMP_DIR"
|
|
689
|
+
exit 1
|
|
690
|
+
fi
|
|
691
|
+
"$HERMESC_PATH" -emit-binary -out "$TARGET_BUNDLE" "$TMP_DIR/extracted.bundle"
|
|
692
|
+
fi
|
|
693
|
+
|
|
694
|
+
# Extract base assets manifest
|
|
695
|
+
echo " Scanning assets from binary for base manifest..."
|
|
696
|
+
local NEW_BASE_ASSETS="{"
|
|
697
|
+
local ASSET_JSON_LIST="["
|
|
698
|
+
local FIRST=true
|
|
699
|
+
|
|
700
|
+
if [ "$PLATFORM" == "android" ]; then
|
|
701
|
+
unzip -q -o "$BINARY_PATH" "res/drawable*/*" "res/raw/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
|
|
702
|
+
else
|
|
703
|
+
unzip -q -o "$BINARY_PATH" "Payload/*.app/assets/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
|
|
704
|
+
fi
|
|
705
|
+
|
|
706
|
+
if [ -d "$TMP_DIR/assets_unzipped" ]; then
|
|
707
|
+
while IFS= read -r -d '' ASSET_FILE; do
|
|
708
|
+
local HASH=$(sha256 "$ASSET_FILE")
|
|
709
|
+
local FNAME=$(basename "$ASSET_FILE")
|
|
710
|
+
local EX="${FNAME##*.}"
|
|
711
|
+
local REL_PATH="${ASSET_FILE#$TMP_DIR/assets_unzipped/}"
|
|
712
|
+
|
|
713
|
+
if [ "$NEW_BASE_ASSETS" != "{" ]; then NEW_BASE_ASSETS+=", "; fi
|
|
714
|
+
NEW_BASE_ASSETS+="\"$REL_PATH\": \"$HASH\""
|
|
715
|
+
|
|
716
|
+
if [ "$FIRST" = true ]; then FIRST=false; else ASSET_JSON_LIST+=", "; fi
|
|
717
|
+
ASSET_JSON_LIST+="{\"hash\": \"$HASH\", \"ext\": \"$EX\", \"originalPath\": \"$REL_PATH\", \"isBase\": true}"
|
|
718
|
+
done < <(find "$TMP_DIR/assets_unzipped" -type f -print0 2>/dev/null)
|
|
719
|
+
fi
|
|
720
|
+
|
|
721
|
+
NEW_BASE_ASSETS+="}"
|
|
722
|
+
ASSET_JSON_LIST+="]"
|
|
723
|
+
|
|
724
|
+
echo "$NEW_BASE_ASSETS" > "$BASE_ASSET_MANIFEST"
|
|
725
|
+
cat > "$ASSET_MANIFEST_FILE" << EOF
|
|
726
|
+
{ "version": 1, "appVersion": "$APP_VERSION", "platform": "$PLATFORM", "flavor": "$FLAVOR_LOWER", "assets": $ASSET_JSON_LIST }
|
|
727
|
+
EOF
|
|
728
|
+
|
|
729
|
+
rm -rf "$TMP_DIR"
|
|
730
|
+
|
|
731
|
+
local BUNDLE_HASH=$(sha256 "$TARGET_BUNDLE")
|
|
732
|
+
echo " ✓ Base bundle v1 successfully created: $(basename "$TARGET_BUNDLE")"
|
|
733
|
+
echo " SHA-256: $BUNDLE_HASH"
|
|
734
|
+
|
|
735
|
+
# Update registry for v1
|
|
736
|
+
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "1" "[]" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
|
|
737
|
+
|
|
738
|
+
# Push if requested
|
|
739
|
+
if [ "$PUSH_REMOTE" = true ]; then
|
|
740
|
+
echo " Pushing seeded base bundle to remote..."
|
|
741
|
+
bash "$SCRIPT_DIR/upload_ota_custom.sh" "$APP_VERSION" "$FLAVOR_LOWER" "1" "$TARGET_BUNDLE" "$PLATFORM"
|
|
742
|
+
fi
|
|
743
|
+
|
|
744
|
+
echo " ✓ Seeding complete for $PLATFORM!"
|
|
745
|
+
}
|
|
746
|
+
|
|
591
747
|
# ── Main ───────────────────────────────────────────────────────────────────
|
|
592
|
-
echo "=== OTA
|
|
748
|
+
echo "=== OTA Build Pipeline | App: $APP_VERSION | Flavor: $FLAVOR ==="
|
|
593
749
|
|
|
594
|
-
# Read current version once to coordinate across platforms
|
|
595
750
|
FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
751
|
+
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
752
|
+
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
753
|
+
|
|
754
|
+
# ── Seed Action ────────────────────────────────────────────────────────────
|
|
755
|
+
if [ "$ACTION" = "seed" ]; then
|
|
756
|
+
if [ -n "$SEED_APK" ]; then
|
|
757
|
+
seed_platform "android" "$FLAVOR_LOWER" "$SEED_APK"
|
|
758
|
+
elif [ -n "$SEED_IPA" ]; then
|
|
759
|
+
seed_platform "ios" "$FLAVOR_LOWER" "$SEED_IPA"
|
|
760
|
+
elif [ -n "$SEED_BUNDLE" ]; then
|
|
761
|
+
PLATFORM="${PLATFORM_ARG:-android}"
|
|
762
|
+
seed_platform "$PLATFORM" "$FLAVOR_LOWER" "$SEED_BUNDLE"
|
|
763
|
+
else
|
|
764
|
+
echo "❌ Please provide a binary file to seed:"
|
|
765
|
+
echo " ./build_ota.sh seed --apk <path-to-apk>"
|
|
766
|
+
echo " ./build_ota.sh seed --ipa <path-to-ipa>"
|
|
767
|
+
exit 1
|
|
768
|
+
fi
|
|
769
|
+
echo ""
|
|
770
|
+
echo "=== Base Seeding Complete ==="
|
|
771
|
+
exit 0
|
|
772
|
+
fi
|
|
773
|
+
|
|
774
|
+
# Read current version once to coordinate across platforms
|
|
596
775
|
CURRENT_TOTAL=0
|
|
597
776
|
if [ -f "$REGISTRY_SHARED" ]; then
|
|
598
777
|
CURRENT_TOTAL=$(node -e "
|
|
@@ -602,6 +781,79 @@ if [ -f "$REGISTRY_SHARED" ]; then
|
|
|
602
781
|
} catch(e) { console.log(0); }
|
|
603
782
|
")
|
|
604
783
|
fi
|
|
784
|
+
|
|
785
|
+
# ── Fail-Safe Guard for OTA Updates ────────────────────────────────────────
|
|
786
|
+
# If building an OTA patch but no base version (v1) exists in the registry:
|
|
787
|
+
if [ "$ACTION" = "ota" ] && [ $CURRENT_TOTAL -eq 0 ]; then
|
|
788
|
+
echo "Checking for pre-existing Base Bundle v1..."
|
|
789
|
+
|
|
790
|
+
CHECK_PLATFORMS=()
|
|
791
|
+
if [ -n "$PLATFORM_ARG" ]; then
|
|
792
|
+
CHECK_PLATFORMS=("$PLATFORM_ARG")
|
|
793
|
+
else
|
|
794
|
+
CHECK_PLATFORMS=("android" "ios")
|
|
795
|
+
fi
|
|
796
|
+
|
|
797
|
+
ALL_FOUND=true
|
|
798
|
+
MISSING_PLATFORM=""
|
|
799
|
+
for P in "${CHECK_PLATFORMS[@]}"; do
|
|
800
|
+
P_OUTPUT="$ROOT/$P/output"
|
|
801
|
+
P_BASE_BUNDLE="$P_OUTPUT/bundle_${APP_VERSION}_v1_${P}_${FLAVOR_LOWER}.hbc"
|
|
802
|
+
|
|
803
|
+
# Check if local exists
|
|
804
|
+
if [ ! -f "$P_BASE_BUNDLE" ]; then
|
|
805
|
+
# Try downloading from remote if available
|
|
806
|
+
REMOTE_BASE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$(basename "$P_BASE_BUNDLE")"
|
|
807
|
+
download_from_minio "$REMOTE_BASE_PATH" "$P_BASE_BUNDLE" || true
|
|
808
|
+
fi
|
|
809
|
+
|
|
810
|
+
if [ -f "$P_BASE_BUNDLE" ]; then
|
|
811
|
+
echo " ✓ Found Base Bundle v1 for $P: $(basename "$P_BASE_BUNDLE")"
|
|
812
|
+
# Auto-adopt base bundle in registry
|
|
813
|
+
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "1" "[]" "$P" "" "$FLAVOR_LOWER"
|
|
814
|
+
else
|
|
815
|
+
ALL_FOUND=false
|
|
816
|
+
MISSING_PLATFORM="$P"
|
|
817
|
+
fi
|
|
818
|
+
done
|
|
819
|
+
|
|
820
|
+
if [ "$ALL_FOUND" = true ]; then
|
|
821
|
+
echo " ✓ Base bundle v1 verified and registered. Proceeding with incremental OTA update..."
|
|
822
|
+
CURRENT_TOTAL=1
|
|
823
|
+
else
|
|
824
|
+
if [ "$FORCE_BASE" = true ]; then
|
|
825
|
+
echo " ⚠ Warning: --force flag detected. Creating base bundle v1 from current working tree..."
|
|
826
|
+
ACTION="base"
|
|
827
|
+
else
|
|
828
|
+
echo ""
|
|
829
|
+
echo "╔══════════════════════════════════════════════════════════════════════════════════════════╗"
|
|
830
|
+
echo "║ ❌ CANNOT BUILD OTA: MISSING BASE BUNDLE (v1) ║"
|
|
831
|
+
echo "╠══════════════════════════════════════════════════════════════════════════════════════════╣"
|
|
832
|
+
echo "║ App Version: $APP_VERSION | Flavor: $FLAVOR_LOWER | Missing: $MISSING_PLATFORM"
|
|
833
|
+
echo "║"
|
|
834
|
+
echo "║ No base bundle (v1) was found for this app version (neither locally nor on remote). ║"
|
|
835
|
+
echo "║ If an OTA update is built now, it would generate a base bundle from your current ║"
|
|
836
|
+
echo "║ working tree, which will MISMATCH the version installed on user devices and force a ║"
|
|
837
|
+
echo "║ 100% full-bundle download (wasting bandwidth and bypassing patch benefits). ║"
|
|
838
|
+
echo "║"
|
|
839
|
+
echo "║ How to resolve: ║"
|
|
840
|
+
echo "║ 1. If you already have the release APK/IPA submitted to Google Play / App Store: ║"
|
|
841
|
+
echo "║ Seed the exact base bundle from your release binary: ║"
|
|
842
|
+
echo "║ ./build_ota.sh seed --apk <path-to-apk> ║"
|
|
843
|
+
echo "║ ./build_ota.sh seed --ipa <path-to-ipa> ║"
|
|
844
|
+
echo "║"
|
|
845
|
+
echo "║ 2. If you are preparing a brand new Store release right now from this commit: ║"
|
|
846
|
+
echo "║ ./build_ota.sh base ║"
|
|
847
|
+
echo "║"
|
|
848
|
+
echo "║ 3. If you really want to force create base v1 from current code: ║"
|
|
849
|
+
echo "║ ./build_ota.sh --force ║"
|
|
850
|
+
echo "╚══════════════════════════════════════════════════════════════════════════════════════════╝"
|
|
851
|
+
echo ""
|
|
852
|
+
exit 1
|
|
853
|
+
fi
|
|
854
|
+
fi
|
|
855
|
+
fi
|
|
856
|
+
|
|
605
857
|
TARGET_OTA=$((CURRENT_TOTAL + 1))
|
|
606
858
|
|
|
607
859
|
if [ -n "$PLATFORM_ARG" ]; then
|
package/tools/setup_project.js
CHANGED
|
@@ -60,6 +60,13 @@ const XCODE_BUNDLE_SYNC_SNIPPET = [
|
|
|
60
60
|
' mkdir -p "$(dirname \\"$DEST_FILE\\")"',
|
|
61
61
|
' cp "$BUNDLE_RESULT" "$DEST_FILE"',
|
|
62
62
|
' echo "Bundle synced to $DEST_FILE"',
|
|
63
|
+
' if [ "$CONFIGURATION" = "Release" ]; then',
|
|
64
|
+
' APP_VER="${MARKETING_VERSION:-1.0.0}"',
|
|
65
|
+
' mkdir -p "$PROJECT_DIR/output"',
|
|
66
|
+
' cp "$BUNDLE_RESULT" "$PROJECT_DIR/output/bundle_${APP_VER}_v1_ios_production.hbc"',
|
|
67
|
+
' cp "$BUNDLE_RESULT" "$PROJECT_DIR/output/bundle_${APP_VER}_v1_ios_development.hbc" 2>/dev/null || true',
|
|
68
|
+
' echo "[react-native-bspatch] ✓ Auto-captured iOS base bundle v1 to output/"',
|
|
69
|
+
' fi',
|
|
63
70
|
'else',
|
|
64
71
|
' echo "Bundle not found at $BUNDLE_RESULT (skipping sync)"',
|
|
65
72
|
'fi',
|
|
@@ -68,6 +75,48 @@ const XCODE_BUNDLE_SYNC_SNIPPET = [
|
|
|
68
75
|
// The escaped version as it appears in pbxproj shellScript value
|
|
69
76
|
const XCODE_BUNDLE_SYNC_MARKER = 'react-native-bspatch] Sync compiled bundle to output/';
|
|
70
77
|
|
|
78
|
+
const ANDROID_GRADLE_MARKER = '[react-native-bspatch] Auto-capture Base Bundle';
|
|
79
|
+
const ANDROID_GRADLE_SNIPPET = `
|
|
80
|
+
// [react-native-bspatch] Auto-capture Base Bundle on Release builds
|
|
81
|
+
afterEvaluate {
|
|
82
|
+
android.applicationVariants.all { variant ->
|
|
83
|
+
if (variant.buildType.name == 'release') {
|
|
84
|
+
def flavorName = (variant.flavorName ?: 'development').toLowerCase()
|
|
85
|
+
if (flavorName == 'dev') flavorName = 'development'
|
|
86
|
+
if (flavorName == 'prod') flavorName = 'production'
|
|
87
|
+
def versionName = variant.versionName ?: '1.0.0'
|
|
88
|
+
def capVariant = variant.name.capitalize()
|
|
89
|
+
def bundleTask = tasks.findByName("createBundle\${capVariant}JsAndAssets")
|
|
90
|
+
if (bundleTask != null) {
|
|
91
|
+
def saveTask = tasks.register("saveOtaBaseBundle\${capVariant}") {
|
|
92
|
+
doLast {
|
|
93
|
+
def bundleDir = file("\$buildDir/generated/assets/react/\${variant.name}")
|
|
94
|
+
def bundleFile = file("\$bundleDir/index.android.bundle")
|
|
95
|
+
def outputDir = file("\$rootDir/output")
|
|
96
|
+
if (bundleFile.exists()) {
|
|
97
|
+
outputDir.mkdirs()
|
|
98
|
+
def destName = "bundle_\${versionName}_v1_android_\${flavorName}.hbc"
|
|
99
|
+
copy {
|
|
100
|
+
from bundleFile
|
|
101
|
+
into outputDir
|
|
102
|
+
rename { destName }
|
|
103
|
+
}
|
|
104
|
+
println "[react-native-bspatch] ✓ Auto-captured base bundle: android/output/\${destName}"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
bundleTask.finalizedBy(saveTask)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
function patchAppBuildGradle(content) {
|
|
116
|
+
if (content.includes(ANDROID_GRADLE_MARKER)) return content;
|
|
117
|
+
return content.trimEnd() + '\n' + ANDROID_GRADLE_SNIPPET;
|
|
118
|
+
}
|
|
119
|
+
|
|
71
120
|
const IOS_IMPORT = 'import react_native_bspatch';
|
|
72
121
|
const IOS_BUNDLE_URL_OVERRIDE = `
|
|
73
122
|
override func bundleURL() -> URL? {
|
|
@@ -275,12 +324,28 @@ function main() {
|
|
|
275
324
|
patchFile(mainAppFiles[0], 'MainApplication.kt', patchMainApplication);
|
|
276
325
|
}
|
|
277
326
|
|
|
327
|
+
const appGradleFiles = findFile(path.join(ANDROID_DIR, 'app'), (name) => name === 'build.gradle');
|
|
328
|
+
if (appGradleFiles.length === 0) {
|
|
329
|
+
console.log(' ⚠ No android/app/build.gradle found.');
|
|
330
|
+
} else {
|
|
331
|
+
patchFile(appGradleFiles[0], 'android/app/build.gradle (auto-capture base bundle)', patchAppBuildGradle);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Ensure android/output/ exists
|
|
335
|
+
const androidOutputDir = path.join(ANDROID_DIR, 'output');
|
|
336
|
+
if (!fs.existsSync(androidOutputDir)) {
|
|
337
|
+
fs.mkdirSync(androidOutputDir, { recursive: true });
|
|
338
|
+
console.log(' ✓ Created android/output/ directory');
|
|
339
|
+
}
|
|
340
|
+
|
|
278
341
|
console.log('\n=== Setup Complete ===');
|
|
279
342
|
console.log('Next steps:');
|
|
280
|
-
console.log(' 1.
|
|
281
|
-
console.log('
|
|
282
|
-
console.log('
|
|
283
|
-
console.log('
|
|
343
|
+
console.log(' 1. Base bundles will now automatically be captured on Release builds (Gradle & Xcode).');
|
|
344
|
+
console.log(' 2. Or seed directly from an existing Store binary:');
|
|
345
|
+
console.log(' yarn react-native-bspatch seed --apk path/to/release.apk');
|
|
346
|
+
console.log(' yarn react-native-bspatch seed --ipa path/to/release.ipa');
|
|
347
|
+
console.log(' 3. Or create base release manually: yarn ota:init');
|
|
348
|
+
console.log(' 4. Then publish OTA updates safely with: yarn ota:build\n');
|
|
284
349
|
}
|
|
285
350
|
|
|
286
351
|
main();
|