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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/nx/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions packages/nx/src/command-line/report/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<NxPackageJson>(
require.resolve('nx/package.json')
Expand Down Expand Up @@ -86,6 +87,7 @@ export async function reportHandler() {
mismatchedNxVersions,
projectGraphError,
nativeTarget,
nativeRuntime,
cache,
daemon,
} = await getReportData();
Expand All @@ -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',
Expand Down Expand Up @@ -309,6 +312,7 @@ export interface ReportData {
}>;
projectGraphError?: Error | null;
nativeTarget: string | null;
nativeRuntime: string | null;
cache: {
max: number;
used: number;
Expand Down Expand Up @@ -443,6 +447,7 @@ export async function getReportData(): Promise<ReportData> {
mismatchedNxVersions,
projectGraphError,
nativeTarget: native ? native.getBinaryTarget() : null,
nativeRuntime: native ? getNativeRuntime(native.IS_WASM) : null,
cache,
daemon: await getDaemonStatus(),
};
Expand Down Expand Up @@ -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');
Expand Down
27 changes: 27 additions & 0 deletions packages/nx/src/native/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
46 changes: 46 additions & 0 deletions packages/nx/src/native/native-package-resolution.ts
Original file line number Diff line number Diff line change
@@ -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;
}
79 changes: 79 additions & 0 deletions packages/nx/src/native/wasm-fallback-warning.js
Original file line number Diff line number Diff line change
@@ -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 };
105 changes: 105 additions & 0 deletions packages/nx/src/native/wasm-fallback-warning.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const {
getExpectedNativePackage,
getWasmFallbackWarning,
} = require('./wasm-fallback-warning');

const gnu = () => false;
const musl = () => true;

const brokenInstall = (overrides: Record<string, unknown> = {}) => ({
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();
});
});
Loading