From b70ea5b172294eed189a56df017e4e853a67ca23 Mon Sep 17 00:00:00 2001 From: fogtape Date: Fri, 2 Oct 2026 17:28:58 +0800 Subject: [PATCH 1/8] build: retain full ICU and tailor Node 26 lite to danmu_api Keep GBK/Big5 decoding, Chinese collation and NFKC normalization in both flavors. Remove optional features unused by the core, align iOS linking and boot smokes, and regenerate the recipe with verified tree hashes. Assisted-by: Codex Signed-off-by: fogtape --- .github/workflows/build.yml | 2 +- .github/workflows/ios-simulator-tests.yml | 9 +- .github/workflows/verify-patches.yml | 7 + README.md | 17 +- docs/BUILDING.md | 72 ++++---- docs/DANMU-LITE.md | 44 +++++ docs/EMBEDDING.md | 10 +- docs/FAQ.md | 13 +- docs/RELEASING.md | 5 +- docs/VERSION-LINES.md | 5 +- expected-tree.txt | 2 +- mobile-src/tools/ios_framework_prepare.sh | 20 ++- .../mobile-test/smoke/mobile-features.js | 98 +++++++---- .../mobile-test/smoke/run-android-smoke.sh | 4 +- .../tools/mobile-test/smoke/run-ios-smoke.sh | 5 +- ...S-dest-os-and-toolchain-configure.py.patch | 15 +- scripts/package-preview.py | 7 +- scripts/test-mobile-flavors.py | 157 ++++++++++++++++++ 18 files changed, 365 insertions(+), 127 deletions(-) create mode 100644 docs/DANMU-LITE.md create mode 100755 scripts/test-mobile-flavors.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48d58f044d..eb69d9b77b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -596,7 +596,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 diff --git a/.github/workflows/ios-simulator-tests.yml b/.github/workflows/ios-simulator-tests.yml index 9c953235aa..4795137a53 100644 --- a/.github/workflows/ios-simulator-tests.yml +++ b/.github/workflows/ios-simulator-tests.yml @@ -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: diff --git a/.github/workflows/verify-patches.yml b/.github/workflows/verify-patches.yml index db8a4cb395..9c84cf896e 100644 --- a/.github/workflows/verify-patches.yml +++ b/.github/workflows/verify-patches.yml @@ -84,6 +84,13 @@ 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: Check mobile toolchain actions run: | python3 -m pip install setuptools packaging diff --git a/README.md b/README.md index 07dbef96d0..956c246138 100644 --- a/README.md +++ b/README.md @@ -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 +full ICU 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: @@ -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`** retains full ICU 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) diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 1c403b4aa9..5f562de307 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -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 @@ -168,45 +167,34 @@ 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`). Both bundle **full ICU**. 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. +Full ICU archives are linked in both iOS flavors. + +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 now assert full ICU, 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) diff --git a/docs/DANMU-LITE.md b/docs/DANMU-LITE.md new file mode 100644 index 0000000000..3748d11cbc --- /dev/null +++ b/docs/DANMU-LITE.md @@ -0,0 +1,44 @@ +# danmu_api runtime contract + +Current recipes build full ICU into full and lite on Android and iOS. This is +required by the core's `TextDecoder('gbk')` source requests, +`localeCompare(..., 'zh-CN')` resource sorting and `normalize('NFKC')` title keys. +Default small-ICU data does not provide the required legacy decoders or Chinese +collation; an ICU data-file override cannot fix a runtime built without ICU. + +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. The core and Android/Flutter hosts use +these capabilities. WASI/HTTP2 are not separately trimmed in this change. + +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. + +## 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 +selection, and runs the shipped ICU regression assertions on host Node. +It checks configuration and archive consistency, not a cross-compilation. + +The Android and iOS boot smokes run the same ICU assertions on freshly built +artifacts and verify the expected optional feature set. Node 26 full still +exercises native FFI calls/callbacks and Temporal date/timezone behavior; +lite asserts those features are absent. Existing fetch and curated device +tests continue to cover networking and the WASM implementation. + +Previously published archives remain unchanged. Compile, run device tests and +measure new artifacts before releasing or updating either App's pinned URL and +checksum. Future core/dependency updates must be checked against this contract. diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index e4f2fcb123..9bbfdab60a 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -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 bundle full ICU, 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 diff --git a/docs/FAQ.md b/docs/FAQ.md index 7665d01896..ce3d28eccd 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -27,13 +27,12 @@ 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)). +Both current flavors bundle full 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). diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 6a817eef8c..d35d0908b1 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -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 retains full ICU. 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. diff --git a/docs/VERSION-LINES.md b/docs/VERSION-LINES.md index a39fdc5f9b..f2cf54f2f7 100644 --- a/docs/VERSION-LINES.md +++ b/docs/VERSION-LINES.md @@ -52,8 +52,9 @@ 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 `--with-intl=full-icu` 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; diff --git a/expected-tree.txt b/expected-tree.txt index 6b0b73b9aa..ea155970f3 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -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 +f31c26a65e8e11d527ba0f94cb5b151b51ecbe03 diff --git a/mobile-src/tools/ios_framework_prepare.sh b/mobile-src/tools/ios_framework_prepare.sh index f0824ed595..fc72082dca 100755 --- a/mobile-src/tools/ios_framework_prepare.sh +++ b/mobile-src/tools/ios_framework_prepare.sh @@ -17,7 +17,7 @@ NODELIB_PROJECT_PATH='tools/ios-framework' XCODE_PROJECT_PATH='tools/ios-framework/NodeMobile.xcodeproj/project.pbxproj' # Flavor switch (mobile-only; see "The lite variant" in docs/BUILDING.md on the -# recipe branch). "full" (default) is unchanged; "lite" is the size-reduced +# recipe branch). "full" (default) includes all features; "lite" is the size-reduced # build. Read here and threaded into both configure blocks and the static-lib # link list. FLAVOR="${NODEJS_MOBILE_FLAVOR:-full}" @@ -44,10 +44,9 @@ V8_LITE_MODE="--v8-lite-mode" V8_NO_TURBOFAN_GYP_DEFINES="v8_enable_turbofan=0" V8_DISABLE_MAGLEV="--v8-disable-maglev" # Temporal needs bundled ICU and Rust; require it in full builds. -# FFI is present in both flavors. lite retains its no-ICU size tradeoff. +# Lite retains full ICU but omits Temporal and FFI. LITE_FLAGS="--v8-enable-temporal-support" if [ "$FLAVOR" = "lite" ]; then - INTL="none" # lite additionally drops features size-constrained consumers don't need. # Deliberately no --experimental-enable-pointer-compression, unlike Android # lite: V8 reserves the 4GB cage as one mmap of 4GB + (4GB - page) which it @@ -56,7 +55,7 @@ if [ "$FLAVOR" = "lite" ]; then # most 7.375GB of address space, so that reservation can never succeed and V8 # aborts the process during Isolate init. See "Pointer compression" in # docs/BUILDING.md on the recipe branch. - LITE_FLAGS="--v8-disable-temporal-support --without-amaro --without-inspector --without-sqlite" + LITE_FLAGS="--v8-disable-temporal-support --without-ffi --without-amaro --without-inspector --without-sqlite --disable-single-executable-application --v8-disable-object-print" fi declare -a outputs_common=( @@ -73,7 +72,6 @@ declare -a outputs_common=( "libnghttp2.a" "libnode.a" "libnode_base.a" - "libffi.a" "libopenssl.a" "libsimdjson.a" "libsimdutf.a" @@ -88,6 +86,10 @@ declare -a outputs_common=( "libhighway.a" "libzlib.a" "libzstd.a" + # Full ICU is linked in both flavors. + "libicudata.a" + "libicui18n.a" + "libicuucx.a" ) # Static libs present only in the full flavor. lite's configure flags mean these # are never built, so for lite they are neither copied nor linked (their @@ -95,7 +97,9 @@ declare -a outputs_common=( # the existing grep -vF idiom): # libcrdtp -- --without-inspector # libsqlite -- --without-sqlite -# libicu* -- --with-intl=none (no ICU) +# libffi -- --without-ffi +# libnode_crates -- --v8-disable-temporal-support +# Full ICU is in outputs_common; only optional feature archives are omitted. # Full ICU embeds its data in libicudata; libicustubdata is small-ICU only. # NB: libv8_snapshot stays in outputs_common — it is the runtime isolate-setup # lib (setup-isolate-deserialize) linked by BOTH flavors. libv8_init @@ -109,11 +113,9 @@ declare -a outputs_common=( # libgtest/libgtest_main are test-only. declare -a outputs_full_only=( "libnode_crates.a" + "libffi.a" "libcrdtp.a" "libsqlite.a" - "libicudata.a" - "libicui18n.a" - "libicuucx.a" ) declare -a outputs_x64_only=() declare -a outputs_arm64_only=( diff --git a/mobile-src/tools/mobile-test/smoke/mobile-features.js b/mobile-src/tools/mobile-test/smoke/mobile-features.js index 3c439d5bbf..0ea3b19c47 100644 --- a/mobile-src/tools/mobile-test/smoke/mobile-features.js +++ b/mobile-src/tools/mobile-test/smoke/mobile-features.js @@ -1,46 +1,80 @@ 'use strict'; -// Exercise the compiled feature set on the mobile runtime, including the -// native callback path (a successful require alone cannot test libffi). +// Keep danmu_api's decoding, normalization and Chinese sorting available in +// both flavors. The boot harness supplies the expected artifact flavor. const assert = require('node:assert/strict'); if (process.platform === 'android' || process.platform === 'ios') { assert.ok(process.versions.mobile, 'mobile version metadata must be present'); assert.equal(process.versions.mobile.split('-')[0], process.versions.node); } +const flavor = globalThis.NODEJS_MOBILE_EXPECT_FLAVOR || + process.env.NODEJS_MOBILE_EXPECT_FLAVOR || + (process.config.variables.node_use_sqlite ? 'full' : 'lite'); +assert.ok(['full', 'lite'].includes(flavor), 'invalid expected build flavor'); +const lite = flavor === 'lite'; +assert.equal(Boolean(process.config.variables.node_use_sqlite), !lite); +assert.equal(Boolean(process.config.variables.node_use_amaro), !lite); +assert.equal(Boolean(process.config.variables.v8_enable_inspector), !lite); +if (lite) { + assert.throws(() => require('node:sqlite'), { code: 'ERR_UNKNOWN_BUILTIN_MODULE' }); + assert.throws(() => require('node:inspector'), { code: 'ERR_INSPECTOR_NOT_AVAILABLE' }); +} + +console.log('NODEJS_MOBILE_FEATURE_STAGE icu'); +assert.ok(process.versions.icu, 'ICU must be compiled into both flavors'); +assert.equal(process.config.variables.icu_small, false, 'full ICU data is required'); +assert.equal('12A'.normalize('NFKC'), '12A'); +assert.ok(new Intl.Collator('zh-CN').resolvedOptions().locale.startsWith('zh')); +assert.deepEqual(['张', '阿', '李'].sort((a, b) => a.localeCompare(b, 'zh-CN')), + ['阿', '李', '张']); +for (const [encoding, bytes] of [ + ['gbk', [0xd6, 0xd0, 0xce, 0xc4]], + ['gb2312', [0xd6, 0xd0, 0xce, 0xc4]], + ['big5', [0xa4, 0xa4, 0xa4, 0xe5]], +]) { + assert.equal(new TextDecoder(encoding, { fatal: true }).decode(Uint8Array.from(bytes)), '中文'); +} -console.log('NODEJS_MOBILE_FEATURE_STAGE ffi-load'); -const ffi = require('node:ffi'); -const sizeType = ['arm', 'ia32'].includes(process.arch) ? 'uint32' : 'uint64'; -const sizeValue = (value) => sizeType === 'uint64' ? BigInt(value) : value; -const { lib, functions } = ffi.dlopen(null, { - abs: { arguments: ['int32'], return: 'int32' }, - qsort: { arguments: ['pointer', sizeType, sizeType, 'function'], return: 'void' }, -}); -console.log('NODEJS_MOBILE_FEATURE_STAGE ffi-call'); -assert.equal(functions.abs(-42), 42); -const numbers = Buffer.alloc(12); -[3, 1, 2].forEach((value, index) => numbers.writeInt32LE(value, index * 4)); -let callbackCalls = 0; -console.log('NODEJS_MOBILE_FEATURE_STAGE ffi-callback'); -const callback = lib.registerCallback( - { arguments: ['pointer', 'pointer'], return: 'int32' }, - (left, right) => { - callbackCalls += 1; - return ffi.toBuffer(left, 4, true).readInt32LE() - - ffi.toBuffer(right, 4, true).readInt32LE(); - }, -); -try { - functions.qsort(ffi.getRawPointer(numbers), sizeValue(3), sizeValue(4), callback); - assert.deepEqual([0, 4, 8].map((offset) => numbers.readInt32LE(offset)), [1, 2, 3]); - assert.ok(callbackCalls > 0, 'native libffi callback did not run'); -} finally { - lib.unregisterCallback(callback); - lib.close(); +const ffiEnabled = Boolean(process.config.variables.node_use_ffi); +assert.equal(ffiEnabled, !lite, 'FFI must be enabled only in full builds'); +if (!ffiEnabled) { + assert.throws(() => require('node:ffi'), { code: 'ERR_UNKNOWN_BUILTIN_MODULE' }); +} else { + console.log('NODEJS_MOBILE_FEATURE_STAGE ffi-load'); + const ffi = require('node:ffi'); + const sizeType = ['arm', 'ia32'].includes(process.arch) ? 'uint32' : 'uint64'; + const sizeValue = (value) => sizeType === 'uint64' ? BigInt(value) : value; + const { lib, functions } = ffi.dlopen(null, { + abs: { arguments: ['int32'], return: 'int32' }, + qsort: { arguments: ['pointer', sizeType, sizeType, 'function'], return: 'void' }, + }); + console.log('NODEJS_MOBILE_FEATURE_STAGE ffi-call'); + assert.equal(functions.abs(-42), 42); + const numbers = Buffer.alloc(12); + [3, 1, 2].forEach((value, index) => numbers.writeInt32LE(value, index * 4)); + let callbackCalls = 0; + console.log('NODEJS_MOBILE_FEATURE_STAGE ffi-callback'); + const callback = lib.registerCallback( + { arguments: ['pointer', 'pointer'], return: 'int32' }, + (left, right) => { + callbackCalls += 1; + return ffi.toBuffer(left, 4, true).readInt32LE() - + ffi.toBuffer(right, 4, true).readInt32LE(); + }, + ); + try { + functions.qsort(ffi.getRawPointer(numbers), sizeValue(3), sizeValue(4), callback); + assert.deepEqual([0, 4, 8].map((offset) => numbers.readInt32LE(offset)), [1, 2, 3]); + assert.ok(callbackCalls > 0, 'native libffi callback did not run'); + } finally { + lib.unregisterCallback(callback); + lib.close(); + } } console.log('NODEJS_MOBILE_FEATURE_STAGE temporal'); const temporalEnabled = !!process.config.variables.v8_enable_temporal_support; +assert.equal(temporalEnabled, !lite, 'Temporal must be enabled only in full builds'); assert.equal(typeof globalThis.Temporal, temporalEnabled ? 'object' : 'undefined'); if (temporalEnabled) { assert.equal(Temporal.PlainDate.from('2024-02-28').add({ days: 1 }).toString(), '2024-02-29'); @@ -48,4 +82,4 @@ if (temporalEnabled) { const spring = Temporal.ZonedDateTime.from('2026-03-08T01:30-05:00[America/New_York]'); assert.equal(spring.add({ hours: 1 }).hour, 3, 'Temporal timezone data/DST is unavailable'); } -console.log(`NODEJS_MOBILE_FEATURES_OK ffi=1 temporal=${Number(temporalEnabled)}`); +console.log(`NODEJS_MOBILE_FEATURES_OK flavor=${flavor} icu=full ffi=${Number(ffiEnabled)} temporal=${Number(temporalEnabled)}`); diff --git a/mobile-src/tools/mobile-test/smoke/run-android-smoke.sh b/mobile-src/tools/mobile-test/smoke/run-android-smoke.sh index 5c12e74b8e..aea72b9cdf 100755 --- a/mobile-src/tools/mobile-test/smoke/run-android-smoke.sh +++ b/mobile-src/tools/mobile-test/smoke/run-android-smoke.sh @@ -21,7 +21,9 @@ adbx push "$SCRIPT_DIR/smoke.js" "$DEST/" >/dev/null adbx push "$SCRIPT_DIR/mobile-features.js" "$DEST/" >/dev/null adbx shell "chmod 755 $DEST/node_smoke" -OUT=$(adbx shell "cd $DEST && LD_LIBRARY_PATH=$DEST TMPDIR=$DEST ./node_smoke smoke.js; echo SMOKE_EXIT:\$?") +FLAVOR="${NODEJS_MOBILE_FLAVOR:-full}" +case "$FLAVOR" in full|lite) ;; *) echo "Invalid build flavor: $FLAVOR" >&2; exit 1 ;; esac +OUT=$(adbx shell "cd $DEST && NODEJS_MOBILE_EXPECT_FLAVOR=$FLAVOR LD_LIBRARY_PATH=$DEST TMPDIR=$DEST ./node_smoke smoke.js; echo SMOKE_EXIT:\$?") echo "----- device output -----" echo "$OUT" echo "--------------------------" diff --git a/mobile-src/tools/mobile-test/smoke/run-ios-smoke.sh b/mobile-src/tools/mobile-test/smoke/run-ios-smoke.sh index dbbc70f59f..e39e57a219 100755 --- a/mobile-src/tools/mobile-test/smoke/run-ios-smoke.sh +++ b/mobile-src/tools/mobile-test/smoke/run-ios-smoke.sh @@ -28,7 +28,10 @@ POLL_SECONDS="${SMOKE_POLL_SECONDS:-180}" # PASS write is skipped and only the atexit FAIL fallback fires. Letting the # event loop drain returns 0 from node_start -> a clean PASS verdict. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -EXPR="$(cat "$SCRIPT_DIR/mobile-features.js") +FLAVOR="${NODEJS_MOBILE_FLAVOR:-full}" +case "$FLAVOR" in full|lite) ;; *) echo "Invalid build flavor: $FLAVOR" >&2; exit 1 ;; esac +EXPR="globalThis.NODEJS_MOBILE_EXPECT_FLAVOR = '${FLAVOR}'; +$(cat "$SCRIPT_DIR/mobile-features.js") console.log('${MARKER} ' + process.version + ' ' + process.platform + ' ' + process.arch);" # Launch once; echo the app output to stderr (for log visibility) and print the diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index fcc0bd1d52..d2cfcc098f 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -26,12 +26,12 @@ Co-authored-by: Vincent Breitmoser Signed-off-by: fogtape Assisted-by: Codex --- - android_configure.py | 76 +++++++++++++++++++++++++++++++++++--------- + android_configure.py | 75 +++++++++++++++++++++++++++++++++++--------- configure.py | 8 +++++ - 2 files changed, 69 insertions(+), 15 deletions(-) + 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/android_configure.py b/android_configure.py -index 5cea0393f4..71daaf7250 100644 +index 5cea0393f4..5593019b06 100644 --- a/android_configure.py +++ b/android_configure.py @@ -1,25 +1,14 @@ @@ -71,7 +71,7 @@ index 5cea0393f4..71daaf7250 100644 toolchain_path = android_ndk_path + "/toolchains/llvm/prebuilt/darwin-x86_64" elif platform.system() == "Linux": -@@ -65,13 +54,70 @@ elif platform.system() == "Linux": +@@ -65,13 +54,69 @@ elif platform.system() == "Linux": os.environ['PATH'] += os.pathsep + toolchain_path + "/bin" os.environ['CC'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang" os.environ['CXX'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang++" @@ -115,14 +115,13 @@ index 5cea0393f4..71daaf7250 100644 + sys.exit(1) +print("\033[92mInfo: \033[0m" + "Build flavor: " + flavor) + -+# Temporal named time zones require the complete ICU data bundle. ++# Both flavors need complete ICU for decoding and Chinese collation. +intl = "full-icu" +# Full builds require Temporal explicitly so a missing Rust toolchain fails -+# instead of silently shipping a different feature set. FFI is enabled in both. ++# instead of silently shipping a different feature set. Lite drops FFI. +extra_flags = " --v8-enable-temporal-support" +if flavor == "lite": -+ intl = "none" -+ extra_flags = " --v8-disable-temporal-support --without-amaro --without-inspector --without-sqlite" ++ extra_flags = " --v8-disable-temporal-support --without-ffi --without-amaro --without-inspector --without-sqlite --disable-single-executable-application --v8-disable-object-print" + # Pointer compression: 32-bit compressed pointers inside a 4GB cage — the + # size lever on the JS heap rather than on the binary. lite only, because it + # changes the V8 ABI for addons that include V8's headers directly (N-API diff --git a/scripts/package-preview.py b/scripts/package-preview.py index 624322bf53..57899c6ea0 100644 --- a/scripts/package-preview.py +++ b/scripts/package-preview.py @@ -43,8 +43,11 @@ def main(): flavor=flavor, recipe_commit=sha, source_tree=tree, branch=os.environ.get('GITHUB_REF_NAME', ''), run_url=f"https://github.com/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}", - preview=True, enabled_features=['node:ffi'] + (['Temporal'] if flavor == 'full' else []), - disabled_features=[] if flavor == 'full' else ['Temporal']) + preview=True, enabled_features=['full-icu'] + (['node:ffi', 'Temporal'] if flavor == 'full' else []), + disabled_features=[] if flavor == 'full' else [ + 'node:ffi', 'Temporal', 'node:sqlite', 'Web Storage', + 'Inspector', 'TypeScript type-stripping', 'SEA', + 'V8 native debugger object print']) (artifact / 'BUILD-INFO.json').write_text(json.dumps(info, indent=2) + '\n') package = output / f'{name}-{version}-preview.zip' with zipfile.ZipFile(package, 'w', zipfile.ZIP_DEFLATED) as archive: diff --git a/scripts/test-mobile-flavors.py b/scripts/test-mobile-flavors.py new file mode 100755 index 0000000000..f64ff8054d --- /dev/null +++ b/scripts/test-mobile-flavors.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Check flavor flags, iOS archive selection and danmu_api ICU regressions.""" +import hashlib +import json +import os +from pathlib import Path +import shlex +import subprocess +import sys +import tempfile +import unittest +import zipfile + +ROOT = Path(__file__).resolve().parent.parent +SOURCE = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else ROOT / 'out' +if len(sys.argv) > 1: + del sys.argv[1] +MAJOR = int(next(line.strip().lstrip('v').split('.')[0] + for line in (ROOT / 'upstream-base.txt').read_text().splitlines() + if line.strip() and not line.startswith('#'))) +# android_configure supports Linux/macOS build hosts; simulate Linux so this +# configure-only test also runs on Termux's Python (which reports Android). +ANDROID_WRAPPER = [sys.executable, '-c', + "import platform,runpy,sys; platform.system=lambda:'Linux'; " + "sys.argv=sys.argv[1:]; runpy.run_path(sys.argv[0],run_name='__main__')", + str(SOURCE / 'android_configure.py')] + +CUTS = {'--without-amaro', '--without-inspector', '--without-sqlite', + '--disable-single-executable-application', '--v8-disable-object-print'} +if MAJOR >= 26: + CUTS |= {'--without-ffi', '--v8-disable-temporal-support'} + + +class MobileFlavors(unittest.TestCase): + def test_android_flags_and_pointer_compression(self): + with tempfile.TemporaryDirectory() as temporary: + folder = Path(temporary) + ndk = folder / 'ndk' + ndk.mkdir() + (ndk / 'marker').touch() + capture = folder / 'configured.json' + configure = folder / 'configure' + configure.write_text(f'#!{sys.executable}\nimport json,sys\n' + f'open({str(capture)!r}, "w").write(json.dumps(sys.argv[1:]))\n') + configure.chmod(0o755) + for flavor in ('full', 'lite'): + for arch in ('arm', 'arm64', 'x86_64'): + with self.subTest(flavor=flavor, arch=arch): + env = dict(os.environ, NODEJS_MOBILE_FLAVOR=flavor) + env.pop('NODEJS_MOBILE_SCCACHE', None) + subprocess.run(ANDROID_WRAPPER + [str(ndk), '24', arch], cwd=folder, env=env, + check=True, capture_output=True, text=True) + flags = set(json.loads(capture.read_text())) + self.assertIn('--with-intl=full-icu', flags) + self.assertIn('--shared', flags) + self.assertIn('--cross-compiling', flags) + if flavor == 'lite': + self.assertTrue(CUTS <= flags, CUTS - flags) + else: + self.assertFalse(CUTS & flags) + if MAJOR >= 26: + self.assertIn('--v8-enable-temporal-support', flags) + self.assertEqual('--experimental-enable-pointer-compression' in flags, + flavor == 'lite' and arch != 'arm') + env['NODEJS_MOBILE_FLAVOR'] = 'invalid' + result = subprocess.run(ANDROID_WRAPPER + [str(ndk), '24', 'arm64'], cwd=folder, env=env, + capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + + def test_ios_flags_and_framework_archives(self): + script = (SOURCE / 'tools/ios_framework_prepare.sh').read_text() + prefix = script.split('build_for_arm64_device() {', 1)[0] + probe = prefix + ''' +printf '\nINTL:%s\nFLAGS:%s\n' "$INTL" "$LITE_FLAGS" +for archive in "${outputs_arm64[@]}"; do printf 'LIB:%s\n' "$archive"; done +''' + project = (SOURCE / 'tools/ios-framework/NodeMobile.xcodeproj/project.pbxproj').read_text() + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / 'ios-flavor-probe.sh' + path.write_text(probe) + for flavor in ('full', 'lite'): + with self.subTest(flavor=flavor): + result = subprocess.run(['bash', str(path)], + env=dict(os.environ, NODEJS_MOBILE_FLAVOR=flavor), + capture_output=True, text=True, check=True) + lines = result.stdout.splitlines() + self.assertIn('INTL:full-icu', lines) + flags = set(shlex.split(next(line[6:] for line in lines + if line.startswith('FLAGS:')))) + archives = {line[4:] for line in lines if line.startswith('LIB:')} + self.assertTrue({'libicudata.a', 'libicui18n.a', 'libicuucx.a'} <= archives) + self.assertNotIn('libicustubdata.a', archives) + self.assertNotIn('libicustubdata.a', project) + for archive in archives: + self.assertIn(archive, project, f'{archive} has no Xcode link entry') + for archive in ('libcrdtp.a', 'libsqlite.a'): + self.assertEqual(archive in archives, flavor == 'full') + if flavor == 'lite': + self.assertTrue(CUTS <= flags, CUTS - flags) + else: + self.assertFalse(CUTS & flags) + if MAJOR >= 26: + for archive in ('libffi.a', 'libnode_crates.a'): + self.assertEqual(archive in archives, flavor == 'full') + + def test_preview_metadata_matches_flavors(self): + with tempfile.TemporaryDirectory() as temporary: + folder = Path(temporary) + source, output = folder / 'input', folder / 'output' + for platform in ('android', 'ios'): + for flavor in ('full', 'lite'): + suffix = '-lite' if flavor == 'lite' else '' + artifact = source / f'nodejs-mobile-{platform}{suffix}' + required = ['include/node/node_version.h'] + if platform == 'android': + required += [f'bin/{abi}/libnode.so' + for abi in ('arm64-v8a', 'armeabi-v7a', 'x86_64')] + else: + required += ['NodeMobile.xcframework/Info.plist'] + for name in required: + path = artifact / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b'fixture') + subprocess.run([sys.executable, str(ROOT / 'scripts/package-preview.py'), + str(source), str(output)], cwd=ROOT, + env=dict(os.environ, GITHUB_REPOSITORY='fogtape/nodejs-mobile', + GITHUB_RUN_ID='1'), check=True, + capture_output=True, text=True) + for package in output.glob('*.zip'): + with zipfile.ZipFile(package) as archive: + info = json.loads(archive.read('BUILD-INFO.json')) + self.assertIn('full-icu', info['enabled_features']) + lite = info['flavor'] == 'lite' + for feature in ('node:ffi', 'Temporal'): + self.assertEqual(feature in info['enabled_features'], not lite) + self.assertEqual(feature in info['disabled_features'], lite) + lines = (output / 'SHA256SUMS').read_text().splitlines() + self.assertEqual(len(lines), 4) + for line in lines: + digest, name = line.split() + self.assertEqual(hashlib.sha256((output / name).read_bytes()).hexdigest(), digest) + + def test_required_icu_smoke_on_host(self): + # Exercise the exact shipped smoke with a host Node that has full ICU. + # FFI/Temporal are specific to the Node 26 host build and are tested by CI. + source = (SOURCE / 'tools/mobile-test/smoke/mobile-features.js').read_text() + icu = source.split("console.log('NODEJS_MOBILE_FEATURE_STAGE icu');", 1)[1] + if MAJOR >= 26: + icu = icu.split('const ffiEnabled =', 1)[0] + else: + icu = icu.split('console.log(`NODEJS_MOBILE_FEATURES_OK', 1)[0] + subprocess.run(['node', '-e', "const assert=require('node:assert/strict');\n" + icu], + check=True, capture_output=True, text=True) + + +if __name__ == '__main__': + unittest.main() From 7b99525ecaea653726f5d5da082bd727e8783349 Mon Sep 17 00:00:00 2001 From: fogtape Date: Fri, 2 Oct 2026 21:55:04 +0800 Subject: [PATCH 2/8] build: profile lite ICU data and strip Android runtime symbols --- .github/workflows/build.yml | 7 + .github/workflows/verify-patches.yml | 3 + README.md | 4 +- docs/BUILDING.md | 10 +- docs/DANMU-LITE.md | 83 +++- docs/EMBEDDING.md | 2 +- docs/FAQ.md | 6 +- docs/RELEASING.md | 2 +- docs/VERSION-LINES.md | 3 +- expected-tree.txt | 2 +- mobile-src/tools/android_build.sh | 5 + mobile-src/tools/ios_framework_prepare.sh | 6 +- .../mobile-test/smoke/mobile-features.js | 93 +++- mobile-src/tools/strip-android-runtime.py | 58 +++ ...S-dest-os-and-toolchain-configure.py.patch | 9 +- ...ICU-data-for-danmu_api-lite-runtimes.patch | 465 ++++++++++++++++++ patches/files.map | 4 + patches/series | 1 + scripts/package-preview.py | 5 +- scripts/test-mobile-flavors.py | 45 +- scripts/test-runtime-stripping.py | 57 +++ 21 files changed, 826 insertions(+), 44 deletions(-) create mode 100755 mobile-src/tools/strip-android-runtime.py create mode 100644 patches/build-profile-ICU-data-for-danmu_api-lite-runtimes.patch create mode 100644 scripts/test-runtime-stripping.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb69d9b77b..f1a8c7494b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/verify-patches.yml b/.github/workflows/verify-patches.yml index 9c84cf896e..e8b596139d 100644 --- a/.github/workflows/verify-patches.yml +++ b/.github/workflows/verify-patches.yml @@ -91,6 +91,9 @@ jobs: - name: Check danmu_api flavor configuration and ICU contract run: python3 scripts/test-mobile-flavors.py out + - 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 diff --git a/README.md b/README.md index 956c246138..ea96237e63 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ 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`. New lite builds retain -full ICU and disable both Temporal and `node:ffi`. See +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: @@ -32,7 +32,7 @@ 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. **`lite`** retains full ICU for +**`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`. diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 5f562de307..5b3b585d55 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -167,7 +167,7 @@ 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`). Both bundle **full ICU**. The lite flavor is tailored to `danmu_api`: +`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. @@ -185,13 +185,17 @@ 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. -Full ICU archives are linked in both iOS flavors. +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 now assert full ICU, GBK/GB2312/Big5 decoding, Chinese +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. diff --git a/docs/DANMU-LITE.md b/docs/DANMU-LITE.md index 3748d11cbc..49b409025d 100644 --- a/docs/DANMU-LITE.md +++ b/docs/DANMU-LITE.md @@ -1,10 +1,38 @@ # danmu_api runtime contract -Current recipes build full ICU into full and lite on Android and iOS. This is -required by the core's `TextDecoder('gbk')` source requests, -`localeCompare(..., 'zh-CN')` resource sorting and `normalize('NFKC')` title keys. -Default small-ICU data does not provide the required legacy decoders or Chinese -collation; an ICU data-file override cannot fix a runtime built without ICU. +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 @@ -13,13 +41,28 @@ 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. The core and Android/Flutter hosts use -these capabilities. WASI/HTTP2 are not separately trimmed in this change. +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: @@ -29,16 +72,16 @@ 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 -selection, and runs the shipped ICU regression assertions on host Node. -It checks configuration and archive consistency, not a cross-compilation. - -The Android and iOS boot smokes run the same ICU assertions on freshly built -artifacts and verify the expected optional feature set. Node 26 full still -exercises native FFI calls/callbacks and Temporal date/timezone behavior; -lite asserts those features are absent. Existing fetch and curated device -tests continue to cover networking and the WASM implementation. - -Previously published archives remain unchanged. Compile, run device tests and -measure new artifacts before releasing or updating either App's pinned URL and -checksum. Future core/dependency updates must be checked against this contract. +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. diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index 9bbfdab60a..86e98dcc40 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -121,7 +121,7 @@ Only the options upstream marks `kAllowedInEnvvar` are accepted, and ### Situational -**`NODE_ICU_DATA`** — current full and lite builds bundle full ICU, so no +**`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 diff --git a/docs/FAQ.md b/docs/FAQ.md index ce3d28eccd..cd21c0c1ba 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -27,7 +27,11 @@ 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. -Both current flavors bundle full ICU, including `Intl`, Unicode +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. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index d35d0908b1..8c0b861a00 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -81,7 +81,7 @@ The four archives are arm64 device and simulator slices. Verify the checksum after downloading. Node 26 full builds enable Temporal and `node:ffi`; the new lite recipe -omits both and retains full ICU. Already published 26.10.0-0 lite assets +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. diff --git a/docs/VERSION-LINES.md b/docs/VERSION-LINES.md index f2cf54f2f7..4dbe019875 100644 --- a/docs/VERSION-LINES.md +++ b/docs/VERSION-LINES.md @@ -52,7 +52,8 @@ 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`. -New lite builds retain `--with-intl=full-icu` and explicitly disable +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 diff --git a/expected-tree.txt b/expected-tree.txt index ea155970f3..21b09459d8 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -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. -f31c26a65e8e11d527ba0f94cb5b151b51ecbe03 +cf1e359f446d47683db958b3f70a66ad52ecbb61 diff --git a/mobile-src/tools/android_build.sh b/mobile-src/tools/android_build.sh index 179846acd9..6efc2ffcd0 100755 --- a/mobile-src/tools/android_build.sh +++ b/mobile-src/tools/android_build.sh @@ -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 diff --git a/mobile-src/tools/ios_framework_prepare.sh b/mobile-src/tools/ios_framework_prepare.sh index fc72082dca..998573d0ec 100755 --- a/mobile-src/tools/ios_framework_prepare.sh +++ b/mobile-src/tools/ios_framework_prepare.sh @@ -21,6 +21,7 @@ XCODE_PROJECT_PATH='tools/ios-framework/NodeMobile.xcodeproj/project.pbxproj' # build. Read here and threaded into both configure blocks and the static-lib # link list. FLAVOR="${NODEJS_MOBILE_FLAVOR:-full}" +ICU_PROFILE_GYP_DEFINES="" if [ "$FLAVOR" != "full" ] && [ "$FLAVOR" != "lite" ]; then echo "Error: NODEJS_MOBILE_FLAVOR must be 'full' or 'lite'"; exit 1 fi @@ -47,6 +48,7 @@ V8_DISABLE_MAGLEV="--v8-disable-maglev" # Lite retains full ICU but omits Temporal and FFI. LITE_FLAGS="--v8-enable-temporal-support" if [ "$FLAVOR" = "lite" ]; then + ICU_PROFILE_GYP_DEFINES="node_mobile_icu_profile=danmu-lite.json" # lite additionally drops features size-constrained consumers don't need. # Deliberately no --experimental-enable-pointer-compression, unlike Android # lite: V8 reserves the 4GB cage as one mmap of 4GB + (4GB - page) which it @@ -130,7 +132,7 @@ fi build_for_arm64_device() { make clean - GYP_DEFINES="target_arch=arm64 host_os=mac target_os=ios $V8_NO_TURBOFAN_GYP_DEFINES" + GYP_DEFINES="target_arch=arm64 host_os=mac target_os=ios $V8_NO_TURBOFAN_GYP_DEFINES $ICU_PROFILE_GYP_DEFINES" export GYP_DEFINES ./configure \ --dest-os=ios \ @@ -168,7 +170,7 @@ build_for_arm64_device() { build_for_arm64_simulator() { make clean - GYP_DEFINES="target_arch=arm64 host_os=mac target_os=ios $V8_NO_TURBOFAN_GYP_DEFINES" + GYP_DEFINES="target_arch=arm64 host_os=mac target_os=ios $V8_NO_TURBOFAN_GYP_DEFINES $ICU_PROFILE_GYP_DEFINES" export GYP_DEFINES ./configure \ --dest-os=ios \ diff --git a/mobile-src/tools/mobile-test/smoke/mobile-features.js b/mobile-src/tools/mobile-test/smoke/mobile-features.js index 0ea3b19c47..5aa5da27b6 100644 --- a/mobile-src/tools/mobile-test/smoke/mobile-features.js +++ b/mobile-src/tools/mobile-test/smoke/mobile-features.js @@ -22,7 +22,7 @@ if (lite) { console.log('NODEJS_MOBILE_FEATURE_STAGE icu'); assert.ok(process.versions.icu, 'ICU must be compiled into both flavors'); -assert.equal(process.config.variables.icu_small, false, 'full ICU data is required'); +assert.equal(process.config.variables.icu_small, false, 'ICU must retain the complete API mode'); assert.equal('12A'.normalize('NFKC'), '12A'); assert.ok(new Intl.Collator('zh-CN').resolvedOptions().locale.startsWith('zh')); assert.deepEqual(['张', '阿', '李'].sort((a, b) => a.localeCompare(b, 'zh-CN')), @@ -35,6 +35,95 @@ for (const [encoding, bytes] of [ assert.equal(new TextDecoder(encoding, { fatal: true }).decode(Uint8Array.from(bytes)), '中文'); } +// Every Node TextDecoder web encoding stays available, including encodings +// not requested by today's sources. Check streaming against a one-shot decode. +const encodingHashes = { + "ibm866": "3c8cc5cb485f93d2bb20ea06c4d6808fcae1d924105a0ec4ee2b280457c14e14", + "windows-1252": "cc916e51644a12e8de4ad160910c171a58621ee5dc3a6da6f8b00f8684085f33", + "iso-8859-6": "beba4e6cf97dce8317ea76b14b77dbe4d2b3d8920b6b0a3fa9235ab532629f82", + "big5": "7a7e1f2dbbff786abf55250e204d6db2433cd6ee699cc27374b1d6ecd28865ea", + "gbk": "9cc45cb5d63e234da6349daddb9b38cc6d3a677a6c1e0bc2877d10bbf673c0b2", + "windows-1250": "03772ed2e875bd125544fe7f243ea9a1dd163a057030970b26d8b6dd4c79a6e5", + "windows-1251": "b16600cf4e6d1a2d4659b6a2cc96caa5ddc3e103ecfb07c5154d05fd54b174b3", + "windows-1253": "e4570135cbc6e3d53eae99c2be1af17c86f4a744bd55757470d2143ece00da0b", + "windows-1254": "4a8e99647c3e28e6a5234ac8b124e5614a3f99dc68ec948fb67da163e210e4f3", + "windows-1255": "870c5c5e687fabcddc1209bc1263f6d6e9d6f594baed8ab280dcdeeb5607207d", + "windows-1256": "6f6e8626197b1b6b280a079d1d842daa09600a39fdb3d1e99596e943c61cc98b", + "windows-1257": "d19a4e888879e36a450470073fc0344cffdfffa40ad82fb433de9f9b40b5c048", + "windows-1258": "e79b48db126bc71dfcf1723e9f6350af101d1eb494e29d736ecf9530113cd361", + "euc-kr": "f82d4597ae2fd7b4cc85f13509cacd40a11d0355de01a146fe6beb3d3ed8d060", + "euc-jp": "9d9e4b1cf9bad807e677e7af6b49f34b58e8767669fce9f2d85fc57c3fc32c39", + "iso-2022-jp": "0a53580eb9bb28a3585042beac7a16697ba21b4abf179771423705eacba3ad4d", + "iso-8859-8": "b43535e7aaeb7bcf8bd8465326ef9ace96e351494306f963fa24cf312e5aaf18", + "iso-8859-8-i": "b43535e7aaeb7bcf8bd8465326ef9ace96e351494306f963fa24cf312e5aaf18", + "iso-8859-2": "a5871b0f978b840b9fad23483563caf9edf42c1828bff529f7594779ebaf5210", + "iso-8859-3": "e83895f2b7d7b82b9356298e197f7ddef190d53209cdf3b46e9eca4d4a582847", + "iso-8859-4": "449076e20ebf45ebbf44f24e39e98684dd2a6e07467ba3b8ba4192eb9405e2e3", + "iso-8859-10": "282514fbd01219c48fc84a8e45654368f161e1c5ab33fc028748688b9acb217f", + "iso-8859-15": "9b58b26dbd8fbff2917ab21d989323703946ba491a1eb15cdb2af7ecf9581e97", + "iso-8859-5": "9f31ddc0f7444afa24ddc2241f303bcd712296d7f2ca1e6bc9f5d1e9163df86f", + "iso-8859-7": "71069977a6798ab799df960847c927edfc3f787ac238f73702d7f37ef8cc1a1c", + "koi8-r": "fb0243455e64ef7026d46b057cfaeb41fef148d7d29a78fde21feda264ac02ee", + "macintosh": "54112bce885d7b1abc9ba5e06e21900b89ea0f7e5da25e393c0bdf72d0ea4a30", + "shift_jis": "63214e041f92daeea69d4ff39caf1cdd9dc39551b2a313e1fa0f9a22163b26f8", + "utf-16le": "c46bd82c3031aaac9e602e1dba920942cefe5427897a162ab7e2a8f5139e2304", + "windows-874": "6a2c7940c3d682164044abd7db7706dfff0307c39092937230f7554ce9846756", + "gb18030": "9cc45cb5d63e234da6349daddb9b38cc6d3a677a6c1e0bc2877d10bbf673c0b2", + "iso-8859-13": "4426f6d2f1b025cdf6d2b46080e2840b0ce85666d424ec909ccab226b34ebcc8", + "iso-8859-14": "f03afb7e01e66cac3cd7ed1a084173244f55b7c2e7fce44969aeade1077d8560", + "iso-8859-16": "2de1faef4dc524c9b94fd90885997e4fe6c2be7c672a1c03a10dcb0edd69487e", + "koi8-u": "896c218aaf12ca1b0489a01d8d2780b0e9de4253e24f0117d5486dfd87acf593", + "utf-8": "0f1a0d9c96b61c6dd842f73714f9e10c01c40383217f0a095c08145ef36b081b", + "utf-16be": "d82384cdf8ccf910d979149596e6eac1305878422cf44cbf028a97d6844a9e88", + "x-mac-cyrillic": "784db55e1c90195e69a4f96d755548fe48a4a6c327d1138cc731af07afec272c", + "x-user-defined": "fb4341fe90799717efc22f5de56d20a92c13e3711e94c9d4433fa4aabaf57c57" +}; +for (const [encoding, expected] of Object.entries(encodingHashes)) { + const bytes = Uint8Array.from({ length: 256 }, (_, index) => index); + const decoded = new TextDecoder(encoding).decode(bytes); + assert.equal(require('node:crypto').createHash('sha256').update(decoded).digest('hex'), + expected, `${encoding}: data differs from complete ICU`); + const streaming = new TextDecoder(encoding); + let chunks = ''; + for (let index = 0; index < bytes.length; index += 7) { + chunks += streaming.decode(bytes.slice(index, index + 7), { stream: true }); + } + chunks += streaming.decode(); + assert.equal(chunks, decoded, `${encoding}: streaming decode differs`); +} +for (const [encoding, bytes, expected] of [ + ['shift_jis', [0x93, 0xfa, 0x96, 0x7b], '日本'], + ['euc-jp', [0xc6, 0xfc, 0xcb, 0xdc], '日本'], + ['iso-2022-jp', [0x1b, 0x24, 0x42, 0x46, 0x7c, 0x4b, 0x5c, 0x1b, 0x28, 0x42], '日本'], + ['euc-kr', [0xc7, 0xd1, 0xb1, 0xdb], '한글'], +]) { + assert.equal(new TextDecoder(encoding, { fatal: true }).decode(Uint8Array.from(bytes)), expected); +} +for (const locale of ['zh-CN', 'zh-TW', 'zh-HK', 'zh-SG', 'zh-MO', + 'en-US', 'en-GB', 'en-AU', 'ja-JP', 'ko-KR']) { + for (const name of ['Collator', 'DateTimeFormat', 'NumberFormat', 'RelativeTimeFormat', + 'PluralRules', 'ListFormat', 'DisplayNames', 'Segmenter']) { + assert.equal(Intl[name].supportedLocalesOf([locale]).length, 1, `${name}: ${locale}`); + } + assert.ok(new Intl.DateTimeFormat(locale, { timeZone: 'Asia/Shanghai' }).format(0)); + assert.ok(new Intl.NumberFormat(locale, { style: 'currency', currency: 'CNY' }).format(1234.5)); + assert.ok(new Intl.NumberFormat(locale, { style: 'unit', unit: 'kilometer-per-hour' }).format(60)); + assert.ok(new Intl.DisplayNames(locale, { type: 'language' }).of('fr')); + assert.ok(new Intl.DisplayNames(locale, { type: 'region' }).of('US')); + assert.ok([...new Intl.Segmenter(locale, { granularity: 'word' }).segment('中文日本어 test')].length); +} +for (const timeZone of ['UTC', 'Asia/Taipei', 'Asia/Hong_Kong', 'Asia/Tokyo', + 'Asia/Seoul', 'America/New_York', 'Europe/London', 'Australia/Sydney']) { + assert.ok(new Intl.DateTimeFormat('en-US', { timeZone }).format(0)); +} +assert.equal(require('node:url').domainToASCII('中文.cn'), 'xn--fiq228c.cn'); +assert.equal(require('node:url').domainToASCII('faß.de'), 'xn--fa-hia.de'); +assert.equal('①ff'.normalize('NFKC'), '1ff'); +if (lite) { + assert.deepEqual(Intl.DateTimeFormat.supportedLocalesOf(['de-DE']), [], + 'lite must embed the selected ICU locale profile, not complete locale data'); +} + const ffiEnabled = Boolean(process.config.variables.node_use_ffi); assert.equal(ffiEnabled, !lite, 'FFI must be enabled only in full builds'); if (!ffiEnabled) { @@ -82,4 +171,4 @@ if (temporalEnabled) { const spring = Temporal.ZonedDateTime.from('2026-03-08T01:30-05:00[America/New_York]'); assert.equal(spring.add({ hours: 1 }).hour, 3, 'Temporal timezone data/DST is unavailable'); } -console.log(`NODEJS_MOBILE_FEATURES_OK flavor=${flavor} icu=full ffi=${Number(ffiEnabled)} temporal=${Number(temporalEnabled)}`); +console.log(`NODEJS_MOBILE_FEATURES_OK flavor=${flavor} icu=${lite ? 'danmu' : 'full'} ffi=${Number(ffiEnabled)} temporal=${Number(temporalEnabled)}`); diff --git a/mobile-src/tools/strip-android-runtime.py b/mobile-src/tools/strip-android-runtime.py new file mode 100755 index 0000000000..76494b169f --- /dev/null +++ b/mobile-src/tools/strip-android-runtime.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Strip copied Android runtimes without changing the dynamic-linking ABI.""" +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + + +def dynamic_symbols(nm, library): + return subprocess.check_output( + [str(nm), '-D', '--format=posix', str(library)], text=True) + + +def strip_library(tools, library): + suffix = '.exe' if os.name == 'nt' else '' + nm, strip = (tools / ('llvm-' + name + suffix) for name in ('nm', 'strip')) + before = dynamic_symbols(nm, library) + if '_ZN4node5StartEiPPc ' not in before: + raise RuntimeError(f'{library}: missing node::Start embedding entry point') + original_size = library.stat().st_size + # Leave the input intact on any tool or symbol verification failure. + with tempfile.TemporaryDirectory(prefix='.strip-', dir=library.parent) as temp: + staged = Path(temp) / library.name + shutil.copy2(library, staged) + subprocess.run([str(strip), '--strip-unneeded', str(staged)], check=True) + if dynamic_symbols(nm, staged) != before: + raise RuntimeError(f'{library}: strip changed dynamic symbols') + if staged.stat().st_size > original_size: + raise RuntimeError(f'{library}: stripped file grew unexpectedly') + os.replace(staged, library) + print(f'{library}: {original_size} -> {library.stat().st_size} bytes; ' + 'dynamic symbols unchanged') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + tools = parser.add_mutually_exclusive_group(required=True) + tools.add_argument('--ndk', type=Path) + tools.add_argument('--tools', type=Path, help='LLVM bin directory (local verification)') + parser.add_argument('path', type=Path, help='libnode.so or out_android directory') + args = parser.parse_args() + if args.ndk: + suffix = '.exe' if os.name == 'nt' else '' + candidates = sorted((args.ndk / 'toolchains/llvm/prebuilt').glob('*/bin/llvm-strip' + suffix)) + if len(candidates) != 1: + parser.error('expected one NDK prebuilt LLVM tool directory') + args.tools = candidates[0].parent + libraries = [args.path] if args.path.is_file() else sorted(args.path.glob('*/libnode.so')) + if not libraries: + parser.error(f'no libnode.so found at {args.path}') + for library in libraries: + strip_library(args.tools, library) + + +if __name__ == '__main__': + main() diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index d2cfcc098f..ee3d17e9fa 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -26,12 +26,12 @@ Co-authored-by: Vincent Breitmoser Signed-off-by: fogtape Assisted-by: Codex --- - android_configure.py | 75 +++++++++++++++++++++++++++++++++++--------- + android_configure.py | 76 +++++++++++++++++++++++++++++++++++--------- configure.py | 8 +++++ - 2 files changed, 68 insertions(+), 15 deletions(-) + 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/android_configure.py b/android_configure.py -index 5cea0393f4..5593019b06 100644 +index 5cea0393f4..c76ec94c8f 100644 --- a/android_configure.py +++ b/android_configure.py @@ -1,25 +1,14 @@ @@ -71,7 +71,7 @@ index 5cea0393f4..5593019b06 100644 toolchain_path = android_ndk_path + "/toolchains/llvm/prebuilt/darwin-x86_64" elif platform.system() == "Linux": -@@ -65,13 +54,69 @@ elif platform.system() == "Linux": +@@ -65,13 +54,70 @@ elif platform.system() == "Linux": os.environ['PATH'] += os.pathsep + toolchain_path + "/bin" os.environ['CC'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang" os.environ['CXX'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang++" @@ -121,6 +121,7 @@ index 5cea0393f4..5593019b06 100644 +# instead of silently shipping a different feature set. Lite drops FFI. +extra_flags = " --v8-enable-temporal-support" +if flavor == "lite": ++ GYP_DEFINES += " node_mobile_icu_profile=danmu-lite.json" + extra_flags = " --v8-disable-temporal-support --without-ffi --without-amaro --without-inspector --without-sqlite --disable-single-executable-application --v8-disable-object-print" + # Pointer compression: 32-bit compressed pointers inside a 4GB cage — the + # size lever on the JS heap rather than on the binary. lite only, because it diff --git a/patches/build-profile-ICU-data-for-danmu_api-lite-runtimes.patch b/patches/build-profile-ICU-data-for-danmu_api-lite-runtimes.patch new file mode 100644 index 0000000000..e400f4dc16 --- /dev/null +++ b/patches/build-profile-ICU-data-for-danmu_api-lite-runtimes.patch @@ -0,0 +1,465 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: fogtape +Date: Fri, 2 Oct 2026 12:00:00 +0800 +Subject: [PATCH] build: profile ICU data for danmu_api lite runtimes + +Keep complete ICU API mode while filtering the lite data payload to the +core's retained locale families and all Node TextDecoder web encodings. +Preserve converter dependencies, Unicode normalization, IDNA, segmentation +and complete timezone coverage. Validate required resources before and after +trimming, and use an independent intermediate package so a host's complete +ICU cannot hide missing data. + +The host genrb's StringBaseResource returns its existing char16_t buffer +directly. The bundled public char16ptr header has the compatibility helper +in global scope, so icu::toUCharPtr fails when rebuilding this host tool. +This does not change the linked mobile runtime. +--- + deps/icu-small/source/tools/genrb/reslist.h | 2 +- + tools/icu/danmu-lite.json | 287 ++++++++++++++++++++ + tools/icu/icu-generic.gyp | 19 +- + tools/icu/prepare-mobile-icu.py | 83 ++++++ + 4 files changed, 385 insertions(+), 6 deletions(-) + create mode 100644 tools/icu/danmu-lite.json + create mode 100755 tools/icu/prepare-mobile-icu.py + +diff --git a/deps/icu-small/source/tools/genrb/reslist.h b/deps/icu-small/source/tools/genrb/reslist.h +index 17797bc36c..93f5384d85 100644 +--- a/deps/icu-small/source/tools/genrb/reslist.h ++++ b/deps/icu-small/source/tools/genrb/reslist.h +@@ -326,7 +326,7 @@ public: + StringBaseResource(int8_t type, const char16_t *value, int32_t len, UErrorCode &errorCode); + virtual ~StringBaseResource(); + +- const char16_t *getBuffer() const { return icu::toUCharPtr(fString.getBuffer()); } ++ const char16_t *getBuffer() const { return fString.getBuffer(); } + int32_t length() const { return fString.length(); } + + virtual void handlePreWrite(uint32_t *byteOffset) override; +diff --git a/tools/icu/danmu-lite.json b/tools/icu/danmu-lite.json +new file mode 100644 +index 0000000000..fb9240da2b +--- /dev/null ++++ b/tools/icu/danmu-lite.json +@@ -0,0 +1,287 @@ ++{ ++ "comment": "danmu_api custom ICU: preserve en/zh/yue/ja/ko locale families, all Node TextDecoder web encodings, complete Unicode normalization, IDNA, segmentation and time zones; remove unrelated locale families, non-web converter tables, transliteration and rule-based spellout data.", ++ "variables": { ++ "locales": { ++ "only": [ ++ "en", ++ "en_001", ++ "en_150", ++ "en_AE", ++ "en_AG", ++ "en_AI", ++ "en_AS", ++ "en_AT", ++ "en_AU", ++ "en_BB", ++ "en_BE", ++ "en_BI", ++ "en_BM", ++ "en_BS", ++ "en_BW", ++ "en_BZ", ++ "en_CA", ++ "en_CC", ++ "en_CH", ++ "en_CK", ++ "en_CM", ++ "en_CX", ++ "en_CY", ++ "en_CZ", ++ "en_DE", ++ "en_DG", ++ "en_DK", ++ "en_DM", ++ "en_EE", ++ "en_ER", ++ "en_ES", ++ "en_FI", ++ "en_FJ", ++ "en_FK", ++ "en_FM", ++ "en_FR", ++ "en_GB", ++ "en_GD", ++ "en_GE", ++ "en_GG", ++ "en_GH", ++ "en_GI", ++ "en_GM", ++ "en_GS", ++ "en_GU", ++ "en_GY", ++ "en_HK", ++ "en_HU", ++ "en_ID", ++ "en_IE", ++ "en_IL", ++ "en_IM", ++ "en_IN", ++ "en_IO", ++ "en_IT", ++ "en_JE", ++ "en_JM", ++ "en_JP", ++ "en_KE", ++ "en_KI", ++ "en_KN", ++ "en_KY", ++ "en_LC", ++ "en_LR", ++ "en_LS", ++ "en_LT", ++ "en_LV", ++ "en_MG", ++ "en_MH", ++ "en_MO", ++ "en_MP", ++ "en_MS", ++ "en_MT", ++ "en_MU", ++ "en_MV", ++ "en_MW", ++ "en_MY", ++ "en_NA", ++ "en_NF", ++ "en_NG", ++ "en_NH", ++ "en_NL", ++ "en_NO", ++ "en_NR", ++ "en_NU", ++ "en_NZ", ++ "en_PG", ++ "en_PH", ++ "en_PK", ++ "en_PL", ++ "en_PN", ++ "en_PR", ++ "en_PT", ++ "en_PW", ++ "en_RH", ++ "en_RO", ++ "en_RW", ++ "en_SB", ++ "en_SC", ++ "en_SD", ++ "en_SE", ++ "en_SG", ++ "en_SH", ++ "en_SI", ++ "en_SK", ++ "en_SL", ++ "en_SS", ++ "en_SX", ++ "en_SZ", ++ "en_TC", ++ "en_TK", ++ "en_TO", ++ "en_TT", ++ "en_TV", ++ "en_TZ", ++ "en_UA", ++ "en_UG", ++ "en_UM", ++ "en_US", ++ "en_US_POSIX", ++ "en_VC", ++ "en_VG", ++ "en_VI", ++ "en_VU", ++ "en_WS", ++ "en_ZA", ++ "en_ZM", ++ "en_ZW", ++ "ja", ++ "ja_JP", ++ "ja_JP_TRADITIONAL", ++ "ko", ++ "ko_CN", ++ "ko_KP", ++ "ko_KR", ++ "root", ++ "yue", ++ "yue_CN", ++ "yue_HK", ++ "yue_Hans", ++ "yue_Hans_CN", ++ "yue_Hant", ++ "yue_Hant_CN", ++ "yue_Hant_HK", ++ "yue_Hant_MO", ++ "zh", ++ "zh_CN", ++ "zh_HK", ++ "zh_Hans", ++ "zh_Hans_CN", ++ "zh_Hans_HK", ++ "zh_Hans_MO", ++ "zh_Hans_MY", ++ "zh_Hans_SG", ++ "zh_Hant", ++ "zh_Hant_HK", ++ "zh_Hant_MO", ++ "zh_Hant_MY", ++ "zh_Hant_TW", ++ "zh_MO", ++ "zh_SG", ++ "zh_TW" ++ ] ++ }, ++ "none": { ++ "only": [] ++ } ++ }, ++ "trees": { ++ "ROOT": "locales", ++ "coll": "locales", ++ "curr": "locales", ++ "lang": "locales", ++ "region": "locales", ++ "unit": "locales", ++ "zone": "locales", ++ "brkitr": "locales", ++ "rbnf": "none", ++ "translit": "none", ++ "converters": { ++ "only": [ ++ "cns-11643-1992", ++ "euc-jp-2007", ++ "gb18030-2022", ++ "ibm-1089_P100-1995", ++ "ibm-1168_P100-2002", ++ "ibm-1363_P11B-1998", ++ "ibm-1373_P100-2002", ++ "ibm-1375_P100-2008", ++ "ibm-1386_P100-2001", ++ "ibm-5012_P100-1999", ++ "ibm-5346_P100-1998", ++ "ibm-5347_P100-1998", ++ "ibm-5348_P100-1997", ++ "ibm-5349_P100-1998", ++ "ibm-5350_P100-1998", ++ "ibm-5354_P100-1998", ++ "ibm-5478_P100-1995", ++ "ibm-866_P100-1995", ++ "ibm-878_P100-1996", ++ "ibm-9005_X110-2007", ++ "ibm-912_P100-1995", ++ "ibm-913_P100-2000", ++ "ibm-914_P100-1995", ++ "ibm-915_P100-1995", ++ "ibm-921_P100-1995", ++ "ibm-923_P100-1998", ++ "ibm-943_P15A-2003", ++ "ibm-9447_P100-2002", ++ "ibm-9448_X100-2005", ++ "ibm-9449_P100-2002", ++ "ibm-949_P110-1999", ++ "ibm-949_P11A-1999", ++ "ibm-970_P110_P110-2006_U2", ++ "iso-8859_10-1998", ++ "iso-8859_14-1998", ++ "jisx-212", ++ "macos-0_2-10.2", ++ "macos-7_3-10.2", ++ "windows-874-2000", ++ "windows-936-2000", ++ "windows-949-2000", ++ "windows-950-2000" ++ ] ++ } ++ }, ++ "remove": [], ++ "keep": [ ++ "pool.res", ++ "supplementalData.res", ++ "zoneinfo64.res", ++ "cnvalias.icu", ++ "brkitr/root.res", ++ "cns-11643-1992.cnv", ++ "euc-jp-2007.cnv", ++ "gb18030-2022.cnv", ++ "ibm-1089_P100-1995.cnv", ++ "ibm-1168_P100-2002.cnv", ++ "ibm-1363_P11B-1998.cnv", ++ "ibm-1373_P100-2002.cnv", ++ "ibm-1375_P100-2008.cnv", ++ "ibm-1386_P100-2001.cnv", ++ "ibm-5012_P100-1999.cnv", ++ "ibm-5346_P100-1998.cnv", ++ "ibm-5347_P100-1998.cnv", ++ "ibm-5348_P100-1997.cnv", ++ "ibm-5349_P100-1998.cnv", ++ "ibm-5350_P100-1998.cnv", ++ "ibm-5354_P100-1998.cnv", ++ "ibm-5478_P100-1995.cnv", ++ "ibm-866_P100-1995.cnv", ++ "ibm-878_P100-1996.cnv", ++ "ibm-9005_X110-2007.cnv", ++ "ibm-912_P100-1995.cnv", ++ "ibm-913_P100-2000.cnv", ++ "ibm-914_P100-1995.cnv", ++ "ibm-915_P100-1995.cnv", ++ "ibm-921_P100-1995.cnv", ++ "ibm-923_P100-1998.cnv", ++ "ibm-943_P15A-2003.cnv", ++ "ibm-9447_P100-2002.cnv", ++ "ibm-9448_X100-2005.cnv", ++ "ibm-9449_P100-2002.cnv", ++ "ibm-949_P110-1999.cnv", ++ "ibm-949_P11A-1999.cnv", ++ "ibm-970_P110_P110-2006_U2.cnv", ++ "iso-8859_10-1998.cnv", ++ "iso-8859_14-1998.cnv", ++ "jisx-212.cnv", ++ "macos-0_2-10.2.cnv", ++ "macos-7_3-10.2.cnv", ++ "windows-874-2000.cnv", ++ "windows-936-2000.cnv", ++ "windows-949-2000.cnv", ++ "windows-950-2000.cnv", ++ "nfkc.nrm", ++ "nfkc_cf.nrm", ++ "nfkc_scf.nrm", ++ "uts46.nrm", ++ "coll/ucadata.icu" ++ ] ++} +diff --git a/tools/icu/icu-generic.gyp b/tools/icu/icu-generic.gyp +index c4e8c6fbb9..d293800d14 100644 +--- a/tools/icu/icu-generic.gyp ++++ b/tools/icu/icu-generic.gyp +@@ -7,6 +7,7 @@ + + { + 'variables': { ++ 'node_mobile_icu_profile%': '', + 'icu_src_derb': [ + '<(icu_path)/source/tools/genrb/derb.c', + '<(icu_path)/source/tools/genrb/derb.cpp' +@@ -224,6 +225,11 @@ + # full data - no trim needed + 'sources': [ '<(SHARED_INTERMEDIATE_DIR)/icudt<(icu_ver_major)_dat.<(icu_asm_ext)' ], + 'dependencies': [ 'genccode#host', 'icupkg#host', 'icu_implementation#host', 'icu_uconfig' ], ++ 'conditions': [ ++ [ 'node_mobile_icu_profile != ""', { ++ 'dependencies': [ 'genrb#host', 'iculslocs#host' ], ++ } ], ++ ], + 'include_dirs': [ + '<(icu_path)/source/common', + ], +@@ -231,12 +237,15 @@ + { + # Copy the .dat file, swapping endianness if needed. + 'action_name': 'icupkg', +- 'inputs': [ '<(icu_data_in)' ], ++ 'inputs': [ '<(icu_data_in)', 'prepare-mobile-icu.py', ++ 'icutrim.py', 'danmu-lite.json' ], + 'outputs':[ '<(SHARED_INTERMEDIATE_DIR)/icudt<(icu_ver_major)<(icu_endianness).dat' ], +- 'action': [ '<(PRODUCT_DIR)/icupkg<(EXECUTABLE_SUFFIX)', +- '-t<(icu_endianness)', +- '<@(_inputs)', +- '<@(_outputs)', ++ 'action': [ '<(python)', 'prepare-mobile-icu.py', ++ '--tools', '<(PRODUCT_DIR)', ++ '--input', '<(icu_data_in)', ++ '--output', '<@(_outputs)', ++ '--endian', '<(icu_endianness)', ++ '--profile', '<(node_mobile_icu_profile)', + ], + }, + { +diff --git a/tools/icu/prepare-mobile-icu.py b/tools/icu/prepare-mobile-icu.py +new file mode 100755 +index 0000000000..1e79535e55 +--- /dev/null ++++ b/tools/icu/prepare-mobile-icu.py +@@ -0,0 +1,83 @@ ++#!/usr/bin/env python3 ++"""Prepare complete or profiled ICU data without enabling Node's small-ICU mode.""" ++import argparse ++import json ++from pathlib import Path ++import re ++import subprocess ++import sys ++import tempfile ++ ++ ++def listing(icupkg, data): ++ return set(subprocess.check_output([str(icupkg), '-l', str(data)], text=True).splitlines()) ++ ++ ++def required_items(profile, original): ++ required = set(profile['keep']) ++ # Protect selected locale resources and aliases, including parent fallbacks. ++ for tree, settings in profile['trees'].items(): ++ if isinstance(settings, str): ++ settings = profile['variables'][settings] ++ prefix = '' if tree in ('ROOT', 'converters') else tree + '/' ++ extension = '.cnv' if tree == 'converters' else '.res' ++ for item in settings.get('only', []): ++ name = prefix + item + extension ++ if tree == 'converters' or name in original: ++ required.add(name) ++ return required ++ ++ ++def prepare(tools, source, output, endian, profile_path): ++ icupkg = tools / 'icupkg' ++ output.parent.mkdir(parents=True, exist_ok=True) ++ if not profile_path: ++ subprocess.run([str(icupkg), '-t' + endian, str(source), str(output)], check=True) ++ return ++ profile = json.loads(Path(profile_path).read_text()) ++ original = listing(icupkg, source) ++ required = required_items(profile, original) ++ if missing := required - original: ++ raise RuntimeError('ICU source is missing required data: ' + ', '.join(sorted(missing))) ++ match = re.fullmatch(r'icudt(\d+)[bl]\.dat', source.name) ++ if not match: ++ raise RuntimeError(f'unrecognized ICU source data filename: {source.name}') ++ # An independent package name prevents an installed host's complete ICU ++ # from supplying resources that were deleted from the candidate package. ++ name = 'dmlt' + match[1].zfill(3) + endian + '.dat' ++ with tempfile.TemporaryDirectory(prefix='icu-profile-', dir=output.parent) as folder: ++ folder = Path(folder) ++ result = subprocess.run([ ++ sys.executable, str(Path(__file__).with_name('icutrim.py')), ++ '-P', str(tools), '-D', str(source), '-T', str(folder / 'trim'), ++ '-F', str(Path(profile_path).resolve()), '-O', name, ++ '-e', 'little' if endian == 'l' else 'big', ++ ], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) ++ if result.returncode: ++ raise RuntimeError('ICU profile generation failed:\n' + result.stdout[-8000:]) ++ trimmed = folder / 'trim' / name ++ if missing := required - listing(icupkg, trimmed): ++ raise RuntimeError('ICU trimming removed required data: ' + ', '.join(sorted(missing))) ++ staged = folder / output.name ++ subprocess.run([str(icupkg), '-t' + endian, str(trimmed), str(staged)], check=True) ++ # icupkg verifies data dependencies while repackaging. The conventional ++ # package/entry-point name must remain icudt for V8 and Node. ++ staged.replace(output) ++ print(f'ICU profile {Path(profile_path).name}: ' ++ f'{source.stat().st_size} -> {output.stat().st_size} bytes') ++ ++ ++def main(): ++ parser = argparse.ArgumentParser(description=__doc__) ++ parser.add_argument('--tools', type=Path, required=True) ++ parser.add_argument('--input', type=Path, required=True) ++ parser.add_argument('--output', type=Path, required=True) ++ parser.add_argument('--endian', choices=('l', 'b'), required=True) ++ parser.add_argument('--profile', default='') ++ args = parser.parse_args() ++ prepare(args.tools.resolve(), args.input.resolve(), args.output.resolve(), ++ args.endian, args.profile) ++ ++ ++if __name__ == '__main__': ++ main() diff --git a/patches/files.map b/patches/files.map index a1309817d1..70a3c0c623 100644 --- a/patches/files.map +++ b/patches/files.map @@ -132,3 +132,7 @@ lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch lib/internal/process/ build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch deps/crates/crates.gyp build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch deps/libffi/generate-headers.py build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch deps/libffi/libffi.gyp +build-profile-ICU-data-for-danmu_api-lite-runtimes.patch deps/icu-small/source/tools/genrb/reslist.h +build-profile-ICU-data-for-danmu_api-lite-runtimes.patch tools/icu/danmu-lite.json +build-profile-ICU-data-for-danmu_api-lite-runtimes.patch tools/icu/icu-generic.gyp +build-profile-ICU-data-for-danmu_api-lite-runtimes.patch tools/icu/prepare-mobile-icu.py diff --git a/patches/series b/patches/series index fd1fa5d7b3..6ce282ba2b 100644 --- a/patches/series +++ b/patches/series @@ -19,3 +19,4 @@ doc-build-mobile-notes-and-ignores.patch ci-remove-upstream-only-workflows-and-config.patch lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch +build-profile-ICU-data-for-danmu_api-lite-runtimes.patch diff --git a/scripts/package-preview.py b/scripts/package-preview.py index 57899c6ea0..bd32575107 100644 --- a/scripts/package-preview.py +++ b/scripts/package-preview.py @@ -43,7 +43,10 @@ def main(): flavor=flavor, recipe_commit=sha, source_tree=tree, branch=os.environ.get('GITHUB_REF_NAME', ''), run_url=f"https://github.com/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}", - preview=True, enabled_features=['full-icu'] + (['node:ffi', 'Temporal'] if flavor == 'full' else []), + preview=True, icu_api_mode='full-icu', + icu_data_profile='complete' if flavor == 'full' else 'danmu-lite', + android_symbols='stripped' if platform == 'android' and flavor == 'lite' else 'default', + enabled_features=(['full-icu', 'node:ffi', 'Temporal'] if flavor == 'full' else ['icu', 'danmu-icu-profile']), disabled_features=[] if flavor == 'full' else [ 'node:ffi', 'Temporal', 'node:sqlite', 'Web Storage', 'Inspector', 'TypeScript type-stripping', 'SEA', diff --git a/scripts/test-mobile-flavors.py b/scripts/test-mobile-flavors.py index f64ff8054d..4a43415a57 100755 --- a/scripts/test-mobile-flavors.py +++ b/scripts/test-mobile-flavors.py @@ -41,7 +41,7 @@ def test_android_flags_and_pointer_compression(self): capture = folder / 'configured.json' configure = folder / 'configure' configure.write_text(f'#!{sys.executable}\nimport json,sys\n' - f'open({str(capture)!r}, "w").write(json.dumps(sys.argv[1:]))\n') + f'open({str(capture)!r}, "w").write(json.dumps({{"flags":sys.argv[1:],"gyp":__import__("os").environ.get("GYP_DEFINES","")}}))\n') configure.chmod(0o755) for flavor in ('full', 'lite'): for arch in ('arm', 'arm64', 'x86_64'): @@ -50,7 +50,10 @@ def test_android_flags_and_pointer_compression(self): env.pop('NODEJS_MOBILE_SCCACHE', None) subprocess.run(ANDROID_WRAPPER + [str(ndk), '24', arch], cwd=folder, env=env, check=True, capture_output=True, text=True) - flags = set(json.loads(capture.read_text())) + captured = json.loads(capture.read_text()) + flags = set(captured['flags']) + self.assertEqual('node_mobile_icu_profile=danmu-lite.json' in captured['gyp'], + flavor == 'lite') self.assertIn('--with-intl=full-icu', flags) self.assertIn('--shared', flags) self.assertIn('--cross-compiling', flags) @@ -71,7 +74,7 @@ def test_ios_flags_and_framework_archives(self): script = (SOURCE / 'tools/ios_framework_prepare.sh').read_text() prefix = script.split('build_for_arm64_device() {', 1)[0] probe = prefix + ''' -printf '\nINTL:%s\nFLAGS:%s\n' "$INTL" "$LITE_FLAGS" +printf '\nINTL:%s\nFLAGS:%s\nICU_PROFILE:%s\n' "$INTL" "$LITE_FLAGS" "$ICU_PROFILE_GYP_DEFINES" for archive in "${outputs_arm64[@]}"; do printf 'LIB:%s\n' "$archive"; done ''' project = (SOURCE / 'tools/ios-framework/NodeMobile.xcodeproj/project.pbxproj').read_text() @@ -85,6 +88,7 @@ def test_ios_flags_and_framework_archives(self): capture_output=True, text=True, check=True) lines = result.stdout.splitlines() self.assertIn('INTL:full-icu', lines) + self.assertIn('ICU_PROFILE:' + ('node_mobile_icu_profile=danmu-lite.json' if flavor == 'lite' else ''), lines) flags = set(shlex.split(next(line[6:] for line in lines if line.startswith('FLAGS:')))) archives = {line[4:] for line in lines if line.startswith('LIB:')} @@ -129,8 +133,11 @@ def test_preview_metadata_matches_flavors(self): for package in output.glob('*.zip'): with zipfile.ZipFile(package) as archive: info = json.loads(archive.read('BUILD-INFO.json')) - self.assertIn('full-icu', info['enabled_features']) lite = info['flavor'] == 'lite' + self.assertEqual(info['icu_api_mode'], 'full-icu') + self.assertEqual(info['icu_data_profile'], 'danmu-lite' if lite else 'complete') + self.assertEqual('full-icu' in info['enabled_features'], not lite) + self.assertEqual('danmu-icu-profile' in info['enabled_features'], lite) for feature in ('node:ffi', 'Temporal'): self.assertEqual(feature in info['enabled_features'], not lite) self.assertEqual(feature in info['disabled_features'], lite) @@ -140,6 +147,34 @@ def test_preview_metadata_matches_flavors(self): digest, name = line.split() self.assertEqual(hashlib.sha256((output / name).read_bytes()).hexdigest(), digest) + def test_icu_profile_required_resources_exist_in_pinned_data(self): + # A version upgrade must fail loudly if renamed ICU data invalidates the + # reviewed locale/converter closure. Parse the actual canned package. + import bz2 + import importlib.util + import re + import struct + major = re.search(r'"(\d+)\.', + (SOURCE / 'deps/icu-small/source/common/unicode/uvernum.h').read_text()).group(1) + canned = SOURCE / f'deps/icu-small/source/data/in/icudt{major}l.dat' + data = canned.read_bytes() if canned.exists() else bz2.decompress( + canned.with_suffix('.dat.bz2').read_bytes()) + header = struct.unpack_from(' 1 else ROOT / 'out' +if len(sys.argv) > 1: + del sys.argv[1] + + +class RuntimeStripping(unittest.TestCase): + def test_exports_runtime_and_failure_atomicity(self): + clang = shutil.which('clang++') + strip = shutil.which('llvm-strip') + self.assertIsNotNone(clang, 'clang++ is required') + self.assertIsNotNone(strip, 'llvm-strip is required') + tools = Path(strip).parent + helper = SOURCE / 'tools/strip-android-runtime.py' + with tempfile.TemporaryDirectory() as directory: + directory = Path(directory) + source = directory / 'embedder.cc' + source.write_text('namespace node { int Start(int argc, char**) { return argc + 40; } }\n' + 'extern "C" int napi_test_export() { return 42; }\n') + library = directory / 'libnode.so' + subprocess.run([clang, '-shared', '-fPIC', '-g', '-O0', str(source), + '-o', str(library)], check=True, capture_output=True) + before = library.stat().st_size + command = [sys.executable, str(helper), '--tools', str(tools), str(library)] + subprocess.run(command, check=True, capture_output=True) + self.assertLess(library.stat().st_size, before) + runtime = ctypes.CDLL(str(library)) + start = getattr(runtime, '_ZN4node5StartEiPPc') + start.argtypes = [ctypes.c_int, ctypes.c_void_p] + start.restype = ctypes.c_int + self.assertEqual(start(2, None), 42) + self.assertEqual(runtime.napi_test_export(), 42) + digest = hashlib.sha256(library.read_bytes()).digest() + subprocess.run(command, check=True, capture_output=True) + self.assertEqual(hashlib.sha256(library.read_bytes()).digest(), digest) + + # A failed entry-point/ABI check must leave the caller's file intact. + invalid = directory / 'invalid.so' + invalid.write_bytes(b'not an ELF library') + result = subprocess.run(command[:-1] + [str(invalid)], capture_output=True) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(invalid.read_bytes(), b'not an ELF library') + + +if __name__ == '__main__': + unittest.main() From 28fc4a86fa5ed16a71a7ca50ecf5034437c07d8d Mon Sep 17 00:00:00 2001 From: fogtape Date: Fri, 2 Oct 2026 21:59:33 +0800 Subject: [PATCH 3/8] ci: install LLVM tools for runtime strip regression --- .github/workflows/verify-patches.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/verify-patches.yml b/.github/workflows/verify-patches.yml index e8b596139d..a14779cafc 100644 --- a/.github/workflows/verify-patches.yml +++ b/.github/workflows/verify-patches.yml @@ -91,6 +91,11 @@ jobs: - 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 From 34bbaea23295b0dbf5b57863b1841aeb8d8bb4d9 Mon Sep 17 00:00:00 2001 From: fogtape Date: Tue, 6 Oct 2026 11:47:36 +0800 Subject: [PATCH 4/8] =?UTF-8?q?upgrade:=20=E5=8D=87=E7=BA=A7=20Node=2026.1?= =?UTF-8?q?1.0=20=E5=AE=98=E6=96=B9=20PR=20=E5=BF=AB=E7=85=A7=E5=B9=B6?= =?UTF-8?q?=E6=B2=BF=E7=94=A8=20lite=20=E8=A3=81=E5=89=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 固定 nodejs/node#66546 的 7f68d75ee7963cd38ddc3783dd529f826bbde614, 重建移动端补丁与源码校验值,保留 danmu ICU 配置、功能裁剪及 Android 符号验证。 适配上游构建变动与新设备回归,支持 SHA 重建/缓存,PR 快照仅生成预览。 全新拉取与配方往返一致;38 项本地回归和 10 个完整 GYP 图验证通过。 ICU 数据包实际裁剪至 11,026,480 字节;完整交叉编译、设备测试及产物大小由 CI 验证。 Assisted-by: Codex Signed-off-by: fogtape --- .github/actions/materialize/action.yml | 2 +- .github/workflows/verify-patches.yml | 5 +- README.md | 12 +- docs/BUILDING.md | 6 +- docs/CHANGELOG.md | 21 +- docs/DANMU-LITE.md | 3 +- docs/UPGRADING.md | 14 + docs/UPSTREAM-26.11.md | 83 ++++++ docs/VERSION-LINES.md | 9 +- expected-tree.txt | 2 +- mobile-src/src/node_mobile_version.h | 2 +- .../tools/mobile-test/audit-test-edits.sh | 6 + .../mobile-test/curated-device-tests.txt | 13 + ...S-dest-os-and-toolchain-configure.py.patch | 6 +- ...poral-Rust-toolsets-and-libffi-iOS-c.patch | 71 ++++- ...ile-platform-settings-in-common.gypi.patch | 31 +-- ...mobile-targets-in-node.gyp-node.gypi.patch | 16 +- ...-host-target-settings-in-v8-gypfiles.patch | 16 +- ...e-upstream-only-workflows-and-config.patch | 250 +++++++++++------- .../doc-build-mobile-notes-and-ignores.patch | 8 +- patches/files.map | 2 + ...bAssembly-polyfill-when-the-engine-h.patch | 6 +- ...e-the-mobile-version-key-in-metadata.patch | 2 +- ...orm-guards-in-node.cc-node_internals.patch | 6 +- ...-skip-unclonable-env-vars-on-Android.patch | 4 +- patches/test-mobile-test-harness.patch | 8 +- ...parallel-and-sequential-tests-for-mo.patch | 130 +++++++-- release-ready.txt | 1 - scripts/clone-upstream.sh | 22 ++ scripts/prepare.sh | 4 +- scripts/release-control.py | 5 +- scripts/test-mobile-flavors.py | 6 +- scripts/test-release-control.py | 13 + scripts/test-upstream-clone.py | 68 +++++ upstream-base.txt | 8 +- 35 files changed, 660 insertions(+), 201 deletions(-) create mode 100644 docs/UPSTREAM-26.11.md delete mode 100644 release-ready.txt create mode 100755 scripts/clone-upstream.sh create mode 100644 scripts/test-upstream-clone.py diff --git a/.github/actions/materialize/action.yml b/.github/actions/materialize/action.yml index 0ef4aef685..e7c3f2abf1 100644 --- a/.github/actions/materialize/action.yml +++ b/.github/actions/materialize/action.yml @@ -33,7 +33,7 @@ runs: set -euo pipefail UP="$RUNNER_TEMP/upstream-$BASE_TAG" if [ ! -d "$UP/.git" ]; then - git clone --depth 1 --branch "$BASE_TAG" https://github.com/nodejs/node.git "$UP" + scripts/clone-upstream.sh https://github.com/nodejs/node.git "$BASE_TAG" "$UP" fi # file:// so git treats the shallow cache as a proper transport. UPSTREAM_REPO="file://$UP" scripts/prepare.sh "$RUNNER_TEMP/matsrc" diff --git a/.github/workflows/verify-patches.yml b/.github/workflows/verify-patches.yml index a14779cafc..2bf3a9b312 100644 --- a/.github/workflows/verify-patches.yml +++ b/.github/workflows/verify-patches.yml @@ -84,6 +84,9 @@ jobs: - name: Reconstruct + verify run: scripts/prepare.sh out + - name: Check immutable upstream clones and cache transport + run: python3 scripts/test-upstream-clone.py + - uses: actions/setup-node@v6 with: node-version: '24' @@ -286,7 +289,7 @@ jobs: run: | set -euo pipefail BASE=$(grep -Ev '^\s*(#|$)' upstream-base.txt | head -n1 | tr -d '[:space:]') - git clone --depth 1 --branch "$BASE" https://github.com/nodejs/node.git src + scripts/clone-upstream.sh https://github.com/nodejs/node.git "$BASE" src git -C src config user.name ci git -C src config user.email ci@invalid while IFS= read -r p; do diff --git a/README.md b/README.md index ea96237e63..0cd6398f02 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,16 @@ in-app, with npm modules and N-API native addons. | Android | `libnode.so` + headers | `arm64-v8a`, `armeabi-v7a`, `x86_64` (16 KB page size) | | iOS | `NodeMobile.xcframework` | `arm64` device, `arm64` simulator | -Current line: **Node.js 26** (`v26.10.0`, prerelease line). +Current line: **Node.js 26** (`v26.11.0`, upstream PR preview). +This checkout pins [official release proposal #66546](https://github.com/nodejs/node/pull/66546) +at `7f68d75ee7963cd38ddc3783dd529f826bbde614`, before its merge/tag. +Builds are CI previews; [provenance and validation](docs/UPSTREAM-26.11.md) +record how to move to the official tag after release. The stable Node 24 line continues independently on [`recipe`](https://github.com/fogtape/nodejs-mobile/tree/recipe). See [maintained version lines](docs/VERSION-LINES.md) for branch selection, build instructions and download locations. `process.version` reports the upstream version unchanged, so version-parsing tools keep working; the -mobile build is identified by `process.versions.mobile` (e.g. `"26.10.0-0"`). +mobile build is identified by `process.versions.mobile` (e.g. `"26.11.0-0"`). ## Get it @@ -53,13 +57,13 @@ for building one — about 1.5 MB instead of a 1 GB fork: | | | |---|---| -| `upstream-base.txt` | the pinned upstream release tag (`v26.10.0`) | +| `upstream-base.txt` | the pinned upstream PR commit (see [26.11 provenance](docs/UPSTREAM-26.11.md)) | | `patches/` | per-concern patches to upstream files, plus `series` (apply order) and `files.map` (which patch owns which file) | | `mobile-src/` | files that have no upstream counterpart — build scripts, the iOS framework project, test apps and harness | | `expected-tree.txt` | the git tree hash the reconstruction must produce | | `scripts/` | `prepare.sh` (recipe → source tree) and `regenerate-patches.py` (source tree → recipe) | -`scripts/prepare.sh` clones upstream at the pinned tag, applies the patch +`scripts/prepare.sh` clones upstream at the pinned tag or commit, applies the patch series, overlays `mobile-src/`, and verifies the result hashes to `expected-tree.txt` — so what CI builds, what a release tag contains, and what was reviewed here are provably the same bytes. diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 5b3b585d55..38a3d95b75 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -1,6 +1,8 @@ # Build Instructions -These instructions describe the Node 26 line (`recipe-v26`). For Node 24, +These instructions describe the Node 26 line. This checkout is the pinned +26.11 upstream PR preview on `upgrade/node26.11-mobile-lite`; +see [provenance](UPSTREAM-26.11.md). For Node 24, use the `recipe` branch and its toolchain instructions. See [VERSION-LINES.md](VERSION-LINES.md) for the maintenance policy and the Node 26 FFI/Temporal configuration. @@ -86,7 +88,7 @@ sdkmanager "ndk;29.0.14206865" This repository holds the recipe, not the source. Generate a tree from it: ```sh -git clone -b recipe-v26 https://github.com/fogtape/nodejs-mobile +git clone -b upgrade/node26.11-mobile-lite https://github.com/fogtape/nodejs-mobile cd nodejs-mobile && scripts/prepare.sh && cd out ``` diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d5312d2bac..9aa13b8df0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ +26.11.0-0 (upstream PR preview)
26.10.0-0
26.1.0-0
24.21.0-0
@@ -41,8 +42,26 @@ + +## Unreleased, Version 26.11.0-0 (upstream PR preview) + +- Pin official Node.js release proposal [#66546](https://github.com/nodejs/node/pull/66546) + at `7f68d75ee7963cd38ddc3783dd529f826bbde614` and rebase the mobile patch series. + This source upgrade does not arm publication. +- Carry forward the danmu lite ICU data profile, all web encoding converters, + Chinese collation and Unicode normalization. Lite omits Temporal, FFI, Amaro, + Inspector, SQLite/Web Storage, SEA and native object-print helpers; Android + retains JIT/native WASM and 64-bit pointer compression. Keep the lite runtime + strip helper and dynamic-export checks. +- Preserve upstream's new Temporal zoneinfo generator alongside the mobile + Rust host/target actions. Adapt the z/OS build refactor without losing iOS guards. +- Triage new subprocess tests on both platforms, keep in-process bench/negative-zero + assertions, update the renamed VFS exclusion and add 11 in-process device regressions. +- Support SHA-pinned materialization in local builds and CI. Block release + preparation/publication until an official release tag is pinned and validated. + -## 2026-10-01, Version 26.10.0-0 (Current) +## 2026-10-01, Version 26.10.0-0 - Upgrade the independent Node 26 line to official Node.js 26.10.0. - Enable Temporal in Android/iOS full builds using Rust 1.86.0, with separate diff --git a/docs/DANMU-LITE.md b/docs/DANMU-LITE.md index 49b409025d..40fcef80f5 100644 --- a/docs/DANMU-LITE.md +++ b/docs/DANMU-LITE.md @@ -29,7 +29,8 @@ cannot be inferred from today's imports. 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 +With the Node 26.11 proposal’s bundled ICU data, the production helper +reduces 33,107,952 to 11,026,480 bytes (22,081,472 bytes removed). This measures data only, not a complete newly compiled libnode or compressed ZIP. ## Other lite subtractions diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 8950c7169d..ca62a20868 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -11,6 +11,20 @@ the patches are files, and the upgrade is an ordinary reviewable PR. Expect a handful of small conflicts resolvable in minutes — plus, occasionally, one that only the compile catches (see the warning below). +## Preview an upstream release proposal + +A full 40-character commit SHA in `upstream-base.txt` is supported by +`scripts/clone-upstream.sh`, including the materialization cache and per-patch +CI configuration checks. Pin the exact reviewed proposal commit, never a +moving PR branch. `prepare.sh` records a local `refs/nodejs-mobile/upstream-base` +ref for the in-tree test audit; it creates no upstream release tag. + +PR snapshots can run normal builds and produce CI preview packages. Cut release +and prerelease publication reject a SHA baseline. After upstream releases, +compare the proposal commit with the official tag, rebase/regenerate as needed, +update the tree checksum, and complete the build/device gates before release +preparation. The [26.11 preview](UPSTREAM-26.11.md) records the current pin. + ## Re-base the series ```sh diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md new file mode 100644 index 0000000000..d364015504 --- /dev/null +++ b/docs/UPSTREAM-26.11.md @@ -0,0 +1,83 @@ +# Node 26.11 upstream PR preview + +This recipe previews official Node.js release proposal +[#66546](https://github.com/nodejs/node/pull/66546), titled +`2026-10-07, Version 26.11.0 (Current)`. At preparation time on 2026-10-06, +it was open and in draft, with no official `v26.11.0` tag. + +`upstream-base.txt` pins the proposal's exact commit: +`7f68d75ee7963cd38ddc3783dd529f826bbde614`. +The upstream version header reports `v26.11.0`; the mobile header reports +`26.11.0-0`. These identifiers do not mean the official release or a mobile +release has been published. The immutable commit and expected source-tree +checksum identify this preview even if the proposal branch changes. + +## Build and inspect + +Use `upgrade/node26.11-mobile-lite` for this preview. The Node 24 `recipe` +line and maintained Node 26.10 release assets remain independent. + +```sh +git clone -b upgrade/node26.11-mobile-lite https://github.com/fogtape/nodejs-mobile.git +cd nodejs-mobile +scripts/prepare.sh +``` + +For platform builds, follow [BUILDING.md](BUILDING.md). Reconstruction, +the CI source cache and per-patch configure checks all accept the full SHA; +no local release tag is fabricated. The test audit uses a local Git ref +recording the upstream commit, outside the source tree. + +## Flavor contract + +Full retains complete ICU data, Temporal and FFI, with the existing mobile +Rust/libffi integration. The 26.11 rebase also preserves upstream's new +Temporal zoneinfo source generator and the iOS platform guards. + +Lite carries forward the reviewed [danmu contract](DANMU-LITE.md): the ICU +API with all Node web encoding converters, selected English/Chinese/Cantonese/ +Japanese/Korean locale families, Unicode normalization/IDNA, time zones and +segmentation. Temporal, FFI, Amaro, Inspector, SQLite/Web Storage, SEA and +native object-print helpers remain disabled. Android retains JIT/native +WebAssembly and pointer compression on 64-bit targets. iOS retains jitless +V8 and polywasm without pointer compression. Android lite strips non-runtime +symbols only after confirming the dynamic exports remain unchanged. + +The actual 26.11 ICU profile generation retained its required resources and +passed package dependency validation: **33,107,952 → 11,026,480 bytes**. +The 22,081,472-byte reduction measures ICU data, not a compiled libnode or ZIP. + +## Validation + +- A fresh shallow fetch from `nodejs/node` applied all 22 regenerated patches + directly; regeneration from that fresh tree preserved every recipe byte. + It reconstructed tree `a92053298021a6780ac09b4c31653fc210d68ec8`. +- 38 local regression tests passed: immutable SHA/tag/cache cloning (4), + flavor/ICU contracts (5), Rust/libffi toolchain actions (5), embedded verdict + isolation (6), real shared-library symbol stripping (1), release control (17). +- Recipe ownership, YAML/shell/JavaScript syntax, all 219 curated test entries, + zero upstream workflows and the test-edit audit passed. +- All ten full GYP graphs generated successfully: Android arm/arm64/x64 and + iOS arm64 device/simulator, each in full/lite. Config assertions confirmed + full ICU, the Temporal/FFI split and Android-only 64-bit lite pointer + compression. This is graph validation using Termux Clang and Rust version + stubs; it does not compile with the Linux NDK or Xcode toolchains. +- Eleven new in-process Buffer, HTTP, crypto, stream and compression tests + were added to the curated device gate. The new subprocess exclusions + (11 parallel tests and one sequential test per platform) are inferred from + their code; the full device suite must confirm them. The renamed VFS load + exclusion was updated. Existing bench/negative-zero tests retain their + in-process assertions and skip only the child startup checks. + +Complete Android/iOS compilation, newly added device tests and final artifact +size measurements remain CI work. The local checks do not establish that new +mobile binaries have passed device testing. + +## Move to the official release + +After upstream publishes `v26.11.0`, compare its commit against this pin, +review any additional changes, update `upstream-base.txt` to the official tag, +regenerate the recipe and checksum, and rerun the platform/device gates. +The SHA baseline is deliberately rejected by Cut release and manual +prerelease publication. This upgrade does not include a release-ready marker. +Follow [RELEASING.md](RELEASING.md) after the official-tag upgrade is reviewed. diff --git a/docs/VERSION-LINES.md b/docs/VERSION-LINES.md index 4dbe019875..fd7da16b38 100644 --- a/docs/VERSION-LINES.md +++ b/docs/VERSION-LINES.md @@ -8,6 +8,11 @@ versioned together on that branch. | --- | --- | --- | --- | | `recipe` | Node.js 24.21.0 | Stable Node 24 maintenance and releases | Android and iOS, full and lite | | `recipe-v26` | Node.js 26.10.0 | Independent Node 26 maintenance and prereleases | Android and iOS, full and lite | +| `upgrade/node26.11-mobile-lite` | Node.js 26.11.0 proposal at a pinned SHA | Preview of upstream PR #66546 with the danmu lite profile | Android and iOS, full and lite | + +For the 26.11 preview, clone `upgrade/node26.11-mobile-lite`; it pins an immutable +commit, and release preparation is disabled until the official release tag is +reviewed and pinned. See [26.11 provenance](UPSTREAM-26.11.md). The authoritative base is always `upstream-base.txt` on the selected branch. Updating Node 26 does not change the Node 24 recipe or replace its published @@ -42,7 +47,7 @@ partition and expected source-tree checksum. The Node 26 line uses Android NDK r29 (`29.0.14206865`) and Clang 19 for Linux host tools. Node 24 keeps its existing NDK r27d configuration. -Node 26.10 full builds enable V8 Temporal and the experimental `node:ffi` +This Node 26.11 preview’s full builds enable V8 Temporal and the experimental `node:ffi` module on Android and iOS, with complete ICU data (`--with-intl=full-icu`) for named time zones. small-ICU cannot load Temporal timezone information in this upstream version. The build requires Rust **1.86.0** and its standard @@ -89,7 +94,7 @@ run URL with the downloaded files; do not mix platform slices or flavors from different runs. Versioned preview packages additionally contain `BUILD-INFO.json` and are accompanied by `SHA256SUMS`. -`process.version` reports `v26.10.0`; `process.versions.mobile` identifies +This preview reports `process.version` as `v26.11.0`; `process.versions.mobile` identifies the mobile build revision. A CI preview is not a published release. **Cut release** also supports `recipe-v26`; its reviewed PR arms a specific version; after merge, manually run **Build** with `operation=prerelease` and that exact mobile version. Push/PR builds do diff --git a/expected-tree.txt b/expected-tree.txt index 21b09459d8..9fc7d4f9fd 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -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. -cf1e359f446d47683db958b3f70a66ad52ecbb61 +a92053298021a6780ac09b4c31653fc210d68ec8 diff --git a/mobile-src/src/node_mobile_version.h b/mobile-src/src/node_mobile_version.h index 4260606965..4dd80bfb8f 100644 --- a/mobile-src/src/node_mobile_version.h +++ b/mobile-src/src/node_mobile_version.h @@ -4,7 +4,7 @@ #include "node_version.h" #define NODE_MOBILE_MAJOR_VERSION 26 -#define NODE_MOBILE_MINOR_VERSION 10 +#define NODE_MOBILE_MINOR_VERSION 11 #define NODE_MOBILE_PATCH_VERSION 0 // Mobile rebuild revision of the same upstream Node version: the -R in the diff --git a/mobile-src/tools/mobile-test/audit-test-edits.sh b/mobile-src/tools/mobile-test/audit-test-edits.sh index 2924db7e25..0145caff43 100755 --- a/mobile-src/tools/mobile-test/audit-test-edits.sh +++ b/mobile-src/tools/mobile-test/audit-test-edits.sh @@ -18,6 +18,9 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) REPO_ROOT=$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel) cd "${REPO_ROOT}" +# prepare.sh records the exact upstream commit as local git metadata, so +# unreleased PR snapshots can be audited without a fabricated release tag. +# For older materializations, derive the tag from src/node_version.h. # The upstream base is derived from the tree itself: src/node_version.h is # upstream's own version header and is not patched by this project, so it # states exactly which nodejs/node release this tree was built from. That @@ -28,6 +31,9 @@ MA=$(grep -oE '#define NODE_MAJOR_VERSION [0-9]+' src/node_version.h | awk '{pri MI=$(grep -oE '#define NODE_MINOR_VERSION [0-9]+' src/node_version.h | awk '{print $3}') PA=$(grep -oE '#define NODE_PATCH_VERSION [0-9]+' src/node_version.h | awk '{print $3}') BASE="v${MA}.${MI}.${PA}" +if git rev-parse --verify --quiet 'refs/nodejs-mobile/upstream-base^{commit}' >/dev/null; then + BASE=refs/nodejs-mobile/upstream-base +fi if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then echo "error: upstream base '${BASE}' (from src/node_version.h) is not a commit here." >&2 echo " This audit needs the upstream tag present, as it is in a tree" >&2 diff --git a/mobile-src/tools/mobile-test/curated-device-tests.txt b/mobile-src/tools/mobile-test/curated-device-tests.txt index 4a8b86bf63..f067800772 100644 --- a/mobile-src/tools/mobile-test/curated-device-tests.txt +++ b/mobile-src/tools/mobile-test/curated-device-tests.txt @@ -218,3 +218,16 @@ parallel/test-zlib-convenience-methods parallel/test-zlib-destroy-pipe parallel/test-zlib-flush-drain parallel/test-zlib-invalid-arg-value-brotli-compress + +# Node 26.11 in-process buffer, HTTP, crypto, stream and compression regressions. +parallel/test-buffer-islatin1 +parallel/test-buffer-stringlength +parallel/test-http-is-valid-header +parallel/test-http-parser-cached-callbacks +parallel/test-http-server-raw-header-lookup +parallel/test-crypto-randomfill-properties +parallel/test-whatwg-readablestream-async-iterator-shape +parallel/test-zlib-decode-strings +parallel/test-zlib-reset-incomplete-output +parallel/test-zlib-brotli-reset +parallel/test-zlib-zstd-reset-incomplete-frame diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index ee3d17e9fa..d5358b0de8 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -144,10 +144,10 @@ index 5cea0393f4..c76ec94c8f 100644 - os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --openssl-no-asm --cross-compiling") + sys.exit(os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --openssl-no-asm --with-intl=" + intl + extra_flags + " --cross-compiling --shared") >> 8) diff --git a/configure.py b/configure.py -index 8b3332a461..fa5dc65bd1 100755 +index acda6a16c9..c44509883d 100755 --- a/configure.py +++ b/configure.py -@@ -1303,6 +1303,11 @@ parser.add_argument('-C', +@@ -1304,6 +1304,11 @@ parser.add_argument('-C', default=None, help=argparse.SUPPRESS) @@ -159,7 +159,7 @@ index 8b3332a461..fa5dc65bd1 100755 parser.add_argument('--clang-cl', action='store', dest='clang_cl', -@@ -1899,6 +1904,9 @@ def configure_node_cctest_sources(o): +@@ -1901,6 +1906,9 @@ def configure_node_cctest_sources(o): SearchFiles('test/cctest', 'h') def configure_node(o): diff --git a/patches/build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch b/patches/build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch index 7f6a8a4be3..3b7967d715 100644 --- a/patches/build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch +++ b/patches/build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch @@ -5,26 +5,29 @@ Subject: [PATCH] build: mobile Temporal Rust toolsets and libffi iOS callbacks Separate native host tools from Android/iOS Rust archives and use libffi's precompiled executable trampoline table for iOS callbacks. Full builds -retain ICU and enable Temporal; lite keeps its existing no-ICU policy. +retain complete ICU and enable Temporal; lite retains the danmu ICU profile +and disables Temporal and FFI. Preserve upstream's Temporal zoneinfo generator. Signed-off-by: fogtape Assisted-by: Codex --- - deps/crates/crates.gyp | 215 +++++++++++++------------------- + deps/crates/crates.gyp | 261 ++++++++++++-------------------- deps/libffi/generate-headers.py | 19 ++- - deps/libffi/libffi.gyp | 70 ++++++++--- - 3 files changed, 153 insertions(+), 151 deletions(-) + deps/libffi/libffi.gyp | 70 ++++++--- + 3 files changed, 166 insertions(+), 184 deletions(-) diff --git a/deps/crates/crates.gyp b/deps/crates/crates.gyp -index 0720ee6dcf..9ebf172222 100644 +index 216b0cff51..12c1718b6d 100644 --- a/deps/crates/crates.gyp +++ b/deps/crates/crates.gyp -@@ -1,129 +1,86 @@ +@@ -1,162 +1,99 @@ -{ - 'variables': { - 'cargo%': 'cargo', - 'cargo_vendor_dir': './vendor', - 'temporal_capi_dir': 'temporal_capi-v0_2', +- 'zoneinfo64_dir': 'zoneinfo64-v0_3', +- 'temporal_zoneinfo64_data_output': '<(SHARED_INTERMEDIATE_DIR)/src/builtins/builtins-temporal-zoneinfo64-data.cc', - 'cargo_rust_target%': '', - }, - 'conditions': [ @@ -147,6 +150,37 @@ index 0720ee6dcf..9ebf172222 100644 - ], - }, - }, +- { +- # Bakes zoneinfo64.res into a C++ source for Temporal when ICU is not +- # available to load it at runtime. +- 'target_name': 'temporal_zoneinfo64_data', +- 'type': 'none', +- 'toolsets': ['host', 'target'], +- 'hard_dependency': 1, +- 'direct_dependent_settings': { +- 'sources': [ +- '<(temporal_zoneinfo64_data_output)', +- ], +- }, +- 'actions': [ +- { +- 'action_name': 'make_temporal_zoneinfo_cpp', +- 'inputs': [ +- '../v8/tools/include-file-as-bytes.py', +- '<(cargo_vendor_dir)/<(zoneinfo64_dir)/src/data/zoneinfo64.res', +- ], +- 'outputs': [ +- '<(temporal_zoneinfo64_data_output)', +- ], +- 'action': [ +- '<(python)', +- '<@(_inputs)', +- '<@(_outputs)', +- 'zoneinfo64_static_data', +- ], +- }, +- ], +- }, - ] -} +# Mobile Rust actions select their triple per toolset; desktop builds retain @@ -154,12 +188,13 @@ index 0720ee6dcf..9ebf172222 100644 +{'variables': {'cargo%': 'cargo', + 'cargo_vendor_dir': './vendor', + 'temporal_capi_dir': 'temporal_capi-v0_2', -+ 'cargo_rust_target%': ''}, ++ 'cargo_rust_target%': '', ++ 'zoneinfo64_dir': 'zoneinfo64-v0_3', ++ 'temporal_zoneinfo64_data_output': '<(SHARED_INTERMEDIATE_DIR)/src/builtins/builtins-temporal-zoneinfo64-data.cc'}, + 'conditions': [['build_type == "Release"', + {'variables': {'cargo_build_flags': ['--release']}, + 'conditions': [['cargo_rust_target!=""', -+ {'variables': {'cargo_build_flags': ['--target', -+ '<(cargo_rust_target)']}}], ++ {'variables': {'cargo_build_flags': ['--target', '<(cargo_rust_target)']}}], + ['OS=="win"', + {'variables': {'node_crates_libpath': '<(SHARED_INTERMEDIATE_DIR)/$(Platform)/release/node_crates.lib'}}, + {'variables': {'node_crates_libpath': '<(SHARED_INTERMEDIATE_DIR)/<(cargo_rust_target)/release/<(STATIC_LIB_PREFIX)node_crates<(STATIC_LIB_SUFFIX)'}}]]}, @@ -173,8 +208,7 @@ index 0720ee6dcf..9ebf172222 100644 + 'hard_dependency': 1, + 'sources': ['Cargo.toml', 'Cargo.lock', 'src/lib.rs'], + 'link_settings': {'libraries': ['<(node_crates_libpath)'], -+ 'conditions': [['OS=="win"', -+ {'libraries': ['-lntdll', '-luserenv']}]]}, ++ 'conditions': [['OS=="win"', {'libraries': ['-lntdll', '-luserenv']}]]}, + 'conditions': [['OS in "android ios"', + {'link_settings': {'libraries!': ['<(node_crates_libpath)'], + 'libraries': ['<(SHARED_INTERMEDIATE_DIR)/mobile-rust/<(_toolset)/libnode_crates.a'], @@ -234,7 +268,20 @@ index 0720ee6dcf..9ebf172222 100644 + 'toolsets': ['host', 'target'], + 'sources': [], + 'dependencies': ['node_crates'], -+ 'direct_dependent_settings': {'include_dirs': ['<(cargo_vendor_dir)/<(temporal_capi_dir)/bindings/cpp']}}]} ++ 'direct_dependent_settings': {'include_dirs': ['<(cargo_vendor_dir)/<(temporal_capi_dir)/bindings/cpp']}}, ++ {'target_name': 'temporal_zoneinfo64_data', ++ 'type': 'none', ++ 'toolsets': ['host', 'target'], ++ 'hard_dependency': 1, ++ 'direct_dependent_settings': {'sources': ['<(temporal_zoneinfo64_data_output)']}, ++ 'actions': [{'action_name': 'make_temporal_zoneinfo_cpp', ++ 'inputs': ['../v8/tools/include-file-as-bytes.py', ++ '<(cargo_vendor_dir)/<(zoneinfo64_dir)/src/data/zoneinfo64.res'], ++ 'outputs': ['<(temporal_zoneinfo64_data_output)'], ++ 'action': ['<(python)', ++ '<@(_inputs)', ++ '<@(_outputs)', ++ 'zoneinfo64_static_data']}]}]} diff --git a/deps/libffi/generate-headers.py b/deps/libffi/generate-headers.py index fb58edf17b..5369c1e194 100644 --- a/deps/libffi/generate-headers.py diff --git a/patches/build-mobile-platform-settings-in-common.gypi.patch b/patches/build-mobile-platform-settings-in-common.gypi.patch index cddaa197ca..6e9e0f6483 100644 --- a/patches/build-mobile-platform-settings-in-common.gypi.patch +++ b/patches/build-mobile-platform-settings-in-common.gypi.patch @@ -23,10 +23,10 @@ Co-authored-by: João Matos 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/common.gypi b/common.gypi -index 71712d3084..a38421b5a5 100644 +index de80a68f49..fd3394b5cb 100644 --- a/common.gypi +++ b/common.gypi -@@ -110,7 +110,7 @@ +@@ -111,7 +111,7 @@ 'obj_dir%': '<(PRODUCT_DIR)/obj.target', 'v8_base': '<(PRODUCT_DIR)/obj.target/tools/v8_gypfiles/libv8_snapshot.a', }], @@ -35,7 +35,7 @@ index 71712d3084..a38421b5a5 100644 'obj_dir%': '<(PRODUCT_DIR)/obj.target', 'v8_base': '<(PRODUCT_DIR)/libv8_snapshot.a', }], -@@ -221,6 +221,23 @@ +@@ -219,11 +219,28 @@ }], ], }], @@ -59,34 +59,31 @@ index 71712d3084..a38421b5a5 100644 ['OS=="solaris"', { # pull in V8's postmortem metadata 'ldflags': [ '-Wl,-z,allextract' ] -@@ -229,7 +246,7 @@ - # increase performance, number from experimentation - 'cflags': [ '-qINLINE=::150:100000' ] }], -- ['OS!="mac" and OS!="win" and OS!="zos"', { -+ ['OS!="mac" and OS!="ios" and OS!="win" and OS!="zos"', { +- ['OS!="mac" and OS!="win"', { ++ ['OS!="mac" and OS!="ios" and OS!="win"', { # -fno-omit-frame-pointer is necessary for the --perf_basic_prof # flag to work correctly. perf(1) gets confused about JS stack # frames otherwise, even with --call-graph dwarf. -@@ -455,7 +472,7 @@ +@@ -477,7 +494,7 @@ [ 'target_arch=="arm64"', { 'msvs_configuration_platform': 'arm64', }], -- ['asan == 1 and OS != "mac" and OS != "zos"', { -+ ['asan == 1 and OS != "mac" and OS != "ios" and OS != "zos"', { +- ['asan == 1 and OS != "mac"', { ++ ['asan == 1 and OS != "mac" and OS != "ios"', { 'cflags+': [ '-fno-omit-frame-pointer', '-fsanitize=address', -@@ -483,7 +500,7 @@ +@@ -505,7 +522,7 @@ }], ], }], -- ['ubsan == 1 and OS != "mac" and OS != "zos"', { -+ ['ubsan == 1 and OS != "mac" and OS != "ios" and OS != "zos"', { +- ['ubsan == 1 and OS != "mac"', { ++ ['ubsan == 1 and OS != "mac" and OS != "ios"', { 'cflags+': [ '-fno-omit-frame-pointer', '-fsanitize=undefined', -@@ -492,7 +509,7 @@ +@@ -514,7 +531,7 @@ 'cflags!': [ '-fno-omit-frame-pointer' ], 'ldflags': [ '-fsanitize=undefined' ], }], @@ -95,7 +92,7 @@ index 71712d3084..a38421b5a5 100644 'xcode_settings': { 'OTHER_CFLAGS+': [ '-fno-omit-frame-pointer', -@@ -591,11 +608,13 @@ +@@ -616,11 +633,13 @@ }], ['_toolset=="host"', { 'conditions': [ @@ -111,7 +108,7 @@ index 71712d3084..a38421b5a5 100644 'cflags': [ '-m64' ], 'ldflags': [ '-m64' ], }], -@@ -777,6 +796,98 @@ +@@ -802,6 +821,98 @@ }], ], }], diff --git a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch index 51c4f1854c..2f72b2f2e5 100644 --- a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch +++ b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch @@ -27,7 +27,7 @@ Assisted-by: Codex 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/node.gyp b/node.gyp -index 2cf1e62b56..010117fb97 100644 +index 587df5cace..c1d6e6735f 100644 --- a/node.gyp +++ b/node.gyp @@ -77,6 +77,11 @@ @@ -42,7 +42,7 @@ index 2cf1e62b56..010117fb97 100644 '<@(node_builtin_shareable_builtins)', ], 'node_sources': [ -@@ -583,6 +588,11 @@ +@@ -580,6 +585,11 @@ ['OS=="linux" and clang==1', { 'libraries': ['-latomic'], }], @@ -54,7 +54,7 @@ index 2cf1e62b56..010117fb97 100644 ], }, -@@ -1434,8 +1444,9 @@ +@@ -1427,8 +1437,9 @@ ['OS=="solaris"', { 'ldflags': [ '-I<(SHARED_INTERMEDIATE_DIR)' ] }], @@ -66,7 +66,7 @@ index 2cf1e62b56..010117fb97 100644 'type': 'none', }], [ 'node_shared=="true"', { -@@ -1659,7 +1670,8 @@ +@@ -1652,7 +1663,8 @@ [ 'node_shared_libuv=="false"', { 'dependencies': [ 'deps/uv/uv.gyp:libuv#host' ], }], @@ -77,7 +77,7 @@ index 2cf1e62b56..010117fb97 100644 }], [ 'debug_node=="true"', { diff --git a/node.gypi b/node.gypi -index b382784e61..75d619dbeb 100644 +index 9eb972adc4..67d8100fa5 100644 --- a/node.gypi +++ b/node.gypi @@ -280,6 +280,11 @@ @@ -120,9 +120,9 @@ index b382784e61..75d619dbeb 100644 # -force_load or --whole-archive are not applicable for # the static library [ 'force_load=="true"', { -@@ -487,5 +496,10 @@ - }, { - 'defines': [ 'HAVE_QUIC=0' ] +@@ -484,5 +493,10 @@ + }], + ], }], + [ 'OS=="android" or OS=="ios"', { + 'defines': [ diff --git a/patches/build-v8-mobile-host-target-settings-in-v8-gypfiles.patch b/patches/build-v8-mobile-host-target-settings-in-v8-gypfiles.patch index 1f9a9ab2eb..ff2436e85f 100644 --- a/patches/build-v8-mobile-host-target-settings-in-v8-gypfiles.patch +++ b/patches/build-v8-mobile-host-target-settings-in-v8-gypfiles.patch @@ -46,7 +46,7 @@ index 3676848df8..61af0a4044 100644 'configurations': { 'Debug': { diff --git a/tools/v8_gypfiles/v8.gyp b/tools/v8_gypfiles/v8.gyp -index 093be6e539..664542b0d6 100644 +index fd8394b370..caba744b0a 100644 --- a/tools/v8_gypfiles/v8.gyp +++ b/tools/v8_gypfiles/v8.gyp @@ -906,6 +906,15 @@ @@ -112,7 +112,7 @@ index 093be6e539..664542b0d6 100644 ['v8_enable_turbofan==1', { 'dependencies': ['v8_compiler_sources'], }, { -@@ -1263,7 +1274,7 @@ +@@ -1268,7 +1279,7 @@ 'conditions': [ ['v8_enable_webassembly==1', { 'conditions': [ @@ -121,7 +121,7 @@ index 093be6e539..664542b0d6 100644 'sources': [ '<(V8_ROOT)/src/trap-handler/handler-inside-posix.cc', '<(V8_ROOT)/src/trap-handler/handler-outside-posix.cc', -@@ -1406,7 +1417,8 @@ +@@ -1411,7 +1422,8 @@ # Platforms that don't have Compare-And-Swap (CAS) support need to link atomic library # to implement atomic memory access. # Clang needs it for some atomic operations (https://clang.llvm.org/docs/Toolchain.html#atomics-library). @@ -131,7 +131,7 @@ index 093be6e539..664542b0d6 100644 'link_settings': { 'libraries': ['-latomic', ], }, -@@ -1560,6 +1572,8 @@ +@@ -1565,6 +1577,8 @@ '<(V8_ROOT)/src/base/platform/platform-posix.h', '<(V8_ROOT)/src/base/platform/platform-posix-time.cc', '<(V8_ROOT)/src/base/platform/platform-posix-time.h', @@ -140,7 +140,7 @@ index 093be6e539..664542b0d6 100644 ], 'link_settings': { 'target_conditions': [ -@@ -1572,9 +1586,18 @@ +@@ -1577,9 +1591,18 @@ }, 'target_conditions': [ ['_toolset=="host"', { @@ -162,7 +162,7 @@ index 093be6e539..664542b0d6 100644 ], }, { 'sources': [ -@@ -2183,12 +2206,14 @@ +@@ -2188,12 +2211,14 @@ ], }, { # 'OS!="win"' 'conditions': [ @@ -179,7 +179,7 @@ index 093be6e539..664542b0d6 100644 'sources': [ '<(V8_ROOT)/src/heap/base/asm/ia32/push_registers_asm.cc', ], -@@ -2406,6 +2431,46 @@ +@@ -2411,6 +2436,46 @@ }, }, # postmortem-metadata @@ -226,7 +226,7 @@ index 093be6e539..664542b0d6 100644 { 'target_name': 'v8_zlib', 'type': 'static_library', -@@ -2420,6 +2485,10 @@ +@@ -2425,6 +2490,10 @@ }] ] }], diff --git a/patches/ci-remove-upstream-only-workflows-and-config.patch b/patches/ci-remove-upstream-only-workflows-and-config.patch index 87b5d00790..b30c929ea4 100644 --- a/patches/ci-remove-upstream-only-workflows-and-config.patch +++ b/patches/ci-remove-upstream-only-workflows-and-config.patch @@ -22,12 +22,12 @@ is read only from the default branch and is left untouched. --- .github/ISSUE_TEMPLATE/config.yml | 14 - .github/SUPPORT.md | 27 -- - .github/label-pr-config.yml | 224 ----------- + .github/label-pr-config.yml | 236 ----------- .github/workflows/author-ready-conflicts.yml | 48 --- - .github/workflows/auto-start-ci.yml | 80 ---- - .github/workflows/benchmark.yml | 279 -------------- - .github/workflows/build-shared.yml | 96 ----- - .github/workflows/build-tarball.yml | 132 ------- + .github/workflows/auto-start-ci.yml | 93 ----- + .github/workflows/benchmark.yml | 271 ------------- + .github/workflows/build-shared.yml | 93 ----- + .github/workflows/build-tarball.yml | 137 ------- .github/workflows/close-stalled.yml | 36 -- .github/workflows/codeql.yml | 44 --- .github/workflows/comment-labeled.yml | 57 --- @@ -36,7 +36,7 @@ is read only from the default branch and is left untouched. .github/workflows/contributor-guidance.yml | 141 ------- .../workflows/coverage-linux-without-intl.yml | 90 ----- .github/workflows/coverage-linux.yml | 90 ----- - .github/workflows/coverage-windows.yml | 106 ------ + .github/workflows/coverage-windows.yml | 106 ----- .github/workflows/create-release-proposal.yml | 81 ---- .github/workflows/daily-wpt-fyi.yml | 163 -------- .github/workflows/daily.yml | 30 -- @@ -47,11 +47,11 @@ is read only from the default branch and is left untouched. .github/workflows/label-pr.yml | 18 - .github/workflows/license-builder.yml | 38 -- .github/workflows/lint-release-proposal.yml | 119 ------ - .github/workflows/linters.yml | 255 ------------- + .github/workflows/linters.yml | 278 ------------- .github/workflows/major-release.yml | 49 --- .github/workflows/nix-changes-comment.yml | 98 ----- - .github/workflows/nix-changes.yml | 122 ------ - .github/workflows/notify-on-push.yml | 64 ---- + .github/workflows/nix-changes.yml | 128 ------ + .github/workflows/notify-on-push.yml | 64 --- .github/workflows/notify-on-review-wanted.yml | 45 --- .github/workflows/post-release.yml | 33 -- .../remark-lint-problem-matcher.json | 22 -- @@ -59,17 +59,17 @@ is read only from the default branch and is left untouched. .github/workflows/stale.yml | 51 --- .github/workflows/stress-test.yml | 114 ------ .github/workflows/test-internet.yml | 70 ---- - .github/workflows/test-linux-perfetto.yml | 70 ---- + .github/workflows/test-linux-perfetto.yml | 73 ---- .github/workflows/test-linux-quic.yml | 80 ---- .github/workflows/test-linux.yml | 100 ----- .github/workflows/test-macos.yml | 142 ------- - .github/workflows/test-shared.yml | 274 -------------- + .github/workflows/test-shared.yml | 273 ------------- .github/workflows/timezone-update.yml | 74 ---- - .github/workflows/tools.yml | 355 ------------------ - .github/workflows/update-openssl.yml | 61 --- + .github/workflows/tools.yml | 365 ------------------ + .github/workflows/update-openssl.yml | 69 ---- .github/workflows/update-v8.yml | 57 --- .github/workflows/update-wpt.yml | 95 ----- - 49 files changed, 4690 deletions(-) + 49 files changed, 4758 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/SUPPORT.md delete mode 100644 .github/label-pr-config.yml @@ -175,10 +175,10 @@ index 4613512bf2..0000000000 -your expectations. diff --git a/.github/label-pr-config.yml b/.github/label-pr-config.yml deleted file mode 100644 -index 8b36066473..0000000000 +index f30b6b8a5e..0000000000 --- a/.github/label-pr-config.yml +++ /dev/null -@@ -1,224 +0,0 @@ +@@ -1,236 +0,0 @@ -## Order of entries in this map *does* matter for the resolved labels -## earlier entries override later entries -subSystemLabels: @@ -196,6 +196,7 @@ index 8b36066473..0000000000 - /^src\/uv\./: c++, libuv - /^src\/(?:connect(?:ion)?|pipe|tcp)_/: c++, net - /^src\/node_os/: c++, os +- /^src\/permission\//: c++, permission - /^src\/(?:node_main|signal_)/: c++, process - /^src\/timer[_s]/: c++, timers - /^src\/node_root_certs/: c++, tls @@ -283,6 +284,7 @@ index 8b36066473..0000000000 - /^lib\/test.js$/: test_runner - /^lib\/v8.js$/: v8 module - /^lib\/internal\/url\.js$/: whatwg-url +- /^lib\/internal\/process\/permission\.js$/: permission - /^lib\/internal\/modules\/esm/: esm - /^lib\/internal\/modules/: module - /^lib\/internal\/source_map/: source maps @@ -297,6 +299,13 @@ index 8b36066473..0000000000 - -exlusiveLabels: - # More specific tests +- /^test\/(?:parallel|sequential)\/test-(?:[^/]+-)?permission(?:-|\.)/: test, permission +- /^test\/ffi\/test-ffi-permissions?(?:-|\.)/: test, permission +- /^test\/addons\/no-addons\/permission\.js$/: test, addons, permission +- /^test\/es-module\/test-[^/]*-permission(?:-|\.)/: test, esm, permission +- /^test\/module-hooks\/test-[^/]*-permission(?:-|\.)/: test, module, loaders, permission +- /^test\/fixtures\/permission\//: test, permission +- /^test\/fixtures\/rc\/permission-namespace\.json$/: test, permission - /^test\/addons\//: test, addons - /^test\/debugger/: test, debugger - /^test\/doctool\//: test, doc, tools @@ -331,6 +340,7 @@ index 8b36066473..0000000000 - # test_runner documentation - /^doc\/api\/test.md$/: doc, test_runner - # Automatically tag JS subsystem-specific API doc changes +- /^doc\/api\/permissions\.md$/: doc, permission - /^doc\/api\/(\w+)\.md$/: doc, $1 - # Add deprecations label to PRs that affect doc/api/deprecations.md - /^doc\/api\/deprecations.md$/: doc, deprecations @@ -343,6 +353,7 @@ index 8b36066473..0000000000 - /^doc\//: doc - - # More specific benchmarks +- /^benchmark\/fs\/readfile-permission-enabled\.js$/: benchmark, fs, permission - /^benchmark\/buffers\//: benchmark, buffer - /^benchmark\/es\//: benchmark, v8 engine - /^benchmark\/_http/: benchmark, http @@ -379,6 +390,7 @@ index 8b36066473..0000000000 - - os - - path - - perf_hooks +- - permission - - process - - punycode - - querystring @@ -459,13 +471,16 @@ index f447c6b80a..0000000000 - done diff --git a/.github/workflows/auto-start-ci.yml b/.github/workflows/auto-start-ci.yml deleted file mode 100644 -index dc827d62ba..0000000000 +index 85f3e92f2f..0000000000 --- a/.github/workflows/auto-start-ci.yml +++ /dev/null -@@ -1,80 +0,0 @@ +@@ -1,93 +0,0 @@ -# This action uses the following secrets: -# JENKINS_USER: GitHub user whose Jenkins token is defined below --# JENKINS_TOKEN: Jenkins token, to be used to start CI +-# JENKINS_TOKEN: Jenkins token, to be used to start or resume CI +-# Optional repository variables: +-# AUTO_START_CI_BATCH_SIZE: maximum PRs to process per run (default: 5) +-# AUTO_START_CI_MAX_WORKLOAD: pause at this many running or queued PR jobs (default: 15) -name: Auto Start CI - -on: @@ -495,16 +510,25 @@ index dc827d62ba..0000000000 - steps: - - name: Get Pull Requests - id: get_prs_for_ci -- run: > -- echo "numbers=$(gh pr list \ +- # Explicit bash enables pipefail so jq cannot hide a failed gh query. +- shell: bash +- run: | +- if ! [[ "$BATCH_SIZE" =~ ^[1-9][0-9]*$ ]]; then +- echo '::error::AUTO_START_CI_BATCH_SIZE must be a strictly positive integer' +- exit 1 +- fi +- # Filter before selecting the batch; UNKNOWN mergeability is retried later. +- numbers=$(gh pr list \ - --repo "$GITHUB_REPOSITORY" \ -- --label 'request-ci' \ -- --json 'number' \ -- --search 'review:approved' \ -- -t '{{ range . }}{{ .number }} {{ end }}' \ -- --limit 5)" >> "$GITHUB_OUTPUT" +- --json 'number,mergeable' \ +- --search 'review:approved label:request-ci,resume-ci' \ +- --limit 100 \ +- | jq --argjson limit "$BATCH_SIZE" \ +- '[.[] | select(.mergeable == "MERGEABLE") | .number] | .[:$limit] | map(tostring) | join(" ")' -r) +- echo "numbers=$numbers" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ github.token }} +- BATCH_SIZE: ${{ vars.AUTO_START_CI_BATCH_SIZE || '5' }} - start-ci: - permissions: - checks: read @@ -535,7 +559,7 @@ index dc827d62ba..0000000000 - GH_TOKEN: ${{ github.token }} - JENKINS_TOKEN: ${{ secrets.JENKINS_TOKEN }} - -- - name: Start the CI +- - name: Start or resume the CI - run: | - read -r -a numbers <<< "$PULL_REQUESTS" - curl -fsSL "https://github.com/${GITHUB_REPOSITORY}/raw/${GITHUB_SHA}/tools/actions/start-ci.sh" \ @@ -543,12 +567,13 @@ index dc827d62ba..0000000000 - env: - GH_TOKEN: ${{ github.token }} - PULL_REQUESTS: ${{ needs.get-prs-for-ci.outputs.numbers }} +- MAX_WORKLOAD: ${{ vars.AUTO_START_CI_MAX_WORKLOAD || '15' }} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 -index 92bab7856b..0000000000 +index 0e901de78d..0000000000 --- a/.github/workflows/benchmark.yml +++ /dev/null -@@ -1,279 +0,0 @@ +@@ -1,271 +0,0 @@ -name: Benchmark - -on: @@ -672,15 +697,11 @@ index 92bab7856b..0000000000 - --pure --keep TAR_DIR --keep FLAKY_TESTS \ - --keep SCCACHE_GHA_ENABLED --keep SCCACHE_GHA_RW_MODE \ - --keep ACTIONS_CACHE_SERVICE_V2 --keep ACTIONS_RESULTS_URL --keep ACTIONS_RUNTIME_TOKEN \ -- --arg useSeparateDerivationForV8 true \ - --arg withPerfetto ${{ matrix.perfetto || false }} \ -- --arg loadJSBuiltinsDynamically false \ - --arg ccache '(import {}).sccache' \ -- --arg devTools '[]' \ -- --arg benchmarkTools '[]' \ - --run ' - make build-ci -j4 V=1 && sccache --stop-server -- ' +- ' tools/nix/ci-shell.nix - mv out/Release/node base_node - - - name: Checkout the merge commit @@ -692,15 +713,11 @@ index 92bab7856b..0000000000 - nix-shell \ - -I nixpkgs=./tools/nix/pkgs.nix \ - --pure --keep SCCACHE_LOCAL_RW_MODE \ -- --arg useSeparateDerivationForV8 true \ - --arg withPerfetto ${{ matrix.perfetto || false }} \ -- --arg loadJSBuiltinsDynamically false \ - --arg ccache '(import {}).sccache' \ -- --arg devTools '[]' \ -- --arg benchmarkTools '[]' \ - --run ' - make build-ci -j4 V=1 -- ' +- ' tools/nix/ci-shell.nix - env: - SCCACHE_LOCAL_RW_MODE: READ_ONLY - @@ -783,7 +800,7 @@ index 92bab7856b..0000000000 - nix-shell \ - -I nixpkgs=./tools/nix/pkgs.nix \ - --pure \ -- -E '(import {}).mkShell { buildInputs = import ./tools/nix/benchmarkTools.nix { withHttpBenchmarkDeps = false; }; }' \ +- -E '(import {}).mkShellNoCC { buildInputs = import ./tools/nix/benchmarkTools.nix { withHttpBenchmarkDeps = false; }; }' \ - --run ' - export LC_ALL=C.UTF-8 - echo "> [!WARNING] " @@ -830,10 +847,10 @@ index 92bab7856b..0000000000 - PR_ID: ${{ inputs.pr_id }} diff --git a/.github/workflows/build-shared.yml b/.github/workflows/build-shared.yml deleted file mode 100644 -index 7d77c13e56..0000000000 +index 73f7ec6afb..0000000000 --- a/.github/workflows/build-shared.yml +++ /dev/null -@@ -1,96 +0,0 @@ +@@ -1,93 +0,0 @@ -name: Build Node.js (shared libraries) - -on: @@ -921,21 +938,18 @@ index 7d77c13e56..0000000000 - -I "nixpkgs=$TAR_DIR/tools/nix/pkgs.nix" \ - --pure --keep TAR_DIR --keep FLAKY_TESTS \ - --keep SCCACHE_GHA_ENABLED --keep ACTIONS_CACHE_SERVICE_V2 --keep ACTIONS_RESULTS_URL --keep ACTIONS_RUNTIME_TOKEN \ -- --arg loadJSBuiltinsDynamically false \ - --arg ccache "${NIX_SCCACHE:-null}" \ -- --arg devTools '[]' \ -- --arg benchmarkTools '[]' \ - --arg pkcs11 ${{ inputs.pkcs11-store-test }} \ - ${{ inputs.extra-nix-flags }} \ - --run ' - make -C "$TAR_DIR" run-ci -j4 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9 --skip-tests=$CI_SKIP_TESTS" -- ' "$TAR_DIR/shell.nix" +- ' "$TAR_DIR/tools/nix/ci-shell.nix" diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml deleted file mode 100644 -index 045bd3ae93..0000000000 +index 53e625ef88..0000000000 --- a/.github/workflows/build-tarball.yml +++ /dev/null -@@ -1,132 +0,0 @@ +@@ -1,137 +0,0 @@ -name: Build from tarball - -on: @@ -1005,6 +1019,7 @@ index 045bd3ae93..0000000000 - PYTHON_VERSION: '3.14' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' +- NODE_VERSION: lts/* - -permissions: - contents: read @@ -1022,12 +1037,16 @@ index 045bd3ae93..0000000000 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true +- - name: Use Node.js ${{ env.NODE_VERSION }} +- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 +- with: +- node-version: ${{ env.NODE_VERSION }} - - name: Make tarball - run: | - export DISTTYPE=nightly - export DATESTRING=$(date "+%Y-%m-%d") - export COMMIT=$(git rev-parse --short=10 "$GITHUB_SHA") -- ./configure && make tar -j4 SKIP_XZ=1 +- ./configure && make tar -j4 SKIP_XZ=1 GZIP_COMPRESSION=6 - - name: Upload tarball artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: @@ -1112,7 +1131,7 @@ index 3c7ab04339..0000000000 - days-before-stale: -1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 -index 1b3179ef4a..0000000000 +index d8eca1f2b7..0000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,44 +0,0 @@ @@ -1148,16 +1167,16 @@ index 1b3179ef4a..0000000000 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL -- uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 +- uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 - with: - languages: ${{ matrix.language }} - config-file: ./.github/codeql-config.yml - - - name: Autobuild -- uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 +- uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 - - - name: Perform CodeQL Analysis -- uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 +- uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 - with: - category: /language:${{matrix.language}} diff --git a/.github/workflows/comment-labeled.yml b/.github/workflows/comment-labeled.yml @@ -1643,7 +1662,7 @@ index 4035ea9320..0000000000 - gh pr comment "$NUMBER" --repo "$GITHUB_REPOSITORY" --body-file - diff --git a/.github/workflows/coverage-linux-without-intl.yml b/.github/workflows/coverage-linux-without-intl.yml deleted file mode 100644 -index a98492ec66..0000000000 +index 9b8f8e2fd8..0000000000 --- a/.github/workflows/coverage-linux-without-intl.yml +++ /dev/null @@ -1,90 +0,0 @@ @@ -1734,12 +1753,12 @@ index a98492ec66..0000000000 - - name: Clean tmp - run: rm -rf coverage/tmp && rm -rf out - - name: Upload -- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 +- uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0 - with: - directory: ./coverage diff --git a/.github/workflows/coverage-linux.yml b/.github/workflows/coverage-linux.yml deleted file mode 100644 -index 4ba373b790..0000000000 +index 0c7fc292d3..0000000000 --- a/.github/workflows/coverage-linux.yml +++ /dev/null @@ -1,90 +0,0 @@ @@ -1830,12 +1849,12 @@ index 4ba373b790..0000000000 - - name: Clean tmp - run: rm -rf coverage/tmp && rm -rf out - - name: Upload -- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 +- uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0 - with: - directory: ./coverage diff --git a/.github/workflows/coverage-windows.yml b/.github/workflows/coverage-windows.yml deleted file mode 100644 -index 5da225b171..0000000000 +index a4db4ccc08..0000000000 --- a/.github/workflows/coverage-windows.yml +++ /dev/null @@ -1,106 +0,0 @@ @@ -1942,7 +1961,7 @@ index 5da225b171..0000000000 - - name: Clean tmp - run: npx rimraf ./coverage/tmp - - name: Upload -- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 +- uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0 - with: - directory: ./coverage diff --git a/.github/workflows/create-release-proposal.yml b/.github/workflows/create-release-proposal.yml @@ -2662,10 +2681,10 @@ index 027aee336a..0000000000 - GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml deleted file mode 100644 -index 848fffc117..0000000000 +index 9afdb2ef5a..0000000000 --- a/.github/workflows/linters.yml +++ /dev/null -@@ -1,255 +0,0 @@ +@@ -1,278 +0,0 @@ -name: Linters - -on: @@ -2709,6 +2728,12 @@ index 848fffc117..0000000000 - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false +- # Skip large vendored trees that lint-cpp does not use. +- sparse-checkout: | +- /* +- !/deps/openssl/ +- !/deps/v8/ +- sparse-checkout-cone-mode: false - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: @@ -2722,7 +2747,8 @@ index 848fffc117..0000000000 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: -- fetch-depth: 0 +- # The first parent of the PR merge commit is the base branch commit. +- fetch-depth: 2 - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -2740,7 +2766,7 @@ index 848fffc117..0000000000 - # because it is irrelevant. We already check if the formatter produced - # a diff in the next line. - # Refs: https://github.com/nodejs/node/pull/42764 -- CLANG_FORMAT_START="$(git merge-base HEAD refs/remotes/origin/$GITHUB_BASE_REF)" \ +- CLANG_FORMAT_START=HEAD^ \ - make format-cpp || true - git --no-pager diff --exit-code && EXIT_CODE="$?" || EXIT_CODE="$?" - if [ "$EXIT_CODE" != "0" ] @@ -2761,13 +2787,29 @@ index 848fffc117..0000000000 - with: - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} +- id: setup-node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.NODE_VERSION }} +- cache: npm +- cache-dependency-path: | +- tools/doc/package-lock.json +- tools/eslint/package-lock.json +- tools/lint-md/package-lock.json +- - name: Cache ESLint results +- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 +- env: +- # ESLint only invalidates its cache on config changes, so the key also +- # covers the custom rules and the files they read. +- cache-prefix: eslint-${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('eslint.config.mjs', '*/eslint.config_partial.mjs', 'tools/eslint/*', 'tools/eslint-rules/**', 'doc/api/errors.md', 'doc/api/deprecations.md') }} +- with: +- path: .eslintcache +- key: ${{ env.cache-prefix }}-${{ github.sha }} +- restore-keys: ${{ env.cache-prefix }}- - - name: Lint JavaScript files - run: | - set +e -- NODE=$(command -v node) make lint-js +- NODE=$(command -v node) make lint-js ESLINT_CACHE_FLAGS='--cache --cache-strategy content' - EXIT_CODE="$?" - if [ "$EXIT_CODE" != "0" ]; then - echo @@ -2797,7 +2839,7 @@ index 848fffc117..0000000000 - - name: Lint markdown files - run: | - echo "::add-matcher::.github/workflows/remark-lint-problem-matcher.json" -- NODE=$(command -v node) make lint-md +- NODE=$(command -v node) make lint-md ESLINT_CACHE_FLAGS='--cache --cache-strategy content' - env: - NODE_RELEASED_VERSIONS: ${{ steps.get-released-versions.outputs.NODE_RELEASED_VERSIONS }} - @@ -3082,10 +3124,10 @@ index db96ff4a09..0000000000 - PR_NUMBER: ${{ steps.pr-data.outputs.NUMBER }} diff --git a/.github/workflows/nix-changes.yml b/.github/workflows/nix-changes.yml deleted file mode 100644 -index 8aeaf2c0e4..0000000000 +index c80e9e53f5..0000000000 --- a/.github/workflows/nix-changes.yml +++ /dev/null -@@ -1,122 +0,0 @@ +@@ -1,128 +0,0 @@ -# This action uses the following secrets: -# CACHIX_AUTH_TOKEN: Write access to nodejs.cachix.org – without it, the cache is read-only. -name: Nix files edited @@ -3156,10 +3198,16 @@ index 8aeaf2c0e4..0000000000 - - - name: Compute requisites before change - run: | -- git reset HEAD^ --hard -- # TODO(aduh95): remove this once list-requisites.sh has reached `main` -- [ -f tools/nix/list-requisites.sh ] || git checkout FETCH_HEAD -- tools/nix/list-requisites.sh +- BASE_SHA="$(git rev-parse HEAD^)" +- if [ -n "$PUSH_BEFORE_SHA" ] && [ "$PUSH_BEFORE_SHA" != "$BASE_SHA" ] \ +- && [ "$PUSH_BEFORE_SHA" != "0000000000000000000000000000000000000000" ] \ +- && git fetch --no-tags --depth=1 origin "$PUSH_BEFORE_SHA"; then +- BASE_SHA="$PUSH_BEFORE_SHA" +- fi +- git reset "$BASE_SHA" --hard - ./tools/nix/list-requisites.sh > requisites-${{ matrix.system }}-before.list +- env: +- PUSH_BEFORE_SHA: ${{ github.event_name == 'push' && github.event.before || '' }} - - - name: Output diff - run: | @@ -3398,7 +3446,7 @@ index cfb281310a..0000000000 -} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml deleted file mode 100644 -index 4a7415bc90..0000000000 +index 2663019bcc..0000000000 --- a/.github/workflows/scorecard.yml +++ /dev/null @@ -1,81 +0,0 @@ @@ -3480,7 +3528,7 @@ index 4a7415bc90..0000000000 - - # Upload the results to GitHub's code scanning dashboard. - - name: Upload to code-scanning -- uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 +- uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 - with: - sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml @@ -3738,10 +3786,10 @@ index 43d71f5f31..0000000000 - run: make test-internet -j4 V=1; diff --git a/.github/workflows/test-linux-perfetto.yml b/.github/workflows/test-linux-perfetto.yml deleted file mode 100644 -index ce31704f81..0000000000 +index d2bfbfbd68..0000000000 --- a/.github/workflows/test-linux-perfetto.yml +++ /dev/null -@@ -1,70 +0,0 @@ +@@ -1,73 +0,0 @@ -name: Test Linux (with Perfetto) - -on: @@ -3806,6 +3854,9 @@ index ce31704f81..0000000000 - - name: Build - working-directory: node - run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn --v8-enable-temporal-support --with-perfetto" +- - name: Get trace_processor +- working-directory: node +- run: make tools/perfetto/trace_processor_shell - - name: Test - working-directory: node - run: make test-ci -j1 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9" @@ -4154,10 +4205,10 @@ index 6954112115..0000000000 - DIR: dir%20with $unusual"chars?'åß∂ƒ©∆¬…` diff --git a/.github/workflows/test-shared.yml b/.github/workflows/test-shared.yml deleted file mode 100644 -index 9f4d2e031f..0000000000 +index 8292ac253f..0000000000 --- a/.github/workflows/test-shared.yml +++ /dev/null -@@ -1,274 +0,0 @@ +@@ -1,273 +0,0 @@ -# This action uses the following secrets: -# CACHIX_AUTH_TOKEN: Write access to nodejs.cachix.org – without it, the cache is read-only. -name: Test Shared libraries @@ -4173,11 +4224,12 @@ index 9f4d2e031f..0000000000 - - android-patches/** - - benchmarks/** - - codecov.yml +- - deps/LIEF/** - - deps/ada/** - - deps/brotli/** - - deps/cares/** -- - deps/crates/** - - deps/corepack/** +- - deps/crates/** - - deps/googletest/** - - deps/histogram/** - - deps/icu-small/** @@ -4230,11 +4282,12 @@ index 9f4d2e031f..0000000000 - - android-patches/** - - benchmarks/** - - codecov.yml +- - deps/LIEF/** - - deps/ada/** - - deps/brotli/** - - deps/cares/** -- - deps/crates/** - - deps/corepack/** +- - deps/crates/** - - deps/googletest/** - - deps/histogram/** - - deps/icu-small/** @@ -4293,7 +4346,7 @@ index 9f4d2e031f..0000000000 - run: | - export DATESTRING=$(date "+%Y-%m-%d") - export COMMIT=$(git rev-parse --short=10 "$GITHUB_SHA") -- ./configure && make tar -j4 SKIP_XZ=1 SKIP_SHARED_DEPS=1 +- ./configure && make tar -j4 SKIP_XZ=1 SKIP_SHARED_DEPS=1 GZIP_COMPRESSION=6 - env: - DISTTYPE: nightly - @@ -4325,10 +4378,7 @@ index 9f4d2e031f..0000000000 - with: - runner: ${{ matrix.runner }} - with-sccache: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} -- extra-nix-flags: | -- --arg useSeparateDerivationForV8 true \ -- ${{ matrix.perfetto && '--arg withPerfetto true \' || '\' }} -- ${{ endsWith(matrix.system, '-darwin') && '--arg withAmaro false --arg withLief false --arg withSQLite false --arg withFFI false --arg extraConfigFlags ''["--without-inspector" "--without-node-options"]'' \' || '\' }} +- extra-nix-flags: ${{ matrix.perfetto && '--arg withPerfetto true' || '' }} - secrets: - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - @@ -4360,7 +4410,7 @@ index 9f4d2e031f..0000000000 - id: v8-drv - run: | - V8_DRV=$( -- nix-instantiate -E "builtins.filter (p: p.pname == ''v8'') (import $TAR_DIR/shell.nix { useSeparateDerivationForV8=true; }).buildInputs" +- nix-instantiate -E "builtins.filter (p: p.pname == ''v8'') (import $TAR_DIR/tools/nix/ci-shell.nix {}).buildInputs" - ) - V8_STORE_PATH=$(nix-store -q --outputs "$V8_DRV") - echo "V8_DRV=$V8_DRV" >> "$GITHUB_OUTPUT" @@ -4514,10 +4564,10 @@ index abe4516db4..0000000000 - update-pull-request-title-and-body: true diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml deleted file mode 100644 -index fe2db7ac95..0000000000 +index 497a5c1230..0000000000 --- a/.github/workflows/tools.yml +++ /dev/null -@@ -1,355 +0,0 @@ +@@ -1,365 +0,0 @@ -name: Tools and deps update -on: - schedule: @@ -4548,6 +4598,7 @@ index fe2db7ac95..0000000000 - - inspector_protocol - - libffi - - libuv +- - lief - - llhttp - - minimatch - - nbytes @@ -4670,6 +4721,7 @@ index fe2db7ac95..0000000000 - - id: icu - subsystem: deps - label: dependencies, test, icu +- requires-python: true - run: | - ./tools/dep_updaters/update-icu.sh > temp-output - cat temp-output @@ -4678,6 +4730,7 @@ index fe2db7ac95..0000000000 - - id: inspector_protocol - subsystem: deps - label: dependencies, inspector +- requires-python: true - run: | - ./tools/dep_updaters/update-inspector-protocol.sh > temp-output - cat temp-output @@ -4699,6 +4752,15 @@ index fe2db7ac95..0000000000 - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output +- - id: lief +- subsystem: deps +- label: dependencies, dont-land-on-v22.x +- requires-python: true +- run: | +- ./tools/dep_updaters/update-lief.sh > temp-output +- cat temp-output +- tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true +- rm temp-output - - id: llhttp - subsystem: deps - label: dependencies @@ -4841,9 +4903,7 @@ index fe2db7ac95..0000000000 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} -- if: | -- (matrix.id == 'icu' || matrix.id == 'inspector_protocol') && -- (github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id) +- if: matrix.requires-python && (github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id) - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: ${{ env.PYTHON_VERSION }} @@ -4875,10 +4935,10 @@ index fe2db7ac95..0000000000 - author: Node.js GitHub Bot diff --git a/.github/workflows/update-openssl.yml b/.github/workflows/update-openssl.yml deleted file mode 100644 -index 10eb3289a1..0000000000 +index b81a260312..0000000000 --- a/.github/workflows/update-openssl.yml +++ /dev/null -@@ -1,61 +0,0 @@ +@@ -1,69 +0,0 @@ -name: OpenSSL update -on: - schedule: @@ -4908,6 +4968,7 @@ index 10eb3289a1..0000000000 - env: - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} - - name: Create PR with first commit +- id: create-pull-request - if: env.NEW_VERSION - uses: gr2m/create-or-update-pull-request-action@b65137ca591da0b9f43bad7b24df13050ea45d1b # v1.10.1 - # Creates a PR with the new OpenSSL source code committed @@ -4922,6 +4983,7 @@ index 10eb3289a1..0000000000 - title: 'deps: update OpenSSL to ${{ env.NEW_VERSION }}' - path: deps/openssl - update-pull-request-title-and-body: true +- draft: true - - name: Regenerate platform specific files - if: env.NEW_VERSION - run: | @@ -4940,6 +5002,12 @@ index 10eb3289a1..0000000000 - branch: actions/tools-update-openssl # Custom branch *just* for this Action. - commit-message: 'deps: update archs files for openssl-${{ env.NEW_VERSION }}' - path: deps/openssl +- - name: Mark PR as ready for review +- if: env.NEW_VERSION +- env: +- GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} +- PR_ID: ${{ steps.create-pull-request.outputs.pull-request-number }} +- run: gh --repo "$GITHUB_REPOSITORY" pr ready "$PR_ID" diff --git a/.github/workflows/update-v8.yml b/.github/workflows/update-v8.yml deleted file mode 100644 index 8e71e56389..0000000000 diff --git a/patches/doc-build-mobile-notes-and-ignores.patch b/patches/doc-build-mobile-notes-and-ignores.patch index 2aa24a42c0..4116702f1f 100644 --- a/patches/doc-build-mobile-notes-and-ignores.patch +++ b/patches/doc-build-mobile-notes-and-ignores.patch @@ -16,7 +16,7 @@ Co-authored-by: Andre Staltz 2 files changed, 32 insertions(+), 893 deletions(-) diff --git a/.gitignore b/.gitignore -index 9277cdf090..b6f48a2e7c 100644 +index 2a87849224..96f28306c5 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,10 @@ @@ -39,7 +39,7 @@ index 9277cdf090..b6f48a2e7c 100644 /deps/openssl/openssl.props /deps/openssl/openssl.targets diff --git a/README.md b/README.md -index d90887e0f5..1d3ab7e321 100644 +index d14fba943d..1d3ab7e321 100644 --- a/README.md +++ b/README.md @@ -1,915 +1,49 @@ @@ -516,6 +516,8 @@ index d90887e0f5..1d3ab7e321 100644 - **Vladimir Morozov** <> (he/him) -* [watilde](https://github.com/watilde) - - **Daijiro Wachi** <> (he/him) +-* [XadillaX](https://github.com/XadillaX) - +- **Khaidi Chu** <> (he/him) -* [ZYSzys](https://github.com/ZYSzys) - - **Yongsheng Zhang** <> (he/him) - @@ -796,8 +798,6 @@ index d90887e0f5..1d3ab7e321 100644 - **Thomas Watson** <> -* [whitlockjc](https://github.com/whitlockjc) - - **Jeremy Whitlock** <> --* [XadillaX](https://github.com/XadillaX) - -- **Khaidi Chu** <> (he/him) -* [yashLadha](https://github.com/yashLadha) - - **Yash Ladha** <> (he/him) -* [yhwang](https://github.com/yhwang) - diff --git a/patches/files.map b/patches/files.map index 70a3c0c623..219ba50d38 100644 --- a/patches/files.map +++ b/patches/files.map @@ -34,6 +34,7 @@ test-mobile-test-harness.patch test/message/message.status test-mobile-test-harness.patch test/message/testcfg.py test-mobile-test-harness.patch tools/test.py test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/parallel.status +test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-bench-stream.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-common-wpt-inspect.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-dgram-bind-fd.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-dgram-membership.js @@ -41,6 +42,7 @@ test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-dg test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-copyfile.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-lchmod.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-mkdir.js +test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-negative-zero.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-open-flags.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-promises.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-read-file-sync-hostname.js diff --git a/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch b/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch index f09ccc7ba3..1eaa3de85e 100644 --- a/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch +++ b/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch @@ -46,7 +46,7 @@ index 67cd2feb69..994885b996 100644 + SOFTWARE. + """ diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js -index 00647022a8..c70a1e99f5 100644 +index 2111579151..9a1b965e5c 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -10,6 +10,7 @@ const { @@ -57,7 +57,7 @@ index 00647022a8..c70a1e99f5 100644 NumberParseInt, ObjectDefineProperty, ObjectFreeze, -@@ -113,6 +114,7 @@ function prepareExecution(options) { +@@ -111,6 +112,7 @@ function prepareExecution(options) { // Patch the process object and get the resolved main entry point. const mainEntry = patchProcessObject(expandArgv1); @@ -65,7 +65,7 @@ index 00647022a8..c70a1e99f5 100644 setupTraceCategoryState(); setupInspectorHooks(); setupNetworkInspection(); -@@ -492,6 +494,94 @@ function setupEventsource() { +@@ -451,6 +453,94 @@ function setupEventsource() { } } diff --git a/patches/src-expose-the-mobile-version-key-in-metadata.patch b/patches/src-expose-the-mobile-version-key-in-metadata.patch index 1b85b7e2f8..9b88c1e6d0 100644 --- a/patches/src-expose-the-mobile-version-key-in-metadata.patch +++ b/patches/src-expose-the-mobile-version-key-in-metadata.patch @@ -15,7 +15,7 @@ Co-authored-by: Jaime Bernardo 2 files changed, 14 insertions(+) diff --git a/src/node_metadata.cc b/src/node_metadata.cc -index b91b1b4881..e7ffe0457e 100644 +index 0f3a76105a..1ef9ec58df 100644 --- a/src/node_metadata.cc +++ b/src/node_metadata.cc @@ -55,6 +55,10 @@ diff --git a/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch b/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch index 53827956fd..5bc362d085 100644 --- a/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch +++ b/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch @@ -18,7 +18,7 @@ Co-authored-by: Jaime Bernardo 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/node.cc b/src/node.cc -index cd83e7c043..63dd2b6e2d 100644 +index 163070b501..468b5d2c2c 100644 --- a/src/node.cc +++ b/src/node.cc @@ -706,6 +706,8 @@ void ResetStdio() { @@ -39,10 +39,10 @@ index cd83e7c043..63dd2b6e2d 100644 #endif // __POSIX__ } diff --git a/src/node_internals.h b/src/node_internals.h -index cf3ce9e13f..2795f738dd 100644 +index 1c4da8e2d2..3dc190f0a0 100644 --- a/src/node_internals.h +++ b/src/node_internals.h -@@ -319,7 +319,7 @@ class ThreadPoolWork { +@@ -326,7 +326,7 @@ class ThreadPoolWork { // Functions defined in node.cc that are exposed via the bootstrapper object diff --git a/patches/src-skip-unclonable-env-vars-on-Android.patch b/patches/src-skip-unclonable-env-vars-on-Android.patch index 35a5cf6c7c..f8ec57267c 100644 --- a/patches/src-skip-unclonable-env-vars-on-Android.patch +++ b/patches/src-skip-unclonable-env-vars-on-Android.patch @@ -19,10 +19,10 @@ Co-authored-by: Jaime Bernardo 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/node_env_var.cc b/src/node_env_var.cc -index 25e405be86..6c7ac610c5 100644 +index 8a16f9ec13..2340ed463b 100644 --- a/src/node_env_var.cc +++ b/src/node_env_var.cc -@@ -240,7 +240,9 @@ std::shared_ptr KVStore::Clone(Isolate* isolate) const { +@@ -300,7 +300,9 @@ std::shared_ptr KVStore::Clone(Isolate* isolate) const { } CHECK(key->IsString()); if (!Get(isolate, key.As()).ToLocal(&value)) { diff --git a/patches/test-mobile-test-harness.patch b/patches/test-mobile-test-harness.patch index d74ad0ee99..d0c4bbd8bb 100644 --- a/patches/test-mobile-test-harness.patch +++ b/patches/test-mobile-test-harness.patch @@ -24,7 +24,7 @@ Co-authored-by: Andre Staltz diff --git a/test/common/index.js b/test/common/index.js old mode 100755 new mode 100644 -index ce083ee729..bef5f99a3c +index a2c7106a52..d94274866a --- a/test/common/index.js +++ b/test/common/index.js @@ -184,6 +184,8 @@ const isFreeBSD = process.platform === 'freebsd'; @@ -36,7 +36,7 @@ index ce083ee729..bef5f99a3c const isASan = process.config.variables.asan === 1; const isRiscv64 = process.arch === 'riscv64'; const isDebug = process.features.debug; -@@ -1032,8 +1034,10 @@ const common = { +@@ -1033,8 +1035,10 @@ const common = { invalidArgTypeHelper, isAlive, isASan, @@ -153,10 +153,10 @@ index 5470fd2bd9..4db0a9d5c6 100644 return result diff --git a/tools/test.py b/tools/test.py -index 90df2cfb2f..8f226307df 100755 +index fee5172b41..1b71ecba21 100755 --- a/tools/test.py +++ b/tools/test.py -@@ -1749,6 +1749,12 @@ def Main(): +@@ -1752,6 +1752,12 @@ def Main(): 'asan': get_asan_state(vm, context), 'pointer_compression': get_pointer_compression_state(vm, context), } diff --git a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch index 8207f52d06..773b7c0fd8 100644 --- a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch +++ b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch @@ -19,7 +19,8 @@ Co-authored-by: Andre Staltz Signed-off-by: fogtape Assisted-by: Codex --- - test/parallel/parallel.status | 1627 +++++++++++++++++ + test/parallel/parallel.status | 1659 +++++++++++++++++ + test/parallel/test-bench-stream.js | 3 +- test/parallel/test-common-wpt-inspect.js | 10 +- test/parallel/test-dgram-bind-fd.js | 2 +- test/parallel/test-dgram-membership.js | 10 +- @@ -27,6 +28,7 @@ Assisted-by: Codex test/parallel/test-fs-copyfile.js | 3 +- test/parallel/test-fs-lchmod.js | 4 +- test/parallel/test-fs-mkdir.js | 5 +- + test/parallel/test-fs-negative-zero.js | 19 +- test/parallel/test-fs-open-flags.js | 3 +- test/parallel/test-fs-promises.js | 6 +- .../test-fs-read-file-sync-hostname.js | 2 +- @@ -58,21 +60,35 @@ Assisted-by: Codex test/parallel/test-vfs-fs-readFileSync.js | 3 +- test/parallel/test-vfs-fs-writeFileSync.js | 3 +- .../test-watch-mode-files_watcher.mjs | 3 + - test/sequential/sequential.status | 129 ++ + test/sequential/sequential.status | 137 ++ test/sequential/test-async-wrap-getasyncid.js | 15 +- test/sequential/test-fs-watch.js | 4 + ...test-vm-timeout-escape-promise-module-2.js | 6 +- - 43 files changed, 1980 insertions(+), 57 deletions(-) + 45 files changed, 2033 insertions(+), 66 deletions(-) diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status -index d8fba447e9..d52a0ea801 100644 +index 0fe217d4e4..b3000c5a03 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status -@@ -145,6 +145,1633 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP +@@ -138,6 +138,1665 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP test-tls-set-default-ca-certificates-reset-fetch: SKIP test-inspector-invalid-protocol: SKIP +[$system==android] ++# Node 26.11: inferred subprocess exclusions; confirm with the full device suite. ++# These tests require independent child Node executables, unavailable in apps. ++test-crypto-randomfillsync-trace-sync-io: SKIP ++test-module-builtin-policies: SKIP ++test-permission-net-cluster: SKIP ++test-permission-net-socket-fd: SKIP ++test-process-timeout: SKIP ++test-process-timeout-blocked: SKIP ++test-process-timeout-coverage: SKIP ++test-process-timeout-inspector: SKIP ++test-process-timeout-options: SKIP ++test-snapshot-perf-hooks-gc-observer: SKIP ++test-worker-message-port-transfer-filehandle-fd0: SKIP ++ +# Snapshot creation needs child Node executables. The Android full suite +# confirms that process.execPath launches an app host instead. +test-crypto-provider-cache-snapshot: SKIP @@ -112,7 +128,7 @@ index d8fba447e9..d52a0ea801 100644 +test-snapshot-invalid-blob: SKIP +test-vfs-flag: SKIP +test-vfs-module-paths: SKIP -+test-vfs-mount-load: SKIP ++test-vfs-load: SKIP +test-zlib-zip-experimental-warning: SKIP +test-zlib-zip-security-hardening: SKIP +# Permission flags deny the mobile harness its per-launch verdict write. @@ -800,6 +816,20 @@ index d8fba447e9..d52a0ea801 100644 +# +test-worker-cleanup-handles: PASS, FLAKY +[$system==ios] ++# Node 26.11: inferred subprocess exclusions; confirm with the full device suite. ++# These tests require independent child Node executables, unavailable in apps. ++test-crypto-randomfillsync-trace-sync-io: SKIP ++test-module-builtin-policies: SKIP ++test-permission-net-cluster: SKIP ++test-permission-net-socket-fd: SKIP ++test-process-timeout: SKIP ++test-process-timeout-blocked: SKIP ++test-process-timeout-coverage: SKIP ++test-process-timeout-inspector: SKIP ++test-process-timeout-options: SKIP ++test-snapshot-perf-hooks-gc-observer: SKIP ++test-worker-message-port-transfer-filehandle-fd0: SKIP ++ +# Snapshot creation needs child Node executables. The Android full suite +# confirms that process.execPath launches an app host instead. +test-crypto-provider-cache-snapshot: SKIP @@ -837,7 +867,7 @@ index d8fba447e9..d52a0ea801 100644 +test-snapshot-invalid-blob: SKIP +test-vfs-flag: SKIP +test-vfs-module-paths: SKIP -+test-vfs-mount-load: SKIP ++test-vfs-load: SKIP +test-zlib-zip-experimental-warning: SKIP +test-zlib-zip-security-hardening: SKIP +# Permission flags deny the mobile harness its per-launch verdict write. @@ -1699,9 +1729,25 @@ index d8fba447e9..d52a0ea801 100644 +# +test-https-client-renegotiation-limit: PASS, FLAKY +test-https-selfsigned-no-keycertsign-no-crash: PASS, FLAKY - [$asan==on] - # https://github.com/nodejs/node/issues/39655 - test-cluster-primary-error: PASS, FLAKY ++[$asan==on] ++# https://github.com/nodejs/node/issues/39655 ++test-cluster-primary-error: PASS, FLAKY ++ + [$arch==loong64] + # https://github.com/nodejs/node/issues/51662 + test-http-correct-hostname: SKIP +diff --git a/test/parallel/test-bench-stream.js b/test/parallel/test-bench-stream.js +index dbb68f8be9..5347abd707 100644 +--- a/test/parallel/test-bench-stream.js ++++ b/test/parallel/test-bench-stream.js +@@ -412,5 +412,6 @@ function testOperatorsWithoutStreamModule() { + await testReportingFailureSettlesBenchmarks(); + await testSummaryListenerFailure(); + await testRecordOwnership(); +- testOperatorsWithoutStreamModule(); ++ // nodejs-mobile: only this startup check needs an independent child Node. ++ if (!common.isAndroid && !common.isIOS) testOperatorsWithoutStreamModule(); + })().then(common.mustCall()); diff --git a/test/parallel/test-common-wpt-inspect.js b/test/parallel/test-common-wpt-inspect.js index e614a4cfaa..47f206fb4e 100644 --- a/test/parallel/test-common-wpt-inspect.js @@ -1840,6 +1886,42 @@ index 185da03aeb..5a47def129 100644 const pathname = tmpdir.resolve(nextdir()); fs.mkdirSync(pathname); process.chdir(pathname); +diff --git a/test/parallel/test-fs-negative-zero.js b/test/parallel/test-fs-negative-zero.js +index 09f965eb17..b9d11059a3 100644 +--- a/test/parallel/test-fs-negative-zero.js ++++ b/test/parallel/test-fs-negative-zero.js +@@ -1,6 +1,6 @@ + 'use strict'; + +-require('../common'); ++const common = require('../common'); + + const assert = require('assert'); + const { spawnSync } = require('child_process'); +@@ -36,13 +36,16 @@ ignoreExpectedError(() => fs.writeFileSync(missing, '', { mode: -0 })); + fs.writeFileSync(-0, ''); + fs.appendFileSync(-0, ''); + +-const child = spawnSync( +- process.execPath, +- ['-e', 'process.stdout.write(require("fs").readFileSync(-0, "utf8"))'], +- { input: 'hello' }, +-); +-assert.strictEqual(child.status, 0); +-assert.strictEqual(child.stdout.toString(), 'hello'); ++// nodejs-mobile: retain the in-process checks; stdin startup needs a child Node. ++if (!common.isAndroid && !common.isIOS) { ++ const child = spawnSync( ++ process.execPath, ++ ['-e', 'process.stdout.write(require("fs").readFileSync(-0, "utf8"))'], ++ { input: 'hello' }, ++ ); ++ assert.strictEqual(child.status, 0); ++ assert.strictEqual(child.stdout.toString(), 'hello'); ++} + + fs.watchFile(missing, { interval: -0 }, () => {}); + fs.unwatchFile(missing); diff --git a/test/parallel/test-fs-open-flags.js b/test/parallel/test-fs-open-flags.js index 55478247f6..cdfaaa0b88 100644 --- a/test/parallel/test-fs-open-flags.js @@ -2440,7 +2522,7 @@ index b84d2063a2..4411854e0b 100644 }, 1); diff --git a/test/parallel/test-sqlite-backup.mjs b/test/parallel/test-sqlite-backup.mjs -index ee5f54fd90..64e8371ac2 100644 +index cae097aa92..31e159f2a3 100644 --- a/test/parallel/test-sqlite-backup.mjs +++ b/test/parallel/test-sqlite-backup.mjs @@ -1,6 +1,8 @@ @@ -2507,10 +2589,10 @@ index 7bde5f6c7b..2001bd55dd 100644 fs.mkdirSync(root, { recursive: true }); diff --git a/test/parallel/test-watch-mode-files_watcher.mjs b/test/parallel/test-watch-mode-files_watcher.mjs -index e1595350cd..f5d84f7480 100644 +index 10e078286e..9c4ec8787d 100644 --- a/test/parallel/test-watch-mode-files_watcher.mjs +++ b/test/parallel/test-watch-mode-files_watcher.mjs -@@ -199,6 +199,8 @@ describe('watch mode file watcher', () => { +@@ -228,6 +228,8 @@ describe('watch mode file watcher', () => { assert.deepStrictEqual(watcher.watchedPaths, []); }); @@ -2519,21 +2601,25 @@ index e1595350cd..f5d84f7480 100644 it('should watch files from subprocess IPC events', async () => { const file = fixtures.path('watch-mode/ipc.js'); const child = spawn(process.execPath, [file], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'], encoding: 'utf8' }); -@@ -210,4 +212,5 @@ describe('watch mode file watcher', () => { +@@ -241,4 +243,5 @@ describe('watch mode file watcher', () => { } assert.deepStrictEqual(watcher.watchedPaths, expected); }); + */ }); diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status -index 6b5714500b..9c8a8e0f5c 100644 +index f155b67473..2e1401325e 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status -@@ -43,6 +43,135 @@ test-perf-hooks: SKIP +@@ -33,6 +33,143 @@ test-perf-hooks: SKIP # https://github.com/nodejs/node/issues/39683 test-tls-psk-client: PASS, FLAKY +[$system==android] ++# Node 26.11: inferred subprocess exclusions; confirm with the full device suite. ++# These tests require independent child Node executables, unavailable in apps. ++test-watch-mode-process-timeout: SKIP ++ +# +# Tests that require child process that currently is not supported on mobile +# @@ -2591,6 +2677,10 @@ index 6b5714500b..9c8a8e0f5c 100644 +test-without-async-context-frame: SKIP +test-write-heapsnapshot-options: SKIP +[$system==ios] ++# Node 26.11: inferred subprocess exclusions; confirm with the full device suite. ++# These tests require independent child Node executables, unavailable in apps. ++test-watch-mode-process-timeout: SKIP ++ +# +# Tests that require child process that currently is not supported on mobile +# @@ -2666,7 +2756,7 @@ index 6b5714500b..9c8a8e0f5c 100644 # https://github.com/nodejs/node/issues/49933 test-watch-mode-inspect: SKIP diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js -index 0842f8fdfa..03233c2fa4 100644 +index 0800f07788..a738ae2c39 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -12,6 +12,8 @@ const providers = { ...internalBinding('async_wrap').Providers }; @@ -2678,7 +2768,7 @@ index 0842f8fdfa..03233c2fa4 100644 // Make sure that all Providers are tested. { -@@ -79,6 +81,12 @@ const { getSystemErrorName } = require('util'); +@@ -78,6 +80,12 @@ const { getSystemErrorName } = require('util'); delete providers.DTLS_ENDPOINT; delete providers.DTLS_SESSION; @@ -2691,7 +2781,7 @@ index 0842f8fdfa..03233c2fa4 100644 const objKeys = Object.keys(providers); if (objKeys.length > 0) process._rawDebug(objKeys); -@@ -200,7 +208,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check +@@ -199,7 +207,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check testInitialized(handle, 'Pipe'); } @@ -2701,7 +2791,7 @@ index 0842f8fdfa..03233c2fa4 100644 tmpdir.refresh(); const server = net.createServer(common.mustCall((socket) => { -@@ -332,6 +341,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check +@@ -331,6 +340,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check // DIRHANDLE { const dirBinding = internalBinding('fs_dir'); diff --git a/release-ready.txt b/release-ready.txt deleted file mode 100644 index 538901308c..0000000000 --- a/release-ready.txt +++ /dev/null @@ -1 +0,0 @@ -26.10.0-0 diff --git a/scripts/clone-upstream.sh b/scripts/clone-upstream.sh new file mode 100755 index 0000000000..95c1f0d6d7 --- /dev/null +++ b/scripts/clone-upstream.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Clone an upstream tag/branch or an immutable PR commit, without inventing tags. +# Usage: scripts/clone-upstream.sh +set -euo pipefail + +REPO=$1 +REF=$2 +OUT=$3 +[ ! -e "$OUT" ] || { echo "error: $OUT already exists" >&2; exit 1; } + +if [[ "$REF" =~ ^[0-9a-f]{40}$ ]]; then + git init -q "$OUT" + git -C "$OUT" remote add origin "$REPO" + git -C "$OUT" fetch --depth 1 origin "$REF" + git -C "$OUT" checkout -q --detach FETCH_HEAD + GOT=$(git -C "$OUT" rev-parse HEAD) + [ "$GOT" = "$REF" ] || { echo "error: upstream commit mismatch: $GOT != $REF" >&2; exit 1; } +else + git clone --depth 1 --branch "$REF" "$REPO" "$OUT" +fi +# Local metadata for the in-tree test audit, including unreleased PR snapshots. +git -C "$OUT" update-ref refs/nodejs-mobile/upstream-base HEAD diff --git a/scripts/prepare.sh b/scripts/prepare.sh index cc47969d35..ae4c589c4a 100755 --- a/scripts/prepare.sh +++ b/scripts/prepare.sh @@ -9,7 +9,7 @@ # # Env overrides (mainly for CI/offline use): # UPSTREAM_REPO git URL or local path (default https://github.com/nodejs/node.git) -# UPSTREAM_REF tag/branch to clone (default: first line of upstream-base.txt) +# UPSTREAM_REF tag/branch/full SHA (default: first line of upstream-base.txt) # SKIP_VERIFY=1 don't fail on an expected-tree mismatch (prints both hashes) set -euo pipefail @@ -27,7 +27,7 @@ if [ -e "${OUT}" ]; then fi echo "1/4 Cloning ${UPSTREAM_REPO} @ ${UPSTREAM_REF} (shallow)..." -git clone --depth 1 --branch "${UPSTREAM_REF}" "${UPSTREAM_REPO}" "${OUT}" +"${HERE}/scripts/clone-upstream.sh" "${UPSTREAM_REPO}" "${UPSTREAM_REF}" "${OUT}" git -C "${OUT}" config user.name "${GIT_AUTHOR_NAME:-nodejs-mobile}" git -C "${OUT}" config user.email "${GIT_AUTHOR_EMAIL:-nodejs-mobile@invalid}" diff --git a/scripts/release-control.py b/scripts/release-control.py index 8f7fce9a70..d3c5c838ba 100644 --- a/scripts/release-control.py +++ b/scripts/release-control.py @@ -38,7 +38,10 @@ def upstream(root): lines = [line.strip() for line in (root / 'upstream-base.txt').read_text().splitlines() if line.strip() and not line.lstrip().startswith('#')] if len(lines) != 1: - raise ReleaseError('upstream-base.txt must pin exactly one upstream release tag') + raise ReleaseError('upstream-base.txt must pin exactly one upstream ref') + if re.fullmatch(r'[0-9a-f]{40}', lines[0]): + raise ReleaseError('This source pins an unreleased upstream PR commit. ' + 'Build a CI preview; pin the official release tag before Cut release or publication.') return parse_version(lines[0]) diff --git a/scripts/test-mobile-flavors.py b/scripts/test-mobile-flavors.py index 4a43415a57..b8c337d59a 100755 --- a/scripts/test-mobile-flavors.py +++ b/scripts/test-mobile-flavors.py @@ -4,6 +4,7 @@ import json import os from pathlib import Path +import re import shlex import subprocess import sys @@ -15,9 +16,8 @@ SOURCE = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else ROOT / 'out' if len(sys.argv) > 1: del sys.argv[1] -MAJOR = int(next(line.strip().lstrip('v').split('.')[0] - for line in (ROOT / 'upstream-base.txt').read_text().splitlines() - if line.strip() and not line.startswith('#'))) +MAJOR = int(re.search(r'^#define NODE_MAJOR_VERSION (\d+)$', + (SOURCE / 'src/node_version.h').read_text(), re.M)[1]) # android_configure supports Linux/macOS build hosts; simulate Linux so this # configure-only test also runs on Termux's Python (which reports Android). ANDROID_WRAPPER = [sys.executable, '-c', diff --git a/scripts/test-release-control.py b/scripts/test-release-control.py index dd6846a1d2..b3b0e6d102 100644 --- a/scripts/test-release-control.py +++ b/scripts/test-release-control.py @@ -70,6 +70,19 @@ def test_auto_and_explicit_upstream_choose_free_revision(self): def test_choose_exact_unused_version(self): self.assertEqual(self.plan('v26.10.0-7')['version'], '26.10.0-7') + def test_upstream_pr_snapshot_builds_but_cannot_arm_or_publish(self): + (self.root / 'upstream-base.txt').write_text('7f68d75ee7963cd38ddc3783dd529f826bbde614\n') + for event in ('push', 'pull_request', 'workflow_dispatch'): + with self.subTest(event=event): + result = self.check(event=event, operation='build', version='') + self.assertEqual(result['release'], 'false') + with self.assertRaisesRegex(CONTROL.ReleaseError, 'unreleased upstream PR'): + self.plan() + for operation in ('prerelease', 'prerelease-dryrun'): + with self.subTest(operation=operation), self.assertRaisesRegex( + CONTROL.ReleaseError, 'unreleased upstream PR'): + self.check(operation=operation) + def test_plan_never_mutates_source(self): before = {p.relative_to(self.root): p.read_bytes() for p in self.root.rglob('*') if p.is_file()} self.plan('26.10.0-0') diff --git a/scripts/test-upstream-clone.py b/scripts/test-upstream-clone.py new file mode 100644 index 0000000000..e84f013ec2 --- /dev/null +++ b/scripts/test-upstream-clone.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Verify immutable upstream clones and cache transport without public network.""" +from pathlib import Path +import subprocess +import tempfile +import unittest + +CLONE = Path(__file__).with_name('clone-upstream.sh').resolve() + + +class UpstreamClone(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='node-mobile-upstream-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.repo = self.root / 'upstream' + self.repo.mkdir() + self.git(self.repo, 'init', '-q') + self.git(self.repo, 'config', 'user.name', 'fixture') + self.git(self.repo, 'config', 'user.email', 'fixture@invalid') + (self.repo / 'version').write_text('released\n') + self.git(self.repo, 'add', 'version') + self.git(self.repo, 'commit', '-qm', 'release') + self.git(self.repo, 'tag', '-a', 'v26.10.0', '-m', 'release') + self.released = self.git(self.repo, 'rev-parse', 'HEAD') + (self.repo / 'version').write_text('proposal\n') + self.git(self.repo, 'commit', '-qam', 'proposal') + self.proposal = self.git(self.repo, 'rev-parse', 'HEAD') + + def git(self, repo, *args): + return subprocess.check_output(['git', '-C', str(repo), *args], text=True).strip() + + def clone(self, repo, ref, dest, *, check=True): + return subprocess.run([str(CLONE), repo.as_uri(), ref, str(dest)], + text=True, capture_output=True, check=check) + + def test_sha_clone_and_cached_reclone_preserve_exact_commit_without_tags(self): + cache, output = self.root / 'cache', self.root / 'output' + self.clone(self.repo, self.proposal, cache) + self.clone(cache, self.proposal, output) + self.assertEqual(self.git(output, 'rev-parse', 'HEAD'), self.proposal) + self.assertEqual(self.git(output, 'rev-parse', 'refs/nodejs-mobile/upstream-base'), + self.proposal) + self.assertEqual((output / 'version').read_text(), 'proposal\n') + self.assertEqual(self.git(output, 'tag', '-l'), '') + self.assertEqual(self.git(output, 'rev-parse', '--is-shallow-repository'), 'true') + + def test_annotated_release_tag_still_clones(self): + output = self.root / 'output' + self.clone(self.repo, 'v26.10.0', output) + self.assertEqual(self.git(output, 'rev-parse', 'HEAD'), self.released) + self.assertEqual(self.git(output, 'rev-parse', 'v26.10.0^{commit}'), self.released) + + def test_existing_destination_is_preserved(self): + output = self.root / 'output' + output.mkdir() + (output / 'keep').write_text('existing work\n') + self.assertNotEqual(self.clone(self.repo, self.proposal, output, check=False).returncode, 0) + self.assertEqual((output / 'keep').read_text(), 'existing work\n') + + def test_missing_sha_fails_without_checked_out_source(self): + output = self.root / 'output' + self.assertNotEqual(self.clone(self.repo, '0' * 40, output, check=False).returncode, 0) + self.assertFalse((output / 'version').exists()) + + +if __name__ == '__main__': + unittest.main() diff --git a/upstream-base.txt b/upstream-base.txt index 8cc2e82470..3d45fd0180 100644 --- a/upstream-base.txt +++ b/upstream-base.txt @@ -1,4 +1,4 @@ -# The upstream nodejs/node release the patch series applies to. -# First non-comment line is the tag (or SHA); update it as part of every -# upstream bump (see README.md). -v26.10.0 +# Node.js 26.11.0 release proposal, not yet an official release tag. +# https://github.com/nodejs/node/pull/66546 (v26.11.0-proposal) +# Pin the reviewed PR commit; do not follow the moving proposal branch. +7f68d75ee7963cd38ddc3783dd529f826bbde614 From cd1d84614fcd79fe8069c8bed1b78bd2f7f7013b Mon Sep 17 00:00:00 2001 From: fogtape Date: Tue, 6 Oct 2026 11:50:30 +0800 Subject: [PATCH 5/8] =?UTF-8?q?ci:=20=E6=94=AF=E6=8C=81=20PR=20=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E5=9F=BA=E7=BA=BF=E6=A3=80=E6=9F=A5=E5=B9=B6=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=20iOS=20lite=20=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 验证官方提交 SHA,按实际移动端版本命名预览包,上游升级 PR 覆盖 iOS full/lite 编译和冒烟。新增基线检查回归,39 项本地检查通过。 Signed-off-by: fogtape --- .github/workflows/build.yml | 33 ++++++++++++++++++++++----------- docs/UPSTREAM-26.11.md | 6 +++++- scripts/test-upstream-clone.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1a8c7494b..3fb332f429 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,8 +70,8 @@ permissions: jobs: package-preview: - if: github.ref == 'refs/heads/recipe-v26' - needs: [ci-required, curated-tests-android, curated-tests-ios] + if: github.ref == 'refs/heads/recipe-v26' || (github.event_name == 'pull_request' && needs.upgrade-check.outputs.upgrade == 'true') + needs: [release-check, upgrade-check, ci-required, curated-tests-android, curated-tests-ios] runs-on: ubuntu-24.04 permissions: contents: read @@ -86,7 +86,7 @@ jobs: run: python3 scripts/package-preview.py preview-input preview-packages - uses: actions/upload-artifact@v7 with: - name: nodejs-mobile-26.10.0-0-preview + name: nodejs-mobile-${{ needs.release-check.outputs.version }}-preview path: preview-packages retention-days: 90 if-no-files-found: error @@ -94,9 +94,10 @@ jobs: # Flavor matrices are inline expressions, not a helper job (a full VM boot # to echo two constants earns nothing). Android always runs both flavors # (ubuntu+KVM is 1x-billed and plentiful); iOS drops to full-only on - # pull_request because hosted macOS concurrency is capped far below Linux + # ordinary pull_request because hosted macOS concurrency is capped far below Linux # and two PRs running the complete iOS matrix could queue the release - # pipeline behind them. What the lite iOS leg uniquely catches — + # pipeline behind them. Upstream upgrades build and smoke both iOS flavors. + # What the lite iOS leg uniquely catches — # lite-specific configure/link breakage — is still caught at compile time # on the merge run. Pushes and releases run both. See docs/TESTING.md, # "Flavors, and what's required to merge". @@ -319,11 +320,11 @@ jobs: path: out_android build-ios: - needs: release-check + needs: [release-check, upgrade-check] strategy: fail-fast: false matrix: - flavor: ${{ github.event_name == 'pull_request' && fromJSON('["full"]') || fromJSON('["full","lite"]') }} + flavor: ${{ github.event_name == 'pull_request' && needs.upgrade-check.outputs.upgrade != 'true' && fromJSON('["full"]') || fromJSON('["full","lite"]') }} config: # x86_64 (Intel Mac) simulator support was dropped for v24 — Intel # Macs are EOL. @@ -567,9 +568,9 @@ jobs: strategy: fail-fast: false matrix: - flavor: ${{ github.event_name == 'pull_request' && fromJSON('["full"]') || fromJSON('["full","lite"]') }} + flavor: ${{ github.event_name == 'pull_request' && needs.upgrade-check.outputs.upgrade != 'true' && fromJSON('["full"]') || fromJSON('["full","lite"]') }} runs-on: macos-26 - needs: [build-ios] + needs: [build-ios, upgrade-check] env: NODEJS_MOBILE_FLAVOR: ${{ matrix.flavor }} FLAVOR_SUFFIX: ${{ matrix.flavor == 'lite' && '-lite' || '' }} @@ -696,11 +697,11 @@ jobs: # Boot smoke for iOS: install the testnode app on a booted simulator and run a # console.log via the freshly built NodeMobile.xcframework. smoke-ios: - needs: [combine-ios] + needs: [combine-ios, upgrade-check] strategy: fail-fast: false matrix: - flavor: ${{ github.event_name == 'pull_request' && fromJSON('["full"]') || fromJSON('["full","lite"]') }} + flavor: ${{ github.event_name == 'pull_request' && needs.upgrade-check.outputs.upgrade != 'true' && fromJSON('["full"]') || fromJSON('["full","lite"]') }} runs-on: macos-26 env: NODEJS_MOBILE_FLAVOR: ${{ matrix.flavor }} @@ -1223,6 +1224,8 @@ jobs: fetch-depth: 1 - name: Check the pinned upstream base is on this fork + env: + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail # || true: grep exits 1 on a comments-only file, which under pipefail @@ -1230,6 +1233,14 @@ jobs: BASE=$(grep -Ev '^\s*(#|$)' upstream-base.txt | head -n1 | tr -d '[:space:]' || true) [ -n "$BASE" ] || { echo "::error::no base in upstream-base.txt"; exit 1; } + if [[ "$BASE" =~ ^[0-9a-f]{40}$ ]]; then + WANT=$(gh api "repos/nodejs/node/commits/$BASE" --jq .sha) + [ "$WANT" = "$BASE" ] || { echo "::error::upstream snapshot SHA mismatch"; exit 1; } + echo "Verified upstream PR snapshot $BASE; no release ancestry ref is required." + # release-control.py rejects Cut release/publication for SHA bases. + exit 0 + fi + # Each major keeps an independent upstream ancestry ref. MAJOR=${BASE#v}; MAJOR=${MAJOR%%.*} BASE_REF=upstream-base diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md index d364015504..06844039e0 100644 --- a/docs/UPSTREAM-26.11.md +++ b/docs/UPSTREAM-26.11.md @@ -30,6 +30,10 @@ recording the upstream commit, outside the source tree. ## Flavor contract +Upstream upgrade PRs build and boot-smoke both iOS flavors, and preview +package names use the recorded mobile version. SHA baselines are verified +against the official repository without requiring a release ancestry branch. + Full retains complete ICU data, Temporal and FFI, with the existing mobile Rust/libffi integration. The 26.11 rebase also preserves upstream's new Temporal zoneinfo source generator and the iOS platform guards. @@ -52,7 +56,7 @@ The 22,081,472-byte reduction measures ICU data, not a compiled libnode or ZIP. - A fresh shallow fetch from `nodejs/node` applied all 22 regenerated patches directly; regeneration from that fresh tree preserved every recipe byte. It reconstructed tree `a92053298021a6780ac09b4c31653fc210d68ec8`. -- 38 local regression tests passed: immutable SHA/tag/cache cloning (4), +- 39 local regression tests passed: immutable SHA/tag/cache/CI baseline checks (5), flavor/ICU contracts (5), Rust/libffi toolchain actions (5), embedded verdict isolation (6), real shared-library symbol stripping (1), release control (17). - Recipe ownership, YAML/shell/JavaScript syntax, all 219 curated test entries, diff --git a/scripts/test-upstream-clone.py b/scripts/test-upstream-clone.py index e84f013ec2..2039860b04 100644 --- a/scripts/test-upstream-clone.py +++ b/scripts/test-upstream-clone.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """Verify immutable upstream clones and cache transport without public network.""" from pathlib import Path +import os +import re import subprocess import tempfile import unittest @@ -63,6 +65,34 @@ def test_missing_sha_fails_without_checked_out_source(self): self.assertNotEqual(self.clone(self.repo, '0' * 40, output, check=False).returncode, 0) self.assertFalse((output / 'version').exists()) + def test_ci_upstream_check_verifies_snapshot_sha_and_rejects_mismatch(self): + workflow = CLONE.parent.parent / '.github/workflows/build.yml' + match = re.search(r' - name: Check the pinned upstream base is on this fork\n' + r'.*? run: \|\n((?: {10}[^\n]*\n|\n)+)', + workflow.read_text(), re.S) + self.assertIsNotNone(match) + script = '\n'.join(line[10:] if line else '' for line in match[1].splitlines()) + (self.root / 'upstream-base.txt').write_text(self.proposal + '\n') + tools = self.root / 'tools' + tools.mkdir() + gh = tools / 'gh' + gh.write_text('#!/bin/sh\nprintf "%s\\n" "$*" > "$API_RECORD"\n' + 'printf "%s\\n" "$API_SHA"\n') + gh.chmod(0o755) + record = self.root / 'api-record' + env = dict(os.environ, PATH=str(tools) + os.pathsep + os.environ['PATH'], + API_RECORD=str(record), API_SHA=self.proposal) + result = subprocess.run(['bash', '-c', script], cwd=self.root, + env=env, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('Verified upstream PR snapshot', result.stdout) + self.assertEqual(record.read_text().strip(), + f'api repos/nodejs/node/commits/{self.proposal} --jq .sha') + result = subprocess.run(['bash', '-c', script], cwd=self.root, + env=dict(env, API_SHA='0' * 40), capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('snapshot SHA mismatch', result.stdout) + if __name__ == '__main__': unittest.main() From 9373f4297b65d9ed9f563d1f49f570cf697581d7 Mon Sep 17 00:00:00 2001 From: fogtape Date: Tue, 6 Oct 2026 17:13:02 +0800 Subject: [PATCH 6/8] =?UTF-8?q?test:=20=E4=BF=AE=E5=A4=8D=20Node=2026.11?= =?UTF-8?q?=20Android=20debugger=20=E5=AD=90=E8=BF=9B=E7=A8=8B=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/UPSTREAM-26.11.md | 27 ++++++++++++++++--- expected-tree.txt | 2 +- ...parallel-and-sequential-tests-for-mo.patch | 16 ++++++++--- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md index 06844039e0..0c4f98866e 100644 --- a/docs/UPSTREAM-26.11.md +++ b/docs/UPSTREAM-26.11.md @@ -55,7 +55,9 @@ The 22,081,472-byte reduction measures ICU data, not a compiled libnode or ZIP. - A fresh shallow fetch from `nodejs/node` applied all 22 regenerated patches directly; regeneration from that fresh tree preserved every recipe byte. - It reconstructed tree `a92053298021a6780ac09b4c31653fc210d68ec8`. + The current recipe reconstructs tree + `e52cbddd35a391b2ae554be1b171e4febeffe5ec` after the Android test correction + described below. - 39 local regression tests passed: immutable SHA/tag/cache/CI baseline checks (5), flavor/ICU contracts (5), Rust/libffi toolchain actions (5), embedded verdict isolation (6), real shared-library symbol stripping (1), release control (17). @@ -73,9 +75,26 @@ The 22,081,472-byte reduction measures ICU data, not a compiled libnode or ZIP. exclusion was updated. Existing bench/negative-zero tests retain their in-process assertions and skip only the child startup checks. -Complete Android/iOS compilation, newly added device tests and final artifact -size measurements remain CI work. The local checks do not establish that new -mobile binaries have passed device testing. +[Build run 37410879823](https://github.com/fogtape/nodejs-mobile/actions/runs/37410879823) +completed all ten Android/iOS full/lite compilations, both platforms' full/lite +boot smokes, Android full/lite curated tests and iOS full curated tests +successfully. Recipe reconstruction and per-patch configure checks also passed. + +The Android full-suite attempt was incomplete: shards 0, 1 and 2 received +`The runner has received a shutdown signal` before completing or uploading +their summaries. Shards 1 and 2 also exposed missing debugger CLI exclusions: +`test-debugger-backtrace`, `test-debugger-exec` and +`test-debugger-restart-message` launch `process.execPath` through +`test/common/debugger.js`. In an Android app this starts `app_process`, whose +child aborted with `Error changing dalvik-cache ownership : Permission denied`. +The recipe now excludes those three tests and the same subprocess-dependent +`test-debugger-low-level` and `test-debugger-repeat-last` on Android. The +in-process `test-debugger-run-restart-init` and `test-debugger-wait-for-debugger` +remain runnable. Node's actual status-file parser verified that distinction. + +These exclusions correct test applicability; they do not explain the runner +shutdowns. A complete device-suite rerun is still required before merge. +Final artifact size measurements are separate from the ICU data measurement. ## Move to the official release diff --git a/expected-tree.txt b/expected-tree.txt index 9fc7d4f9fd..bf8a03d1dd 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -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. -a92053298021a6780ac09b4c31653fc210d68ec8 +e52cbddd35a391b2ae554be1b171e4febeffe5ec diff --git a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch index 773b7c0fd8..ce6ee26e3a 100644 --- a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch +++ b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch @@ -19,7 +19,7 @@ Co-authored-by: Andre Staltz Signed-off-by: fogtape Assisted-by: Codex --- - test/parallel/parallel.status | 1659 +++++++++++++++++ + test/parallel/parallel.status | 1667 +++++++++++++++++ test/parallel/test-bench-stream.js | 3 +- test/parallel/test-common-wpt-inspect.js | 10 +- test/parallel/test-dgram-bind-fd.js | 2 +- @@ -64,13 +64,13 @@ Assisted-by: Codex test/sequential/test-async-wrap-getasyncid.js | 15 +- test/sequential/test-fs-watch.js | 4 + ...test-vm-timeout-escape-promise-module-2.js | 6 +- - 45 files changed, 2033 insertions(+), 66 deletions(-) + 45 files changed, 2041 insertions(+), 66 deletions(-) diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status -index 0fe217d4e4..b3000c5a03 100644 +index 0fe217d4e4..4578a1cf97 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status -@@ -138,6 +138,1665 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP +@@ -138,6 +138,1673 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP test-tls-set-default-ca-certificates-reset-fetch: SKIP test-inspector-invalid-protocol: SKIP @@ -208,23 +208,31 @@ index 0fe217d4e4..b3000c5a03 100644 +test-crypto-secure-heap: SKIP +test-cwd-enoent*: SKIP +test-debug-usage: SKIP ++# The Node 26.11 Android sweep exposed missing debugger CLI exclusions. ++# These tests spawn app_process via test/common/debugger.js (or directly). ++# Keep the in-process run-restart-init and wait-for-debugger tests runnable. +test-debugger-address: SKIP +test-debugger-auto-resume: SKIP ++test-debugger-backtrace: SKIP +test-debugger-break: SKIP +test-debugger-breakpoint-exists: SKIP +test-debugger-clear-breakpoints: SKIP +test-debugger-exceptions: SKIP ++test-debugger-exec: SKIP +test-debugger-exec-scope: SKIP +test-debugger-extract-function-name: SKIP +test-debugger-heap-profiler: SKIP +test-debugger-help: SKIP +test-debugger-invalid-json: SKIP +test-debugger-list: SKIP ++test-debugger-low-level: SKIP +test-debugger-object-type-remote-object: SKIP +test-debugger-profile: SKIP +test-debugger-profile-command: SKIP +test-debugger-random-port: SKIP +test-debugger-random-port-with-inspect-port: SKIP ++test-debugger-repeat-last: SKIP ++test-debugger-restart-message: SKIP +test-debugger-run-after-quit-restart: SKIP +test-debugger-sb-before-load: SKIP +test-debugger-scripts: SKIP From 3c199ac8e2ee7df4ea2e8c987030c94d8beeda1a Mon Sep 17 00:00:00 2001 From: fogtape Date: Tue, 6 Oct 2026 21:43:58 +0800 Subject: [PATCH 7/8] =?UTF-8?q?ci:=20=E7=BC=A9=E7=9F=AD=20Android=20?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E6=B5=8B=E8=AF=95=E4=BC=9A=E8=AF=9D=E5=B9=B6?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=2026.11=20=E6=8F=90=E6=A1=88=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/full-device-suite.yml | 23 ++- .github/workflows/verify-patches.yml | 3 + README.md | 2 +- docs/CHANGELOG.md | 8 +- docs/TESTING.md | 4 +- docs/UPSTREAM-26.11.md | 38 ++++- expected-tree.txt | 2 +- .../android/prepare-android-test.sh | 5 +- .../mobile-test/android/run-suite-shard.sh | 54 +++++++ scripts/test-android-suite.py | 148 ++++++++++++++++++ upstream-base.txt | 2 +- 11 files changed, 270 insertions(+), 19 deletions(-) create mode 100755 mobile-src/tools/mobile-test/android/run-suite-shard.sh create mode 100644 scripts/test-android-suite.py diff --git a/.github/workflows/full-device-suite.yml b/.github/workflows/full-device-suite.yml index dad84ec6e7..7069e8c6c5 100644 --- a/.github/workflows/full-device-suite.yml +++ b/.github/workflows/full-device-suite.yml @@ -1,6 +1,6 @@ # The full device suite: everything test/parallel + test/sequential that -# .status does not skip, on an Android emulator and an iOS simulator, in 4 -# round-robin shards per platform (test.py --run=n,4). +# .status does not skip, on an Android emulator and an iOS simulator, in +# round-robin shards (16 Android, 4 iOS). # # The curated gate (build.yml) is the fast PR allow-list; this job is the # complement: a test upstream adds is picked up automatically, and excluding @@ -97,10 +97,15 @@ jobs: if: needs.precheck.outputs.skip != 'true' && inputs.platform != 'ios' strategy: fail-fast: false - # Keep in step with the ,4 in --run below: a matrix of a different - # length would drop or double-run tests rather than fail. + # Two Node 26.11 runs lost runners after ~470-560 app launches, with + # QEMU hangs and no failing test. Shorter emulator sessions retain all + # test coverage; bound concurrency to the previous four Android jobs. + max-parallel: 4 matrix: - shard: [0, 1, 2, 3] + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + env: + # The denominator and matrix length are checked by the integrity gate. + ANDROID_SUITE_SHARDS: 16 runs-on: ubuntu-24.04 timeout-minutes: 90 steps: @@ -149,13 +154,15 @@ jobs: api-level: ${{ env.EMULATOR_API_LEVEL }} target: default arch: x86_64 + ram-size: 2048M + heap-size: 256M force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true script: | adb wait-for-device ./tools/mobile-test/android/prepare-android-test.sh - bash -o pipefail -c "./tools/test.py -j 1 --flaky-tests=skip --timeout=300 --arch android --run=${{ matrix.shard }},4 parallel sequential 2>&1 | tee shard.log" + ./tools/mobile-test/android/run-suite-shard.sh ${{ matrix.shard }} "$ANDROID_SUITE_SHARDS" - name: Summarize shard ${{ matrix.shard }} if: always() @@ -166,7 +173,9 @@ jobs: if: always() with: name: full-suite-android-shard-${{ matrix.shard }} - path: shard.log + path: | + shard.log + shard-resources.log if-no-files-found: ignore ios: diff --git a/.github/workflows/verify-patches.yml b/.github/workflows/verify-patches.yml index 2bf3a9b312..4e00376a02 100644 --- a/.github/workflows/verify-patches.yml +++ b/.github/workflows/verify-patches.yml @@ -110,6 +110,9 @@ jobs: - name: Check mobile verdict isolation and final output run: python3 scripts/test-mobile-verdicts.py out + - name: Check Android full-suite coverage and failure propagation + run: python3 scripts/test-android-suite.py out + - name: Check manual prerelease version selection and guards run: python3 scripts/test-release-control.py diff --git a/README.md b/README.md index 0cd6398f02..f33e5725c4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ in-app, with npm modules and N-API native addons. Current line: **Node.js 26** (`v26.11.0`, upstream PR preview). This checkout pins [official release proposal #66546](https://github.com/nodejs/node/pull/66546) -at `7f68d75ee7963cd38ddc3783dd529f826bbde614`, before its merge/tag. +at `49072a0be4c7410982557aebf5b5c924fe7db597`, before its merge/tag. Builds are CI previews; [provenance and validation](docs/UPSTREAM-26.11.md) record how to move to the official tag after release. The stable Node 24 line continues independently on [`recipe`](https://github.com/fogtape/nodejs-mobile/tree/recipe). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9aa13b8df0..ec50858089 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -46,8 +46,14 @@ ## Unreleased, Version 26.11.0-0 (upstream PR preview) - Pin official Node.js release proposal [#66546](https://github.com/nodejs/node/pull/66546) - at `7f68d75ee7963cd38ddc3783dd529f826bbde614` and rebase the mobile patch series. + at `49072a0be4c7410982557aebf5b5c924fe7db597` and rebase the mobile patch series. This source upgrade does not arm publication. +- Include the proposal's HdrHistogram 0.12.0, NSS 3.129 certificate source, + WebCrypto/source-map fixtures and Alpine support documentation updates. +- Split the complete Android device suite into 16 shorter sessions, with at + most four running concurrently. Release the Gradle JVM after APK assembly + and publish resource diagnostics while tests run to investigate runner + shutdowns without excluding additional tests. - Carry forward the danmu lite ICU data profile, all web encoding converters, Chinese collation and Unicode normalization. Lite omits Temporal, FFI, Amaro, Inspector, SQLite/Web Storage, SEA and native object-print helpers; Android diff --git a/docs/TESTING.md b/docs/TESTING.md index 8fe2c140ec..45f0a87545 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -70,7 +70,7 @@ it through `tools/test.py`. | `build.yml` → `build-*` / `combine-*` | ubuntu / macos | PR · push `recipe` | the cross-compile actually succeeds — the only check that compiles target code | | `build.yml` → `smoke-{android,ios}` (+ the NAPI symbol assert in `combine-android`) | ubuntu+KVM / macos | PR · push `recipe` | **boot smoke**: the exact shipping artifact boots and runs JS; NAPI symbols in `.dynsym` | | `build.yml` → `curated-tests-android` / `curated-tests-ios` | ubuntu+KVM / macos | PR · push `recipe` · releases | **curated device tests**: the curated subset + crc-native addon load on an x86_64 emulator and arm64 simulator | -| `full-device-suite.yml` (also `build.yml` → `full-suite-android` / `full-suite-ios` on releases and on PRs that bump `upstream-base.txt`) | ubuntu+KVM / macos | nightly 03:00 UTC · dispatch · releases · upstream-bump PRs | **full device suite**: the whole non-`.status`-skipped `test/parallel` + `test/sequential` suite on both platforms, 4 round-robin shards each (`test.py --run=n,4`). The curated gate covers what someone chose; this covers everything else, so a test upstream adds tomorrow is picked up without anyone noticing it exists. On a PR that moves the pinned tag it also gates `ci-required` (via `full-suite-gate`): an upgrade carries whatever tests upstream added, and an allow-list gate cannot see them | +| `full-device-suite.yml` (also `build.yml` → `full-suite-android` / `full-suite-ios` on releases and on PRs that bump `upstream-base.txt`) | ubuntu+KVM / macos | nightly 03:00 UTC · dispatch · releases · upstream-bump PRs | **full device suite**: the whole non-`.status`-skipped `test/parallel` + `test/sequential` suite on both platforms, 16 Android / 4 iOS round-robin shards (`test.py --run=n,16` / `--run=n,4`). The curated gate covers what someone chose; this covers everything else, so a test upstream adds tomorrow is picked up without anyone noticing it exists. On a PR that moves the pinned tag it also gates `ci-required` (via `full-suite-gate`): an upgrade carries whatever tests upstream added, and an allow-list gate cannot see them | | `build.yml` → `real-device-smoke-android` / `real-device-smoke-ios` | ubuntu / macos-15 + BrowserStack | releases (untagged version of record; required to publish) · dispatch | **real-device smoke**: boot + crc-native addon load on physical devices — Android arm64 (Pixel 9, 16 KB pages) via Espresso and iPhone via XCUITest. The iOS leg runs **once per flavor** (`ios_flavors`, both by default): a simulator is a macOS process and is not subject to the address-space limits a real iOS process is, so simulator-only coverage cannot tell you a flavor boots on hardware. Android is still full-only. Needs `BROWSERSTACK_USER`/`BROWSERSTACK_PW` secrets. | Every job first **materializes** the source tree from the recipe branch @@ -139,7 +139,7 @@ for a PR and broad enough to be trusted: "did this change break something we already care about". - **The full device suite** — `full-device-suite.yml`. Everything `parallel.status` and `sequential.status` do not skip — a few thousand - tests per platform, split four ways. It answers "what is true on a device + tests per platform, split into 16 Android shards (four concurrent) and four iOS shards. It answers "what is true on a device that we have not looked at", which is the larger question: the curated gate covers about 6% of the runnable suite. `sequential`'s mobile skips are measured, not assumed — every non-structural skip covers a test that spawns diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md index 0c4f98866e..16fef20ad0 100644 --- a/docs/UPSTREAM-26.11.md +++ b/docs/UPSTREAM-26.11.md @@ -3,15 +3,21 @@ This recipe previews official Node.js release proposal [#66546](https://github.com/nodejs/node/pull/66546), titled `2026-10-07, Version 26.11.0 (Current)`. At preparation time on 2026-10-06, -it was open and in draft, with no official `v26.11.0` tag. +it was open and ready for review, with no official `v26.11.0` tag. `upstream-base.txt` pins the proposal's exact commit: -`7f68d75ee7963cd38ddc3783dd529f826bbde614`. +`49072a0be4c7410982557aebf5b5c924fe7db597`. The upstream version header reports `v26.11.0`; the mobile header reports `26.11.0-0`. These identifiers do not mean the official release or a mobile release has been published. The immutable commit and expected source-tree checksum identify this preview even if the proposal branch changes. +The refreshed pin includes HdrHistogram 0.12.0, NSS 3.129 certificate source +data, WebCrypto WPT and test426 fixture updates, plus the Alpine support +documentation added after the initial `7f68d75ee7` preview. The certificate +source update does not change `src/node_root_certs.h`. V8, ICU, Temporal, +FFI and the existing mobile/lite configuration are unchanged by this refresh. + ## Build and inspect Use `upgrade/node26.11-mobile-lite` for this preview. The Node 24 `recipe` @@ -56,11 +62,16 @@ The 22,081,472-byte reduction measures ICU data, not a compiled libnode or ZIP. - A fresh shallow fetch from `nodejs/node` applied all 22 regenerated patches directly; regeneration from that fresh tree preserved every recipe byte. The current recipe reconstructs tree - `e52cbddd35a391b2ae554be1b171e4febeffe5ec` after the Android test correction + `26d4f4525e6849205d82ccf0ccd33ea85bf42cce` after the upstream refresh and Android test correction described below. - 39 local regression tests passed: immutable SHA/tag/cache/CI baseline checks (5), flavor/ICU contracts (5), Rust/libffi toolchain actions (5), embedded verdict isolation (6), real shared-library symbol stripping (1), release control (17). +- Five additional Android shard regressions passed, including exact coverage + of all 4,127 runnable cases in 16 shards of 257–258 tests, and a failing + test process retaining its nonzero exit status through logging and cleanup. + HdrHistogram 0.12.0 compiled on Termux arm64 and passed recording, + percentile, bounds, extreme-value and four-thread atomic-recording probes. - Recipe ownership, YAML/shell/JavaScript syntax, all 219 curated test entries, zero upstream workflows and the test-edit audit passed. - All ten full GYP graphs generated successfully: Android arm/arm64/x64 and @@ -93,7 +104,26 @@ in-process `test-debugger-run-restart-init` and `test-debugger-wait-for-debugger remain runnable. Node's actual status-file parser verified that distinction. These exclusions correct test applicability; they do not explain the runner -shutdowns. A complete device-suite rerun is still required before merge. +shutdowns. The next [Build run 37441663775](https://github.com/fogtape/nodejs-mobile/actions/runs/37441663775) +again passed all ten builds, boot smokes and curated gates. Android shards +1, 2 and 3 then lost their runners with 514, 559 and 470 passed tests and +zero failed tests; QEMU reported unresponsive threads in two of those logs. +All four iOS full-suite shards passed. No new test exclusion is justified by +the Android failures. + +The Android full suite now uses 16 deterministic shards, with four jobs at +most running concurrently, to limit the number of app relaunches per emulator. +APK assembly uses Gradle's `--no-daemon --max-workers=2`, so its JVM exits +before the sweep. The AVD has explicit 2048 MB RAM / 256 MB Java heap settings. +The shard wrapper records host/guest memory, disk and process statistics at +startup, every minute and exit, in both the live job output and an artifact. +Its pipefail exit status retains actual test failures. The integrity gate +checks the workflow denominator and uses Node's actual test selector to verify +that every runnable parallel/sequential test appears exactly once. + +The runner logs do not prove an out-of-memory cause. These changes reduce +session length and resource contention and provide evidence for a future +failure. A complete device-suite rerun is still required before merge. Final artifact size measurements are separate from the ICU data measurement. ## Move to the official release diff --git a/expected-tree.txt b/expected-tree.txt index bf8a03d1dd..3dd7242c4c 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -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. -e52cbddd35a391b2ae554be1b171e4febeffe5ec +26d4f4525e6849205d82ccf0ccd33ea85bf42cce diff --git a/mobile-src/tools/mobile-test/android/prepare-android-test.sh b/mobile-src/tools/mobile-test/android/prepare-android-test.sh index 121188c075..14c9b75f8c 100755 --- a/mobile-src/tools/mobile-test/android/prepare-android-test.sh +++ b/mobile-src/tools/mobile-test/android/prepare-android-test.sh @@ -23,8 +23,9 @@ unlink "$NODEJS_BASE_DIR/test/fixtures/wasi/subdir/loop2" unlink "$NODEJS_BASE_DIR/test/fixtures/wasi/subdir/outside.txt" set -e -# Build the Android test app -( cd "$TEST_APP_BASE_DIR" && ./gradlew assembleDebug ) +# Build once without retaining a Gradle JVM during the emulator test sweep. +# The daemon can reserve 1.5 GB after APK assembly, alongside QEMU and the app. +( cd "$TEST_APP_BASE_DIR" && ./gradlew --no-daemon --max-workers=2 assembleDebug ) # Copy the Android proxy to the target directory. cp "$SCRIPT_BASE_DIR/node-android-proxy.sh" "$TEST_PROXY_TARGETDIR/node" diff --git a/mobile-src/tools/mobile-test/android/run-suite-shard.sh b/mobile-src/tools/mobile-test/android/run-suite-shard.sh new file mode 100755 index 0000000000..6057a83fec --- /dev/null +++ b/mobile-src/tools/mobile-test/android/run-suite-shard.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Keep the test verdict authoritative and publish runner resource diagnostics +# while the suite is running: a runner shutdown cannot upload post-step logs. +set -euo pipefail + +SHARD="${1:?usage: run-suite-shard.sh }" +SHARDS="${2:?usage: run-suite-shard.sh }" +if ! [[ "$SHARD" =~ ^[0-9]+$ && "$SHARDS" =~ ^[1-9][0-9]*$ ]] || + (( SHARD >= SHARDS )); then + echo "invalid Android shard $SHARD/$SHARDS" >&2 + exit 2 +fi + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "$ROOT" + +resource_snapshot() { + echo "Android shard $SHARD/$SHARDS resources at $(date -u +%FT%TZ)" + free -m || true + df -h . "${ANDROID_AVD_HOME:-${HOME}/.android/avd}" 2>/dev/null || true + ps -eo pid,comm,rss --sort=-rss | head -n 12 || true + for metric in /proc/pressure/memory /sys/fs/cgroup/memory.events; do + if [ -r "$metric" ]; then + echo "$metric" + cat "$metric" || true + fi + done + timeout 5 adb shell 'cat /proc/meminfo; df -h /data' 2>/dev/null || true +} + +report_resources() { + resource_snapshot | tee -a shard-resources.log +} + +MONITOR_PID="" +cleanup() { + if [ -n "$MONITOR_PID" ]; then + # Stop both the loop and its pending sleep/adb query. + pkill -P "$MONITOR_PID" 2>/dev/null || true + kill "$MONITOR_PID" 2>/dev/null || true + wait "$MONITOR_PID" 2>/dev/null || true + fi + report_resources +} +trap cleanup EXIT + +report_resources +( while sleep 60; do report_resources; done ) & +MONITOR_PID=$! + +# pipefail preserves test.py's failure; metrics and tee cannot turn a failed +# test into a successful job. Each deterministic shard runs once, no retries. +./tools/test.py -j 1 --flaky-tests=skip --timeout=300 --arch android \ + "--run=$SHARD,$SHARDS" parallel sequential 2>&1 | tee shard.log diff --git a/scripts/test-android-suite.py b/scripts/test-android-suite.py new file mode 100644 index 0000000000..a8b1203f01 --- /dev/null +++ b/scripts/test-android-suite.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Check Android shard coverage and preserve failures through the log pipeline.""" +import ast +import contextlib +import importlib.util +import io +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +import warnings + +RECIPE = Path(__file__).resolve().parent.parent +SOURCE = Path(sys.argv.pop(1)).resolve() if len(sys.argv) > 1 else RECIPE / 'out' +RUNNER = SOURCE / 'tools/mobile-test/android/run-suite-shard.sh' + + +def load_module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class QuietOutput(io.StringIO): + def reconfigure(self, **_): + pass + + +class AndroidSuite(unittest.TestCase): + def setUp(self): + workflow = (RECIPE / '.github/workflows/full-device-suite.yml').read_text() + android = workflow.split('\n android:\n', 1)[1].split('\n ios:\n', 1)[0] + self.shards = ast.literal_eval(re.search(r'^ shard: (.+)$', android, re.M)[1]) + self.count = int(re.search(r'^ ANDROID_SUITE_SHARDS: (\d+)$', android, re.M)[1]) + + def test_matrix_matches_denominator(self): + self.assertEqual(self.shards, list(range(self.count))) + + def test_real_test_selector_covers_every_runnable_case_once(self): + # Use upstream's actual classification and --run selector, replacing + # only device execution. This detects missing/duplicate coverage when + # the workflow matrix or a future upstream test runner changes. + sys.path.insert(0, str(SOURCE / 'tools')) + self.addCleanup(sys.path.remove, str(SOURCE / 'tools')) + runner = load_module('android_suite_test_runner', SOURCE / 'tools/test.py') + coverage = load_module('android_suite_coverage', + SOURCE / 'tools/mobile-test/coverage-manifest.py') + selected = [] + + def collect(cases, *_): + selected.extend((case.arch, case.mode, case.file, tuple(case.path)) + for case in cases) + return {'allPassed': True, 'failed': []} + + runner.RunTestCases = collect + saved_cwd, saved_argv = os.getcwd(), sys.argv + try: + os.chdir(SOURCE) + with tempfile.TemporaryDirectory() as folder, coverage.host_binary_present(): + stub = str(Path(folder) / 'stub-node') + coverage.write_stub(stub) + + def select(shard=None): + selected.clear() + sys.argv = ['tools/test.py', '-j', '1', '--flaky-tests=skip', + '--arch', 'android', '--shell', stub] + if shard is not None: + sys.argv.append(f'--run={shard},{self.count}') + sys.argv.extend(['parallel', 'sequential']) + with contextlib.redirect_stdout(QuietOutput()), warnings.catch_warnings(): + # Upstream Execute reads temporary files without an + # explicit close. This host-only selection check calls + # it repeatedly; keep those warnings out of CI output. + warnings.simplefilter('ignore', ResourceWarning) + self.assertEqual(runner.Main(), 0) + return list(selected) + + expected = select() + actual = [] + sizes = [] + for shard in self.shards: + cases = select(shard) + sizes.append(len(cases)) + actual.extend(cases) + self.assertTrue(expected) + self.assertEqual(len(actual), len(set(actual))) + self.assertCountEqual(actual, expected) + print(f'Android coverage: {len(expected)} runnable cases, ' + f'{self.count} shards of {min(sizes)}-{max(sizes)}; no gaps or duplicates') + finally: + os.chdir(saved_cwd) + sys.argv = saved_argv + + def run_fake_suite(self, exit_code, args): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + script = root / 'tools/mobile-test/android/run-suite-shard.sh' + script.parent.mkdir(parents=True) + shutil.copy2(RUNNER, script) + record = root / 'invocation.json' + test = root / 'tools/test.py' + test.write_text(f'#!{sys.executable}\nimport json,sys\n' + f'open({str(record)!r}, "w").write(json.dumps(sys.argv[1:]))\n' + 'print("suite output")\n' + f'sys.exit({exit_code})\n') + test.chmod(0o755) + # Keep diagnostics local; no device or adb daemon is required. + fake_adb = root / 'adb' + fake_adb.write_text('#!/bin/sh\nexit 0\n') + fake_adb.chmod(0o755) + proc = subprocess.run(['bash', str(script), *args], cwd=root, + env=dict(os.environ, PATH=str(root) + os.pathsep + os.environ['PATH']), + capture_output=True, text=True, timeout=20) + invocation = json.loads(record.read_text()) if record.exists() else None + log = (root / 'shard.log').read_text() if (root / 'shard.log').exists() else '' + resources = (root / 'shard-resources.log').exists() + return proc, invocation, log, resources + + def test_success_keeps_test_output_and_selector(self): + proc, invocation, log, resources = self.run_fake_suite(0, ['3', str(self.count)]) + self.assertEqual(proc.returncode, 0, proc.stderr) + self.assertIn(f'--run=3,{self.count}', invocation) + self.assertEqual(invocation[-2:], ['parallel', 'sequential']) + self.assertIn('suite output', log) + self.assertTrue(resources) + + def test_failure_is_not_hidden_by_tee_or_cleanup(self): + proc, _, log, resources = self.run_fake_suite(7, ['0', str(self.count)]) + self.assertEqual(proc.returncode, 7, proc.stderr) + self.assertIn('suite output', log) + self.assertTrue(resources) + + def test_invalid_shard_cannot_run_tests(self): + for args in (['-1', '16'], ['16', '16'], ['0', '0'], ['x', '16']): + with self.subTest(args=args): + proc, invocation, _, _ = self.run_fake_suite(0, args) + self.assertEqual(proc.returncode, 2, proc.stderr) + self.assertIsNone(invocation) + + +if __name__ == '__main__': + unittest.main() diff --git a/upstream-base.txt b/upstream-base.txt index 3d45fd0180..0bb94b677e 100644 --- a/upstream-base.txt +++ b/upstream-base.txt @@ -1,4 +1,4 @@ # Node.js 26.11.0 release proposal, not yet an official release tag. # https://github.com/nodejs/node/pull/66546 (v26.11.0-proposal) # Pin the reviewed PR commit; do not follow the moving proposal branch. -7f68d75ee7963cd38ddc3783dd529f826bbde614 +49072a0be4c7410982557aebf5b5c924fe7db597 From b95aa64c3899479985eae09b42fd6518a7da46db Mon Sep 17 00:00:00 2001 From: fogtape Date: Wed, 7 Oct 2026 07:55:29 +0800 Subject: [PATCH 8/8] =?UTF-8?q?ci:=20=E4=BF=AE=E6=AD=A3=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF=20watch=20CLI=20=E6=8E=92=E9=99=A4=E5=B9=B6=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E6=B5=8B=E8=AF=95=E6=97=A5=E5=BF=97=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/upload-test-log/action.yml | 46 +++++++++++++++++++ .github/workflows/full-device-suite.yml | 8 ++-- docs/CHANGELOG.md | 4 ++ docs/TESTING.md | 3 ++ docs/UPSTREAM-26.11.md | 29 ++++++++++-- expected-tree.txt | 2 +- ...parallel-and-sequential-tests-for-mo.patch | 14 ++++-- scripts/test-android-suite.py | 26 +++++++++++ 8 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 .github/actions/upload-test-log/action.yml diff --git a/.github/actions/upload-test-log/action.yml b/.github/actions/upload-test-log/action.yml new file mode 100644 index 0000000000..6415551702 --- /dev/null +++ b/.github/actions/upload-test-log/action.yml @@ -0,0 +1,46 @@ +name: Upload diagnostic test logs +description: > + Upload test logs with one retry for transient artifact-service failures. + Logs are best effort; the test step remains the required pass/fail gate. +inputs: + name: + description: Artifact name + required: true + path: + description: Log paths, one per line + required: true +runs: + using: composite + steps: + - name: Upload logs + id: upload + continue-on-error: true + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.name }} + path: ${{ inputs.path }} + if-no-files-found: ignore + + - name: Wait before retrying the artifact service + if: always() && steps.upload.outcome == 'failure' + shell: bash + run: sleep 15 + + - name: Retry log upload + id: retry + if: always() && steps.upload.outcome == 'failure' + continue-on-error: true + uses: actions/upload-artifact@v7 + with: + # A failed response can still leave an artifact on the service. A + # distinct name avoids conflicts without deleting the first upload. + name: ${{ inputs.name }}-retry + path: ${{ inputs.path }} + if-no-files-found: ignore + + - name: Report unavailable log artifact + if: always() && steps.upload.outcome == 'failure' && steps.retry.outcome == 'failure' + shell: bash + run: | + echo '::warning::Test log upload failed twice; use the live job log and test summary. The test verdict is unchanged.' + echo 'Test log artifact unavailable after two upload attempts. See the live job log and the test summary above.' >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/full-device-suite.yml b/.github/workflows/full-device-suite.yml index 7069e8c6c5..af1b36707d 100644 --- a/.github/workflows/full-device-suite.yml +++ b/.github/workflows/full-device-suite.yml @@ -169,14 +169,15 @@ jobs: run: | ./tools/mobile-test/summarize-suite-run.sh shard.log "android shard ${{ matrix.shard }}" - - uses: actions/upload-artifact@v7 + # Log transport must not change the device-test verdict. Binary uploads + # in Build remain required; only these diagnostic logs are best effort. + - uses: ./.github/actions/upload-test-log if: always() with: name: full-suite-android-shard-${{ matrix.shard }} path: | shard.log shard-resources.log - if-no-files-found: ignore ios: needs: precheck @@ -234,12 +235,11 @@ jobs: run: | ./tools/mobile-test/summarize-suite-run.sh shard.log "ios shard ${{ matrix.shard }}" - - uses: actions/upload-artifact@v7 + - uses: ./.github/actions/upload-test-log if: always() with: name: full-suite-ios-shard-${{ matrix.shard }} path: shard.log - if-no-files-found: ignore - name: Shutdown simulator if: always() diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ec50858089..3998a1932d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -54,6 +54,10 @@ most four running concurrently. Release the Gradle JVM after APK assembly and publish resource diagnostics while tests run to investigate runner shutdowns without excluding additional tests. +- Exclude the watch-mode CLI subprocess tests on mobile, matching the + existing child-Node limitation; in-process debugger and filesystem-watch + tests keep their current coverage. Retry diagnostic log uploads once and + report artifact-service failures separately from the device-test verdict. - Carry forward the danmu lite ICU data profile, all web encoding converters, Chinese collation and Unicode normalization. Lite omits Temporal, FFI, Amaro, Inspector, SQLite/Web Storage, SEA and native object-print helpers; Android diff --git a/docs/TESTING.md b/docs/TESTING.md index 45f0a87545..71b10ecf69 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -164,6 +164,9 @@ per-PR because it costs about eight device-hours per run, which the PR loop cannot absorb; the nightly covers drift the rest of the time. Each shard writes a summary (counts, plus the failing names, with hangs and crashes counted separately — they mean different things) and uploads its log. +Diagnostic log uploads retry once after a service failure, then warn if the +artifact is still unavailable. The live job output and test summary remain +available; test failures and required binary-artifact uploads still fail CI. ### The curated subset diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md index 16fef20ad0..9dd1023ec2 100644 --- a/docs/UPSTREAM-26.11.md +++ b/docs/UPSTREAM-26.11.md @@ -62,14 +62,17 @@ The 22,081,472-byte reduction measures ICU data, not a compiled libnode or ZIP. - A fresh shallow fetch from `nodejs/node` applied all 22 regenerated patches directly; regeneration from that fresh tree preserved every recipe byte. The current recipe reconstructs tree - `26d4f4525e6849205d82ccf0ccd33ea85bf42cce` after the upstream refresh and Android test correction + `366de32e0018c646ea07608448b694716229b35b` after the upstream refresh and Android test correction described below. - 39 local regression tests passed: immutable SHA/tag/cache/CI baseline checks (5), flavor/ICU contracts (5), Rust/libffi toolchain actions (5), embedded verdict isolation (6), real shared-library symbol stripping (1), release control (17). -- Five additional Android shard regressions passed, including exact coverage - of all 4,127 runnable cases in 16 shards of 257–258 tests, and a failing +- Six additional Android shard regressions passed, including exact coverage + of all 4,125 runnable cases in 16 shards of 257–258 tests, and a failing test process retaining its nonzero exit status through logging and cleanup. + The actual Node status parser verifies watch-mode CLI exclusions on both + mobile platforms while retaining in-process debugger/filesystem tests and + leaving the two CLI tests runnable on desktop Linux. HdrHistogram 0.12.0 compiled on Termux arm64 and passed recording, percentile, bounds, extreme-value and four-thread atomic-recording probes. - Recipe ownership, YAML/shell/JavaScript syntax, all 219 curated test entries, @@ -126,6 +129,26 @@ session length and resource contention and provide evidence for a future failure. A complete device-suite rerun is still required before merge. Final artifact size measurements are separate from the ICU data measurement. +The [refreshed Build run 37473151347](https://github.com/fogtape/nodejs-mobile/actions/runs/37473151347) +passed all ten builds, boot smokes and curated gates. All 16 Android shards +finished without a runner shutdown; 14 passed, while shards 8 and 9 exposed +two further CLI tests, `test-watch-mode-inspect` and `test-watch-mode`. Their +independent child Node attempts failed with dalvik-cache permission errors +and SIGABRT. Android now excludes those two specific tests. iOS already +excluded `test-watch-mode`; `test-watch-mode-inspect` now has the same +child-process exclusion because `NodeInstance` uses a separate executable. +The simulator permitted that spawn, which a shipped iOS app cannot use. + +All four iOS shards passed their test sweeps. Shard 0's 1,015 passing tests +were followed by `Failed to CreateArtifact: Unable to make request: ENOTFOUND` +from the diagnostic log upload, causing the job and full-suite gate to fail. +The log action now retries once after 15 seconds, using a distinct retry name +to avoid a partial-upload collision. If both attempts fail, it emits a warning +and a job-summary note; the live test log and preceding summary remain +available. Only diagnostic log uploads are best effort: test execution and +binary-artifact uploads remain required. These corrections require another +complete device-suite run. + ## Move to the official release After upstream publishes `v26.11.0`, compare its commit against this pin, diff --git a/expected-tree.txt b/expected-tree.txt index 3dd7242c4c..1547f16ce6 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -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. -26d4f4525e6849205d82ccf0ccd33ea85bf42cce +366de32e0018c646ea07608448b694716229b35b diff --git a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch index ce6ee26e3a..8c2d8e2157 100644 --- a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch +++ b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch @@ -60,11 +60,11 @@ Assisted-by: Codex test/parallel/test-vfs-fs-readFileSync.js | 3 +- test/parallel/test-vfs-fs-writeFileSync.js | 3 +- .../test-watch-mode-files_watcher.mjs | 3 + - test/sequential/sequential.status | 137 ++ + test/sequential/sequential.status | 143 ++ test/sequential/test-async-wrap-getasyncid.js | 15 +- test/sequential/test-fs-watch.js | 4 + ...test-vm-timeout-escape-promise-module-2.js | 6 +- - 45 files changed, 2041 insertions(+), 66 deletions(-) + 45 files changed, 2047 insertions(+), 66 deletions(-) diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status index 0fe217d4e4..4578a1cf97 100644 @@ -2616,10 +2616,10 @@ index 10e078286e..9c4ec8787d 100644 + */ }); diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status -index f155b67473..2e1401325e 100644 +index f155b67473..c052ca2ceb 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status -@@ -33,6 +33,143 @@ test-perf-hooks: SKIP +@@ -33,6 +33,149 @@ test-perf-hooks: SKIP # https://github.com/nodejs/node/issues/39683 test-tls-psk-client: PASS, FLAKY @@ -2653,6 +2653,10 @@ index f155b67473..2e1401325e 100644 +test-stream2-stderr-sync: SKIP +test-util-debug: SKIP +test-vm-timeout-rethrow: SKIP ++# These CLI tests spawn execPath (app_process), which aborts outside zygote. ++# Confirmed in Build 37473151347: dalvik-cache ownership / SIGABRT. ++test-watch-mode: SKIP ++test-watch-mode-inspect: SKIP +test-worker-prof: SKIP +# +# Tests that require the inspector, not included in the build @@ -2714,6 +2718,8 @@ index f155b67473..2e1401325e 100644 +test-stream2-stderr-sync: SKIP +test-util-debug: SKIP +test-vm-timeout-rethrow: SKIP ++# NodeInstance launches a child Node, permitted only on the simulator. ++test-watch-mode-inspect: SKIP +test-worker-prof: SKIP +# +# Tests that require the inspector, not included in the build diff --git a/scripts/test-android-suite.py b/scripts/test-android-suite.py index a8b1203f01..6494a1128d 100644 --- a/scripts/test-android-suite.py +++ b/scripts/test-android-suite.py @@ -12,6 +12,7 @@ import subprocess import sys import tempfile +from types import SimpleNamespace import unittest import warnings @@ -42,6 +43,31 @@ def setUp(self): def test_matrix_matches_denominator(self): self.assertEqual(self.shards, list(range(self.count))) + def test_watch_cli_exclusions_preserve_in_process_device_tests(self): + sys.path.insert(0, str(SOURCE / 'tools')) + self.addCleanup(sys.path.remove, str(SOURCE / 'tools')) + runner = load_module('mobile_watch_status_runner', SOURCE / 'tools/test.py') + for suite, names in { + 'sequential': ['test-watch-mode', 'test-watch-mode-inspect', 'test-tls-connect'], + 'parallel': ['test-debugger-run-restart-init', 'test-debugger-wait-for-debugger', + 'test-fs-watch', 'test-fs-watch-persistent'], + }.items(): + sections, defs = [], {} + with warnings.catch_warnings(): + warnings.simplefilter('ignore', ResourceWarning) + runner.ReadConfigurationInto(str(SOURCE / 'test' / suite / (suite + '.status')), + sections, defs) + config = runner.Configuration(sections, defs) + for platform in ('android', 'ios', 'linux'): + with self.subTest(suite=suite, platform=platform): + cases = [SimpleNamespace(path=[suite, name]) for name in names] + env = dict(system=platform, arch='x64', mode='release', type='simple', + asan='off', pointer_compression='false') + classified, _ = config.ClassifyTests(cases, env) + skipped = {case.path[-1] for case in classified if runner.SKIP in case.outcomes} + expected = set(names[:2]) if suite == 'sequential' and platform != 'linux' else set() + self.assertEqual(skipped, expected) + def test_real_test_selector_covers_every_runnable_case_once(self): # Use upstream's actual classification and --run selector, replacing # only device execution. This detects missing/duplicate coverage when