| … | … | @@ -128,6 +128,7 @@ async function boot () { |
| 128 | 128 | // APK's own bundle, which is the one known to work because it shipped with this binary. |
| 129 | 129 | let bundlePath = dest |
| 130 | 130 | let bundleHashChosen = null |
| 131 | let stagedAddonsDir = null |
| 131 | 132 | try { |
| 132 | 133 | const bdir = RNFS.DocumentDirectoryPath + '/bundles' |
| 133 | 134 | const readJson = async (p) => { try { return JSON.parse(await RNFS.readFile(p, 'utf8')) } catch { return null } } |
| … | … | @@ -152,6 +153,9 @@ async function boot () { |
| 152 | 153 | await RNFS.writeFile(bdir + '/pending.json', JSON.stringify({ treeHash: choice.hash }), 'utf8') |
| 153 | 154 | bundlePath = bdir + '/' + choice.file |
| 154 | 155 | bundleHashChosen = choice.hash |
| 156 | // chooseBundle owns this name — it is the one module both sides import, and the store that writes |
| 157 | // the directory lives on the other side of a bridge with no shared filesystem API. |
| 158 | if (choice.addons) stagedAddonsDir = bdir + '/' + choice.addons |
| 155 | 159 | console.log('[worklet] booting STAGED bundle ' + choice.hash.slice(0, 12) + '… (' + choice.reason + ')') |
| 156 | 160 | } |
| 157 | 161 | } catch (e) { |
| … | … | @@ -191,8 +195,45 @@ async function boot () { |
| 191 | 195 | console.log('[worklet] could not write the shell handoff (the node boots without it): ' + e.message) |
| 192 | 196 | } |
| 193 | 197 | |
| 198 | // ── THE LOADER ──────────────────────────────────────────────────────────────────────────────────── |
| 199 | // A `linked:` bundle NAMES the sonames the APK must supply, so a staged bundle demanding one this APK |
| 200 | // lacks could only ever die at dlopen. lib/loader.js registers OTA-staged addons in Bare's Addon.cache |
| 201 | // under exactly those names — a cache hit short-circuits dlopen-by-soname — and then loads the chosen |
| 202 | // bundle. Anything not staged still falls through to the APK, so staging is additive. |
| 203 | // |
| 204 | // ⛔ THE LOADER CANNOT BE PART OF THE STAGED BUNDLE. Bare loads whatever file is passed here as its own |
| 205 | // entry, so there is no "before" inside it; and preload code inside the staged bundle would import |
| 206 | // bare-fs as `linked:libbare-fs.<staged version>.so`, the very soname that might be missing. |
| 207 | // |
| 208 | // ⚠ FALLING BACK TO THE OLD SHAPE IS SAFE, and is why every failure below is caught. Starting the |
| 209 | // bundle directly is exactly what this app did before the loader existed: a bundle whose addons the |
| 210 | // APK already satisfies — every release published so far — runs identically. And one that NEEDS staged |
| 211 | // addons fails at dlopen, never reaches ready, and is condemned by the watchdog on the next boot, |
| 212 | // which is the correct outcome rather than a silent downgrade. |
| 213 | let entryPath = bundlePath |
| 214 | let startArgs = [storage] |
| 215 | try { |
| 216 | const loaderDest = RNFS.DocumentDirectoryPath + '/loader.bundle' |
| 217 | // Copied natively and unconditionally: it is a few hundred KB, not the 32 MB worklet bundle, so the |
| 218 | // size-stamp cache that exists for that one would be more machinery than it saves. Copying every |
| 219 | // boot also means an APK upgrade can never leave a stale loader behind. |
| 220 | await RNFS.copyFileAssets('loader.bundle', loaderDest) |
| 221 | // ⛔ STORAGE STAYS LAST. lib/node.js reads Bare.argv[Bare.argv.length - 1], and so does every bundle |
| 222 | // staged before the loader existed — which is what makes this safe to ship to a device already |
| 223 | // holding one. The loader reads its own inputs by TAG, so it does not care where they sit. |
| 224 | const args = ['--bundle=' + bundlePath] |
| 225 | if (bundleHashChosen && stagedAddonsDir) args.push('--addons=' + stagedAddonsDir) |
| 226 | args.push(storage) |
| 227 | entryPath = loaderDest |
| 228 | startArgs = args |
| 229 | console.log('[worklet] entry is the loader' + (stagedAddonsDir ? ' with staged addons ' + stagedAddonsDir : ' (no staged addons)')) |
| 230 | } catch (e) { |
| 231 | // An APK built before pack:loader existed has no such asset. Not fatal — see above. |
| 232 | console.log('[worklet] no loader asset, starting the bundle directly: ' + e.message) |
| 233 | } |
| 234 | |
| 194 | 235 | // Path + null source => NativeBareKit.startFile: the runtime reads it, not us. |
| 195 | | worklet.start(bundlePath, source, [storage]) |
| 236 | worklet.start(entryPath, source, startArgs) |
| 196 | 237 | console.log('[worklet] start() returned') |
| 197 | 238 | |
| 198 | 239 | // ⛔ KEEP RUNNING WITH THE SCREEN OFF. Immediately after start(), because until this returns the node |