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