diff --git a/astro-docs/src/content/docs/reference/environment-variables.mdoc b/astro-docs/src/content/docs/reference/environment-variables.mdoc index 3cdd98d4af1..c756afdc9c8 100644 --- a/astro-docs/src/content/docs/reference/environment-variables.mdoc +++ b/astro-docs/src/content/docs/reference/environment-variables.mdoc @@ -168,6 +168,7 @@ The following environment variables are ones that you can set to change the beha | Property | Type | Description | | ----------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NX_ALLOW_WASM_FALLBACK` | boolean | If set to `true`, silences the warning Nx prints when it falls back to the WebAssembly runtime because the native binary for the current platform is not installed. Useful when running on a platform without a prebuilt native package and WASM fallback is intentional. | | `NX_BAIL` | boolean | If set to `true`, Nx will stop command execution after the first failed task. Can be overridden on the command line with `--nxBail`. | | `NX_BASE` | string | The default base branch to use when calculating the affected projects. Can be overridden on the command line with `--base`. | | `NX_BATCH_MODE` | boolean | If set to `true`, Nx will run task(s) in batches for executors which support batches. | @@ -238,6 +239,7 @@ The following environment variables are ones that you can set to change the beha | `NX_TASKS_RUNNER` | string | The name of task runner from the config to use. Can be overridden on the command line with `--runner`. Preferred over `NX_RUNNER`. | | `NX_TASKS_RUNNER_DYNAMIC_OUTPUT` | boolean | If set to `false`, will use non-dynamic terminal output strategy (what you see in CI), even when you terminal can support the dynamic version | | `NX_USE_V8_SERIALIZER` | boolean | If set to `true`, Nx will use v8 serialization in the pseudo-IPC channel between the daemon and task processes instead of JSON. Improves throughput for workspaces with large task payloads. | +| `NX_WASM_FALLBACK_WARNED` | boolean | Internal. Propagated by Nx to child processes to suppress repeated WASM-fallback warnings within the same process tree. Do not set this manually; set `NX_ALLOW_WASM_FALLBACK=true` instead to silence the warning. | | `NX_WRAPPER_SKIP_INSTALL` | boolean | If set to `true`, the `.nx/nxw.js` wrapper skips verifying and self-installing the pinned Nx version before each command. | ## Plugin environment variables diff --git a/packages/nx/project.json b/packages/nx/project.json index 670a0379956..2a8616260b1 100644 --- a/packages/nx/project.json +++ b/packages/nx/project.json @@ -15,7 +15,7 @@ "inputs": ["native"], "outputs": [ "{projectRoot}/src/native/*.wasm", - "{projectRoot}/src/native/!(index|browser).js", + "{projectRoot}/src/native/!(index|browser|wasm-fallback-warning).js", "{projectRoot}/src/native/*.cjs", "{projectRoot}/src/native/*.mjs", "{projectRoot}/src/native/index.d.ts" @@ -32,7 +32,7 @@ "outputs": [ "{projectRoot}/src/native/*.node", "{projectRoot}/src/native/*.wasm", - "{projectRoot}/src/native/!(index|browser).js", + "{projectRoot}/src/native/!(index|browser|wasm-fallback-warning).js", "{projectRoot}/src/native/index.d.ts" ], "executor": "@monodon/rust:napi", diff --git a/packages/nx/src/command-line/report/report.ts b/packages/nx/src/command-line/report/report.ts index fffcc720374..9888abb55e8 100644 --- a/packages/nx/src/command-line/report/report.ts +++ b/packages/nx/src/command-line/report/report.ts @@ -41,6 +41,7 @@ import { } from '../../tasks-runner/cache'; import { daemonClient } from '../../daemon/client/client'; import { readNxPackageGroup } from '../../utils/nx-package-group'; +import { getMissingNativePackage } from '../../native/native-package-resolution'; const nxPackageJson = readJsonFile( require.resolve('nx/package.json') @@ -86,6 +87,7 @@ export async function reportHandler() { mismatchedNxVersions, projectGraphError, nativeTarget, + nativeRuntime, cache, daemon, } = await getReportData(); @@ -94,6 +96,7 @@ export async function reportHandler() { ['Node', process.versions.node], ['OS', `${process.platform}-${process.arch}`], ['Native Target', nativeTarget ?? 'Unavailable'], + ['Native runtime', nativeRuntime ?? 'Unavailable'], [pm, pmVersion], [ 'daemon', @@ -309,6 +312,7 @@ export interface ReportData { }>; projectGraphError?: Error | null; nativeTarget: string | null; + nativeRuntime: string | null; cache: { max: number; used: number; @@ -443,6 +447,7 @@ export async function getReportData(): Promise { mismatchedNxVersions, projectGraphError, nativeTarget: native ? native.getBinaryTarget() : null, + nativeRuntime: native ? getNativeRuntime(native.IS_WASM) : null, cache, daemon: await getDaemonStatus(), }; @@ -599,6 +604,15 @@ export function findInstalledPackagesWeCareAbout() { })); } +function getNativeRuntime(isWasm: boolean): string { + if (!isWasm) { + return 'native'; + } + // Only name a package when it is genuinely absent; WASM is expected where the binary cannot load. + const missing = getMissingNativePackage(); + return missing ? `wasm (missing ${missing})` : 'wasm'; +} + function isNativeAvailable(): typeof import('../../native') | false { try { return require('../../native'); diff --git a/packages/nx/src/native/index.js b/packages/nx/src/native/index.js index e549ed19cfe..cfcee740451 100644 --- a/packages/nx/src/native/index.js +++ b/packages/nx/src/native/index.js @@ -6,10 +6,16 @@ const { renameSync, statSync, unlinkSync, + writeSync, } = require('fs'); const Module = require('module'); const { nxVersion } = require('../utils/versions'); const { getNativeFileCacheLocation } = require('./native-file-cache-location'); +const { getWasmFallbackWarning } = require('./wasm-fallback-warning'); +const { + isMusl, + getMissingNativePackage, +} = require('./native-package-resolution'); const MAX_COPY_RETRIES = 3; @@ -169,5 +175,26 @@ const indexModulePath = require.resolve('./native-bindings.js'); delete require.cache[indexModulePath]; const indexModule = require('./native-bindings.js'); +if (indexModule.IS_WASM) { + try { + const warning = getWasmFallbackWarning({ + platform: process.platform, + arch: process.arch, + isMusl, + env: process.env, + nativePackageResolvable: getMissingNativePackage() === null, + }); + if (warning) { + // Sync write: once the synchronous WASM work blocks the event loop a queued async + // stderr write cannot drain, so console/logger output would be lost on a piped stderr. + writeSync(process.stderr.fd, warning); + // Propagates to the many processes nx spawns so they do not each repeat the warning. + process.env.NX_WASM_FALLBACK_WARNED = 'true'; + } + } catch (e) { + // a failed warning must never stop nx from loading + } +} + module.exports = indexModule; Module._load = originalLoad; diff --git a/packages/nx/src/native/native-package-resolution.ts b/packages/nx/src/native/native-package-resolution.ts new file mode 100644 index 00000000000..ae37ea01e0f --- /dev/null +++ b/packages/nx/src/native/native-package-resolution.ts @@ -0,0 +1,46 @@ +import { readFileSync } from 'fs'; + +// require rather than import: the helper is plain CommonJS and is not part of this project's ts file list. +const { getExpectedNativePackage } = require('./wasm-fallback-warning'); + +// Mirrors the musl detection in the generated native-bindings.js, which does not export it. +export function isMusl(): boolean { + if (process.platform !== 'linux') { + return false; + } + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl'); + } catch (e) { + (process.report as any).excludeNetwork = true; + return !(process.report.getReport() as any).header.glibcVersionRuntime; + } +} + +function canResolve(request: string): boolean { + try { + require.resolve(request); + return true; + } catch (e) { + // Anything other than a missing module means the binary is installed but will not load, which + // is normal in WebContainers. Treat it as present so only a genuinely absent package is flagged. + return e?.code !== 'MODULE_NOT_FOUND'; + } +} + +/** + * The native package this platform expects, but only when it cannot be resolved at all. + * Returns null when the package is installed, or when the platform ships no prebuilt binary. + */ +export function getMissingNativePackage(): string | null { + const expected = getExpectedNativePackage({ + platform: process.platform, + arch: process.arch, + isMusl, + }); + if (!expected) { + return null; + } + // native-bindings.js accepts either the npm package or a binary built into this directory. + const localBinary = `./nx.${expected.slice('@nx/nx-'.length)}.node`; + return canResolve(expected) || canResolve(localBinary) ? null : expected; +} diff --git a/packages/nx/src/native/wasm-fallback-warning.js b/packages/nx/src/native/wasm-fallback-warning.js new file mode 100644 index 00000000000..e3678f41910 --- /dev/null +++ b/packages/nx/src/native/wasm-fallback-warning.js @@ -0,0 +1,79 @@ +// Keep in sync with the `nxPackages` set in ./index.js. A platform missing here is treated as +// WASM-only and stays silent, so an entry that drifts turns the warning off rather than breaking it. +const NATIVE_PACKAGES = { + 'android-arm': '@nx/nx-android-arm-eabi', + 'android-arm64': '@nx/nx-android-arm64', + 'darwin-arm64': '@nx/nx-darwin-arm64', + 'darwin-x64': '@nx/nx-darwin-x64', + 'freebsd-x64': '@nx/nx-freebsd-x64', + 'linux-arm': '@nx/nx-linux-arm-gnueabihf', + 'linux-arm64': '@nx/nx-linux-arm64-{libc}', + 'linux-x64': '@nx/nx-linux-x64-{libc}', + 'win32-arm64': '@nx/nx-win32-arm64-msvc', + 'win32-ia32': '@nx/nx-win32-ia32-msvc', + 'win32-x64': '@nx/nx-win32-x64-msvc', +}; + +function getExpectedNativePackage({ platform, arch, isMusl } = {}) { + const template = NATIVE_PACKAGES[`${platform}-${arch}`]; + if (!template) { + return null; + } + + let libc; + try { + libc = isMusl() ? 'musl' : 'gnu'; + } catch (e) { + libc = 'gnu'; + } + return template.replace('{libc}', libc); +} + +function getWasmFallbackWarning({ + platform, + arch, + isMusl, + env = {}, + nativePackageResolvable, +} = {}) { + if ( + env.NAPI_RS_FORCE_WASI || + env.NX_ALLOW_WASM_FALLBACK === 'true' || + env.NX_WASM_FALLBACK_WARNED === 'true' + ) { + return null; + } + + // The package being resolvable means it installed fine and failed to load, which is normal in + // WebContainers/StackBlitz. Only an unresolvable package is the broken install this warns about. + if (nativePackageResolvable === true) { + return null; + } + + const pkg = getExpectedNativePackage({ platform, arch, isMusl }); + if (!pkg) { + return null; + } + + return [ + '', + ' NX Nx could not load its native binary and fell back to the WebAssembly runtime.', + '', + ' The WebAssembly runtime is much slower than the native binary and can look like a hang', + ' on large workspaces.', + '', + ` This install is missing ${pkg}.`, + '', + ' The most common cause is a lockfile that lost its platform optionalDependencies, for', + ' example one regenerated on a different operating system (npm/cli#4828) or resolved', + ' after a merge conflict.', + '', + ` Recreate the lockfile so that ${pkg} is present, then reinstall.`, + '', + ' Set NX_ALLOW_WASM_FALLBACK=true to silence this warning.', + '', + '', + ].join('\n'); +} + +module.exports = { getExpectedNativePackage, getWasmFallbackWarning }; diff --git a/packages/nx/src/native/wasm-fallback-warning.spec.ts b/packages/nx/src/native/wasm-fallback-warning.spec.ts new file mode 100644 index 00000000000..5d0804aa6c1 --- /dev/null +++ b/packages/nx/src/native/wasm-fallback-warning.spec.ts @@ -0,0 +1,105 @@ +const { + getExpectedNativePackage, + getWasmFallbackWarning, +} = require('./wasm-fallback-warning'); + +const gnu = () => false; +const musl = () => true; + +const brokenInstall = (overrides: Record = {}) => ({ + platform: 'linux', + arch: 'x64', + isMusl: gnu, + env: {}, + nativePackageResolvable: false, + ...overrides, +}); + +describe('getExpectedNativePackage', () => { + it('names the gnu package on linux x64', () => { + expect( + getExpectedNativePackage({ platform: 'linux', arch: 'x64', isMusl: gnu }) + ).toBe('@nx/nx-linux-x64-gnu'); + }); + + it('names the musl package on linux x64 with musl libc', () => { + expect( + getExpectedNativePackage({ platform: 'linux', arch: 'x64', isMusl: musl }) + ).toBe('@nx/nx-linux-x64-musl'); + }); + + it('names the darwin arm64 package', () => { + expect( + getExpectedNativePackage({ + platform: 'darwin', + arch: 'arm64', + isMusl: gnu, + }) + ).toBe('@nx/nx-darwin-arm64'); + }); + + it('ignores libc where only one variant is published', () => { + expect( + getExpectedNativePackage({ platform: 'linux', arch: 'arm', isMusl: musl }) + ).toBe('@nx/nx-linux-arm-gnueabihf'); + }); + + it('falls back to the gnu package when libc detection throws', () => { + expect( + getExpectedNativePackage({ + platform: 'linux', + arch: 'x64', + isMusl: () => { + throw new Error('ldd is not available'); + }, + }) + ).toBe('@nx/nx-linux-x64-gnu'); + }); + + it('returns null on platforms that have no prebuilt native package', () => { + expect( + getExpectedNativePackage({ platform: 'sunos', arch: 'x64', isMusl: gnu }) + ).toBeNull(); + }); +}); + +describe('getWasmFallbackWarning', () => { + it('warns when the expected package cannot be resolved', () => { + const warning = getWasmFallbackWarning(brokenInstall()); + + expect(warning).toContain('@nx/nx-linux-x64-gnu'); + expect(warning).toContain('WebAssembly'); + expect(warning).toContain('optionalDependencies'); + expect(warning).toContain('NX_ALLOW_WASM_FALLBACK=true'); + }); + + it('names the musl package when the install is musl based', () => { + expect(getWasmFallbackWarning(brokenInstall({ isMusl: musl }))).toContain( + '@nx/nx-linux-x64-musl' + ); + }); + + // WebContainers/StackBlitz report linux/x64 and install @nx/nx-linux-x64-gnu normally, but + // dlopen fails, so nx legitimately runs WASM. That is not a broken install. + it('stays quiet when the expected package resolves but the binary will not load', () => { + expect( + getWasmFallbackWarning(brokenInstall({ nativePackageResolvable: true })) + ).toBeNull(); + }); + + it('stays quiet on platforms that have no prebuilt native package', () => { + expect( + getWasmFallbackWarning(brokenInstall({ platform: 'sunos' })) + ).toBeNull(); + }); + + it.each([ + ['NAPI_RS_FORCE_WASI', '1'], + ['NX_ALLOW_WASM_FALLBACK', 'true'], + ['NX_WASM_FALLBACK_WARNED', 'true'], + ])('stays quiet when %s is set', (name, value) => { + expect( + getWasmFallbackWarning(brokenInstall({ env: { [name]: value } })) + ).toBeNull(); + }); +});