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 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 ──► ota:init ──► ota:build ──► (ota:build ◄── repeat for each release)
376
- │ │ │
377
- │ │ └── 1. Bundle JS → Hermes bytecode (.hbc)
378
- │ │ 2. Compare assets → generate asset-manifest-{ver}.json
379
- │ │ 3. bsdiff against previous bundle → .patch files
380
- │ │ 4. Upload to server + update ota_registry.json
381
- │ │ 5. Write otaPatches.generated.ts for bundled patches fallback
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 — adds build-phase script to sync bundle → ios/output/
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. Run once per project. |
399
- | `yarn ota:init` | First release: build base APK/IPA + bundle + base asset manifest. Creates `ota_registry.json`. |
400
- | `yarn ota:build` | Subsequent releases: generate incremental .patch from previous bundle, upload to server, update registry. |
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. Initialize the first release (creates base APK + registry)
421
- yarn ota:init
422
- # Or per platform:
423
- yarn ota:init --platform android # Android only
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-bspatch",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Premium OTA Update Solution for React Native using TurboModules and bsdiff",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -45,20 +45,41 @@ PLATFORM_ARG=""
45
45
  APP_VERSION_ARG=""
46
46
  NON_INTERACTIVE=false
47
47
 
48
- PUSH_REMOTE=false
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
- apk|ipa|seed) ACTION="base" ;; # Compatibility for legacy/seed actions
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
- HERMESC_PATH="$ROOT/node_modules/hermes-compiler/hermesc/osx-bin/hermesc"
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 Dual Build | App: $APP_VERSION | Flavor: $FLAVOR ==="
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
@@ -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. iOS: Run Xcode build once to populate ios/output/main.jsbundle');
281
- console.log(' (or run: yarn ota:init --platform ios)');
282
- console.log(' 2. Android: Run: yarn ota:init --platform android');
283
- console.log(' 3. Then: yarn ota:build to start publishing OTA updates\n');
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();