Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@ jobs:
- name: Stage NAPI/addon headers
run: ./tools/copy_libnode_headers.sh android

# Run on cache hits too: older cached SDK copies may still carry symbols.
- name: Strip lite runtime symbols
if: matrix.flavor == 'lite'
env:
MY_ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: python3 tools/strip-android-runtime.py --ndk "$MY_ANDROID_NDK_HOME" out_android

# After the header staging, not right after Build, so the saved tree is
# byte-identical to what the combined actions/cache used to save from its
# post step. Guarded on the same condition as Build: nothing to save when
Expand Down Expand Up @@ -596,7 +603,7 @@ jobs:
if [ "$NODEJS_MOBILE_FLAVOR" = "full" ]; then
./configure --with-intl=full-icu --v8-enable-temporal-support
else
./configure --with-intl=none --v8-disable-temporal-support
./configure --with-intl=full-icu --v8-disable-temporal-support --without-ffi --without-amaro --without-inspector --without-sqlite --disable-single-executable-application --v8-disable-object-print
fi

- name: Combine
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/ios-simulator-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,9 @@ permissions:

jobs:
ios-simulator-tests:
# Both flavors run the SAME curated list, no exclusions: the whole list
# verified passing on a local lite build despite iOS lite dropping the
# WebAssembly + Intl globals (--v8-lite-mode + intl=none) — the ICU tests
# self-skip via common.hasIntl and the WebAssembly-named test only
# inspects a string fixture. Each flavor fetches its own framework from
# the same Build run (which produces both).
# Both flavors run the same curated list with full ICU enabled. iOS uses
# jitless V8/polywasm in both flavors. Each leg fetches its own framework
# from this Build run; no flavor-specific test exclusions are added.
strategy:
fail-fast: false
matrix:
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/verify-patches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ jobs:
- name: Reconstruct + verify
run: scripts/prepare.sh out

- uses: actions/setup-node@v6
with:
node-version: '24'

- name: Check danmu_api flavor configuration and ICU contract
run: python3 scripts/test-mobile-flavors.py out

- name: Install LLVM tools for strip regression
run: |
sudo apt-get update
sudo apt-get install -y llvm

- name: Check lite stripping preserves embedding exports
run: python3 scripts/test-runtime-stripping.py out

- name: Check mobile toolchain actions
run: |
python3 -m pip install setuptools packaging
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ mobile build is identified by `process.versions.mobile` (e.g. `"26.10.0-0"`).
Node 26 prereleases are published from `recipe-v26` after the release gates
pass. CI preview packages are also available from successful **Build** runs.
Published Node 24 assets remain on the Releases page.
Node 26 full builds enable Temporal and `node:ffi`. Lite builds also enable
`node:ffi`, while Temporal stays disabled with the no-ICU flavor. See
Node 26 full builds enable Temporal and `node:ffi`. New lite builds retain
a custom ICU data profile and disable both Temporal and `node:ffi`. See
[version-specific configuration](docs/VERSION-LINES.md#node-26-configuration).

Download the zips from [Releases](../../releases) — four per release:
Expand All @@ -32,12 +32,13 @@ nodejs-mobile-android-X.Y.Z-R.zip nodejs-mobile-ios-X.Y.Z-R.zip
nodejs-mobile-android-lite-X.Y.Z-R.zip nodejs-mobile-ios-lite-X.Y.Z-R.zip
```

**`full`** is the general-purpose binary — pick it unless size is critical.
**`lite`** reduces size by dropping ICU, the inspector,
`node:sqlite`, and TypeScript type-stripping (see
[BUILDING.md](docs/BUILDING.md#the-lite-variant) for the
full list and the caveats — the `Intl` one needs checking against your
dependency tree).
**`full`** is the general-purpose binary. **`lite`** embeds a custom ICU data profile for
`danmu_api`'s Chinese sorting, Unicode normalization and GBK/Big5 decoding,
but omits Inspector, SQLite/Web Storage, TypeScript type-stripping and SEA.
Node 26 lite also omits Temporal and `node:ffi`.
See [BUILDING.md](docs/BUILDING.md#the-lite-variant) and the
[danmu_api compatibility contract](docs/DANMU-LITE.md). Older release assets
keep the feature set they were built with; these changes apply to new builds.

To embed it in an app, most people use a plugin rather than the raw library:
[nodejs-mobile-react-native](https://github.com/nodejs-mobile/nodejs-mobile-react-native)
Expand Down
76 changes: 34 additions & 42 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,10 @@ The build action selects the host triple for snapshot tools and a separate
mobile triple for the library. Android ARM snapshots use i686 on x64 Linux
hosts, matching V8’s 32-bit host tools. Android target archives use the supplied NDK
and SDK level; iOS target archives use the device/simulator SDK and iOS 14
minimum deployment target. Lite builds omit Temporal with ICU and need no Rust.
Both flavors include `node:ffi`. Full builds use `--with-intl=full-icu` so
Temporal can load named time zones; small-ICU currently fails that path.
This increases the full library’s bundled ICU data size. Lite retains
`--with-intl=none`.
minimum deployment target. Lite builds omit Temporal and FFI and need no Rust.
Both flavors use `--with-intl=full-icu`; full builds need the complete data for
Temporal named time zones, while both flavors need it for Chinese collation
and legacy character decoders.

## Android — build on Linux

Expand Down Expand Up @@ -168,45 +167,38 @@ To build it instead of the default, set `NODEJS_MOBILE_FLAVOR=lite` on either
target's build command.

The build ships in two flavors (selected by `NODEJS_MOBILE_FLAVOR`, default
`full`). **`full`** is the general-purpose binary all consumers get. **`lite`**
is a smaller binary for consumers that don't need the full feature set, built by
layering feature-drops — and on Android one V8 configuration change, [pointer
compression](#pointer-compression-android-lite-only) — on top of the full
configure, so the full binary and its test gate are unchanged.
`full`). Full bundles **complete ICU data**; lite bundles the reviewed **danmu ICU profile**. The lite flavor is tailored to `danmu_api`:
it keeps Chinese collation, NFKC normalization, legacy text decoders, networking,
compression and JavaScript module loading, while removing unused optional features.

What `lite` drops (all already-available upstream `configure` flags, so no extra
patch-stack surface):

| Cut | Why it can be dropped |
| Lite cut | Compatibility cost |
|---|---|
| `--without-amaro` (TS type-stripping) | for consumers shipping plain `.js` |
| `--without-inspector` | not used in production |
| `--without-sqlite` | for consumers using the `better-sqlite3` addon, not `node:sqlite` |
| `--with-intl=none` (no ICU) | for consumers that use no `Intl.*` — verify against your own dependency tree (a dependency may reference `Intl` from a code path you never call) |
| **iOS only:** `--v8-lite-mode` | drops the compiled JIT + V8 WASM engine, both **dead on iOS** (it runs jitless; WebAssembly is served by the bundled polywasm polyfill — see [FAQ](./FAQ.md#does-fetch-work-what-about-webassembly)). This is the big lever. |

Dead-code stripping (`--gc-sections`) applies to **both** Android flavors: the
linker only discards unreferenced sections, so it costs no functionality.

Historical Node 24 shipping sizes (arm64, after symbol strip; these are
not measurements of the Node 26 port):

- **iOS:** the dead-code removal above cut the lite device slice
**54.5 → 33.8 MB** (−38%); full shrinks by the same ~20 MB since every cut
is flavor-neutral (its size is measured by the release CI).
- **Android:** **61.5 MB (full)** / **45.6 MB (lite)** with gc-sections on
both flavors; no
`--v8-lite-mode` (Android keeps the JIT and V8's native WASM for undici).

Those figures were measured before pointer compression was turned on for
Android; it aims at the heap rather than at the binary, and the release CI
re-measures the shipping artifacts on every release.

`build-id` (`-Wl,--build-id=sha1`) is emitted on the Android `libnode.so` in
**both** flavors so crash reporters (e.g. Sentry) can symbolicate native
crashes. The safeguard for `intl=none` is running your own application's test
suite against the lite binary — it catches `Intl` breakage from future
dependency changes.
| `--without-amaro` | Cannot execute TypeScript through Node's built-in type stripping; JS/MJS/CJS and ESM remain supported |
| `--without-inspector` | No Node Inspector debugging |
| `--without-sqlite` | No `node:sqlite` or Node Web Storage; the core uses files/JSON/Redis and the pure UniDB implementation |
| `--disable-single-executable-application` | No SEA executable packaging; embedding and external core directories remain supported |
| `--v8-disable-object-print` | Omits native debugger object-print helpers; JavaScript logging remains supported |
| `--v8-disable-temporal-support` | No Temporal API; ordinary Date/timers remain supported |
| `--without-ffi` | No experimental `node:ffi`; embedding through `node::Start` and N-API are separate capabilities |

Android retains the JIT and V8's native WebAssembly, which Undici uses for
`fetch`. Its 64-bit lite builds also retain the pointer-compression setting
below. Both iOS flavors retain the existing jitless `--v8-lite-mode` and
polywasm implementation; lite does not add an Android-style 4 GB pointer cage.
Both iOS flavors link ICU; lite profiles its embedded data.

Android lite SDK copies also remove non-runtime symbols/debug sections while
verifying all dynamic exports remain unchanged. See the retained ICU locales
and encodings in [DANMU-LITE.md](DANMU-LITE.md).

Dead-code stripping and Android build IDs continue to apply to both flavors.
Older published size measurements used no-ICU lite builds and do not describe
this feature set. Measure freshly compiled artifacts before quoting size savings.

The device boot smokes assert the ICU API/data contract, GBK/GB2312/Big5 decoding, Chinese
sorting and NFKC normalization, and check the expected full/lite optional
feature set. See [DANMU-LITE.md](DANMU-LITE.md) for the compatibility contract
and the recipe-level configure/archive tests.

### Pointer compression (Android lite only)

Expand Down
87 changes: 87 additions & 0 deletions docs/DANMU-LITE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# danmu_api runtime contract

Full builds embed complete ICU data. Lite keeps the complete ICU API mode
(`--with-intl=full-icu`, `icu_small=false`) but embeds a reviewed data profile
from `tools/icu/danmu-lite.json`. Do not substitute Node's default small-ICU
filter: it removes legacy encoding converters and does not satisfy the core.

## ICU data retained by lite

- Every web encoding exposed by Node's TextDecoder, including GBK/GB2312,
GB18030, Big5, Shift-JIS, EUC-JP, ISO-2022-JP, EUC-KR, European/Windows
encodings and their label aliases. Converter dependency tables are kept too.
- English, simplified/traditional Chinese, Cantonese, Japanese and Korean
locale families and their regional variants/parent fallbacks. Keep their
collation, calendar/date, number/currency/unit, relative-time, plural, list,
display-name and segmentation resources.
- Unicode normalization/property data for all scripts, including NFC/NFD and
NFKC/NFKD; IDNA/UTS46 domain handling; all time zones; break rules and
dictionaries for segmentation. A foreign-language text does not require
that language's locale-formatting resources to be decoded or normalized.

Lite removes unrelated locale families, non-web converter tables,
transliteration and rule-based spellout data. Other explicit Intl locales
may fall back to a retained locale. Adding a source that requires a new
locale's formatting/collation must extend the profile; future feature use
cannot be inferred from today's imports.

`prepare-mobile-icu.py` uses the host ICU tools, checks required resources
before/after trimming and lets icupkg validate package dependencies. A unique
intermediate package name prevents an installed host's full ICU from hiding
missing resources. The final conventional data entry point is preserved.
With the pinned ICU 78.3, data goes from 33,105,536 to 11,024,064 bytes. This
measures data only, not a complete newly compiled libnode or compressed ZIP.

## Other lite subtractions

Lite removes Amaro, Inspector, SQLite/Web Storage, SEA executable packaging
and V8 native-debugger object-print support. Node 26 lite also removes Temporal
and FFI. Core JS/MJS/CJS modules and the pure `@dan-uni/dan-any` UniDB do not
require these features. OpenCC remains a JavaScript dependency.

Keep HTTP/HTTPS/TLS/crypto, DNS, fetch/Undici, WebAssembly, compression including
Brotli/zstd, filesystem/module loading, streams, AsyncLocalStorage and the
existing Worker/child-process APIs. WASI/HTTP2 are not separately trimmed in
this change; Undici itself references node:http2.

Android retains JIT/native WASM. Lite uses pointer compression only on 64-bit
Android; it primarily reduces heap memory and changes the direct V8-addon ABI.
iOS retains its existing jitless/polywasm setup without pointer compression.

## Android symbol stripping

Android lite SDK copies are stripped with the NDK's `llvm-strip
--strip-unneeded`. The linked build output remains available locally for
debugging. CI also trims restored lite cache entries before upload. The
helper compares every dynamic symbol before/after and refuses a changed ABI;
node::Start and exported N-API functions remain available. Non-runtime debug
sections and the ordinary symbol/string tables are removed. Full SDK copies
retain their previous symbol policy.

The apps may already strip their packaged libraries: producer-side stripping
reduces SDK downloads, but repeated stripping does not reduce an already
stripped APK a second time. Header declarations are needed to compile the
embedders and do not become a separate runtime payload.

## Validation

After materialization, run from the recipe directory:

```sh
python3 scripts/test-mobile-flavors.py out
```

This invokes Android's configure wrapper for full/lite on arm, arm64 and
x86_64 using a configure recorder, evaluates the real iOS flavor/archive and
ICU profile selection, and runs shipped ICU regressions on host Node.

Both platform boot smokes check all 39 canonical TextDecoder encodings,
streaming/multibyte examples, retained locale APIs, normalization, IDNA and
time zones. Lite additionally verifies that unselected locale data was
actually removed. Node 26 full still exercises native FFI calls/callbacks and
Temporal timezone behavior; lite asserts those features are absent.

Before publishing, complete cross-compilation and device gates and measure
actual new artifacts. Previously published archives and either app's pinned
URL/checksum remain unchanged. Future core/dependency updates must be checked
against this contract.
10 changes: 5 additions & 5 deletions docs/EMBEDDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ Only the options upstream marks `kAllowedInEnvvar` are accepted, and

### Situational

**`NODE_ICU_DATA`** — meaningful only on the `full` flavor, which is built
`--with-intl=small-icu` on both platforms. Ship a full `icudt*.dat` as an asset
and point at it if you need locales beyond the small set. It is a **no-op on
`lite`**, which is `--with-intl=none` and has no `Intl` at all. See
[BUILDING.md](./BUILDING.md) for the flavor split.
**`NODE_ICU_DATA`** — current full and lite builds embed the required ICU data, so no
external data file is needed for Chinese locales or GBK/Big5 decoding.
If overriding ICU data, use data matching the runtime's ICU version. Older
small-ICU builds may need an external file; older no-ICU lite binaries cannot
be fixed by supplying a data file. See [BUILDING.md](./BUILDING.md).

**`NODE_USE_SYSTEM_CA=1`** — only if you need user- or MDM-installed CAs
honoured, and understand that it buys less than it appears to: iOS has no API to
Expand Down
17 changes: 10 additions & 7 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ Not every API is supported on mobile, the main reason for this being that the mo
- **Android < 8.0**: values can be inconsistent — some devices power cores on and off as an energy-saving strategy, so a core that is off at the moment of the call reports zero.
- `os.availableParallelism()` — subject to the same platform limits as `os.cpus()`; treat the value as advisory.

On the **`lite`** flavor (built without ICU — see
[BUILDING.md](./BUILDING.md#the-lite-variant)), JavaScript features that
depend on internationalization support are unavailable — `Intl.*`, and RegExp
Unicode property escapes such as `/\p{Letter}+/u`. The default `full` flavor
supports them, with English-only locale data built in (`small-icu`); point
`NODE_ICU_DATA` at a full `icudt*.dat` for more locales
([EMBEDDING.md](./EMBEDDING.md#situational)).
Lite limits locale-specific data to English/Chinese/Cantonese/Japanese/Korean,
while keeping all Node web text encodings, Unicode normalization and time zones.
Other locale formatting may fall back; see [DANMU-LITE.md](DANMU-LITE.md).

Both current flavors bundle ICU, including `Intl`, Unicode
normalization, Chinese sorting and legacy decoders such as GBK and Big5.
Lite drops optional features listed in [BUILDING.md](./BUILDING.md#the-lite-variant),
including Inspector, SQLite/Web Storage and TypeScript type-stripping.
Older releases may have small-ICU full builds and no-ICU lite builds; consult
the build recipe for the exact release.

WebAssembly is a special case on iOS — see
[the next question](#does-fetch-work-what-about-webassembly).
Expand Down
5 changes: 3 additions & 2 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ The four archives are
`SHA256SUMS`. Android includes arm64-v8a, armeabi-v7a and x86_64. iOS includes
arm64 device and simulator slices. Verify the checksum after downloading.

Node 26.10 full builds enable Temporal and `node:ffi`; lite enables `node:ffi`
and omits Temporal with ICU. The older 26.1 release disables both features.
Node 26 full builds enable Temporal and `node:ffi`; the new lite recipe
omits both and embeds the danmu ICU data profile. Already published 26.10.0-0 lite assets
have FFI and no ICU; the older 26.1 release disables both features.
iOS uses jitless V8 and the bundled WebAssembly polyfill. See
[VERSION-LINES.md](VERSION-LINES.md) for compatibility limits.

Expand Down
6 changes: 4 additions & 2 deletions docs/VERSION-LINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ Android archives use the matching NDK compiler driver and position-independent
code. iOS frameworks link only the target Rust archive, alongside `libffi`,
`libnode_base`, and `libncrypto_engine`.

Lite builds enable `node:ffi` but retain `--with-intl=none` and explicitly
disable Temporal, which currently requires bundled ICU. They do not need Rust.
New lite builds retain the complete API mode `--with-intl=full-icu`, embed
the danmu ICU data profile, and explicitly disable
Temporal and `node:ffi`. They do not need Rust. Already published 26.10.0-0
lite assets have FFI and no ICU; changing the recipe does not modify those assets.
iOS libffi uses its precompiled executable trampoline table for native-to-JS
callbacks rather than generating executable callback code at runtime. Boot
smokes exercise C calls, native callbacks, and Temporal date/timezone behavior;
Expand Down
2 changes: 1 addition & 1 deletion expected-tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
# run prepare.sh (or regenerate-patches.py, which prints the hash) and
# update the value below in the same commit. If you forget, CI fails and
# prints the hash it got.
a1fc1ec2274055e5a9d3f308c710c37f6e691466
cf1e359f446d47683db958b3f70a66ad52ecbb61
5 changes: 5 additions & 0 deletions mobile-src/tools/android_build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ BUILD_ARCH() {
echo "Could not find libnode.so file after compilation"
exit 1
fi
# Keep the linked build output for debugging; only trim the copied lite SDK.
if [ "${NODEJS_MOBILE_FLAVOR:-full}" = "lite" ]; then
python3 tools/strip-android-runtime.py --ndk "$ANDROID_NDK_PATH" \
"out_android/$TARGET_ARCH_FOLDER/libnode.so"
fi
}

if [ $# -eq 2 ]; then
Expand Down
Loading
Loading