@napi-rs/cli 3.7.1 → 3.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/cli.js +213 -63
  2. package/dist/index.cjs +212 -62
  3. package/dist/index.d.cts +4 -4
  4. package/dist/index.d.ts +4 -4
  5. package/dist/index.js +213 -63
  6. package/package.json +4 -4
  7. package/src/api/__tests__/__snapshots__/templates.spec.ts.md +353 -0
  8. package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
  9. package/src/api/__tests__/android-build.spec.ts +113 -0
  10. package/src/api/__tests__/artifacts.spec.ts +95 -0
  11. package/src/api/__tests__/build-regressions.spec.ts +128 -0
  12. package/src/api/__tests__/build.spec.ts +1020 -0
  13. package/src/api/__tests__/create-npm-dirs.spec.ts +561 -0
  14. package/src/api/__tests__/new-node-api-version.spec.ts +87 -0
  15. package/src/api/__tests__/new.spec.ts +527 -0
  16. package/src/api/__tests__/rename.spec.ts +181 -0
  17. package/src/api/__tests__/templates.spec.ts +164 -0
  18. package/src/api/__tests__/universalize.spec.ts +128 -0
  19. package/src/api/artifacts.ts +7 -16
  20. package/src/api/build.ts +346 -105
  21. package/src/api/new.ts +65 -1
  22. package/src/api/templates/js-binding.ts +12 -11
  23. package/src/commands/__tests__/new.spec.ts +13 -0
  24. package/src/def/build.ts +6 -6
  25. package/src/utils/__tests__/__fixtures__/napi_type_def +202 -0
  26. package/src/utils/__tests__/__fixtures__/runtime_string_enum_flag +2 -0
  27. package/src/utils/__tests__/__snapshots__/config.spec.ts.md +97 -0
  28. package/src/utils/__tests__/__snapshots__/config.spec.ts.snap +0 -0
  29. package/src/utils/__tests__/__snapshots__/target.spec.ts.md +180 -0
  30. package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
  31. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +1197 -0
  32. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
  33. package/src/utils/__tests__/__snapshots__/version.spec.ts.md +22 -0
  34. package/src/utils/__tests__/__snapshots__/version.spec.ts.snap +0 -0
  35. package/src/utils/__tests__/config.spec.ts +81 -0
  36. package/src/utils/__tests__/metadata.spec.ts +28 -0
  37. package/src/utils/__tests__/misc.spec.ts +56 -0
  38. package/src/utils/__tests__/target.spec.ts +19 -0
  39. package/src/utils/__tests__/typegen.spec.ts +85 -0
  40. package/src/utils/__tests__/version.spec.ts +7 -0
  41. package/src/utils/version.ts +2 -0
@@ -0,0 +1,1020 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from 'node:fs'
8
+ import { exec } from 'node:child_process'
9
+ import {
10
+ copyFile,
11
+ mkdir,
12
+ readFile,
13
+ readdir,
14
+ rm,
15
+ stat,
16
+ writeFile,
17
+ } from 'node:fs/promises'
18
+ import { tmpdir } from 'node:os'
19
+ import { join, dirname, resolve } from 'node:path'
20
+ import { join as posixJoin, sep as posixSep } from 'node:path/posix'
21
+ import { sep as win32Sep } from 'node:path/win32'
22
+ import { fileURLToPath } from 'node:url'
23
+
24
+ import ava, { type ExecutionContext, type TestFn } from 'ava'
25
+
26
+ import {
27
+ buildProject,
28
+ generateTypeDef,
29
+ napiCrossToolchainEnvs,
30
+ validateCrossCompileFlags,
31
+ validateNapiCrossSupport,
32
+ writeJsBinding,
33
+ } from '../build.js'
34
+ import { getSystemDefaultTarget } from '../../utils/index.js'
35
+
36
+ const __dirname = dirname(fileURLToPath(import.meta.url))
37
+ const repoRoot = resolve(__dirname, '../../../..')
38
+
39
+ const test = ava as TestFn<{
40
+ tmpDir: string
41
+ projectDir: string
42
+ typeDefDir: string
43
+ }>
44
+
45
+ test.beforeEach(async (t) => {
46
+ const timestamp = Date.now()
47
+ const random = Math.random().toString(36).substring(7)
48
+ const tmpDir = posixJoin(
49
+ tmpdir(),
50
+ 'napi-rs-test',
51
+ `build-spec-${timestamp}-${random}`,
52
+ )
53
+ const projectDir = posixJoin(tmpDir, 'project')
54
+ const typeDefDir = posixJoin(projectDir, 'target', 'type-def')
55
+
56
+ await mkdir(typeDefDir, { recursive: true })
57
+
58
+ t.context = { tmpDir, projectDir, typeDefDir }
59
+ })
60
+
61
+ test.afterEach.always(async (t) => {
62
+ if (existsSync(t.context.tmpDir)) {
63
+ await rm(t.context.tmpDir, { recursive: true, force: true })
64
+ }
65
+ })
66
+
67
+ test('build pipeline generates bindings and artifacts', async (t) => {
68
+ const { projectDir, typeDefDir } = t.context
69
+ const crateName = 'build_integration'
70
+ const binaryName = 'build-integration'
71
+ const packageName = 'build-integration'
72
+ const version = '0.1.0'
73
+ const target = getSystemDefaultTarget()
74
+
75
+ const napiPath = posixJoin(repoRoot, 'crates', 'napi').replaceAll(
76
+ win32Sep,
77
+ posixSep,
78
+ )
79
+ const napiDerivePath = posixJoin(repoRoot, 'crates', 'macro').replaceAll(
80
+ win32Sep,
81
+ posixSep,
82
+ )
83
+ const napiBuildPath = posixJoin(repoRoot, 'crates', 'build').replaceAll(
84
+ win32Sep,
85
+ posixSep,
86
+ )
87
+
88
+ await mkdir(join(projectDir, 'src'), { recursive: true })
89
+
90
+ const cargoToml = `[package]
91
+ name = "${crateName}"
92
+ version = "${version}"
93
+ edition = "2021"
94
+
95
+ [lib]
96
+ crate-type = ["cdylib"]
97
+
98
+ [dependencies]
99
+ napi = { path = "${napiPath}" }
100
+ napi-derive = { path = "${napiDerivePath}" }
101
+
102
+ [build-dependencies]
103
+ napi-build = { path = "${napiBuildPath}" }
104
+ `
105
+
106
+ await writeFile(join(projectDir, 'Cargo.toml'), cargoToml)
107
+ await writeFile(
108
+ join(projectDir, 'package.json'),
109
+ `${JSON.stringify(
110
+ {
111
+ name: packageName,
112
+ version,
113
+ napi: {
114
+ binaryName,
115
+ targets: [target.triple],
116
+ },
117
+ },
118
+ null,
119
+ 2,
120
+ )}\n`,
121
+ )
122
+ await writeFile(
123
+ join(projectDir, 'build.rs'),
124
+ 'fn main() {\n napi_build::setup();\n}\n',
125
+ )
126
+ await writeFile(
127
+ join(projectDir, 'src', 'lib.rs'),
128
+ 'use napi_derive::napi;\n\n#[napi]\npub fn sum(a: i32, b: i32) -> i32 {\n a + b\n}\n',
129
+ )
130
+
131
+ const buildCmd = `cargo build --target ${target.triple}`
132
+
133
+ await new Promise<void>((resolve, reject) => {
134
+ const child = exec(buildCmd, {
135
+ cwd: projectDir,
136
+ env: { ...process.env, NAPI_TYPE_DEF_TMP_FOLDER: typeDefDir },
137
+ })
138
+ child.stderr?.on('data', (data) => {
139
+ console.error(data.toString())
140
+ })
141
+ child.on('exit', (code) => {
142
+ if (code === 0) {
143
+ resolve()
144
+ } else {
145
+ reject(new Error(`cargo build exited with code ${code ?? 'null'}`))
146
+ }
147
+ })
148
+ child.on('error', reject)
149
+ })
150
+
151
+ const files = await readdir(typeDefDir)
152
+ t.true(files.length > 0, 'type definition files should be generated')
153
+
154
+ const { exports, dts } = await generateTypeDef({
155
+ typeDefDir,
156
+ cwd: projectDir,
157
+ })
158
+
159
+ t.true(exports.includes('sum'), 'generateTypeDef should expose napi exports')
160
+
161
+ const jsBinding = await writeJsBinding({
162
+ platform: true,
163
+ idents: exports,
164
+ binaryName,
165
+ packageName,
166
+ version,
167
+ outputDir: projectDir,
168
+ })
169
+
170
+ t.truthy(jsBinding)
171
+ t.is(jsBinding?.path, join(projectDir, 'index.js'))
172
+
173
+ const libName = crateName.replace(/-/g, '_')
174
+ const srcName =
175
+ target.platform === 'darwin'
176
+ ? `lib${libName}.dylib`
177
+ : target.platform === 'win32'
178
+ ? `${libName}.dll`
179
+ : `lib${libName}.so`
180
+ const profile = 'debug'
181
+ const srcPath = join(projectDir, 'target', target.triple, profile, srcName)
182
+ t.true(existsSync(srcPath), 'compiled artifact should exist')
183
+
184
+ const destName = `${binaryName}.${target.platformArchABI}.${srcName.endsWith('.wasm') ? 'wasm' : 'node'}`
185
+ const destPath = join(projectDir, destName)
186
+ await copyFile(srcPath, destPath)
187
+ t.true(existsSync(destPath), 'artifact should be copied to output directory')
188
+
189
+ const nodeStat = await stat(destPath)
190
+ t.true(nodeStat.size > 0)
191
+
192
+ t.regex(dts, /export declare function sum\(a: number, b: number\): number/)
193
+
194
+ const jsPath = join(projectDir, 'index.js')
195
+ t.true(existsSync(jsPath))
196
+ const jsContent = await readFile(jsPath, 'utf-8')
197
+ t.regex(jsContent, /module\.exports\.sum = nativeBinding\.sum/)
198
+ })
199
+
200
+ test('generateTypeDef preserves deterministic file order', async (t) => {
201
+ const { projectDir, typeDefDir } = t.context
202
+
203
+ await mkdir(join(typeDefDir, 'nested'), { recursive: true })
204
+ await writeFile(
205
+ join(typeDefDir, 'b.type'),
206
+ '{"kind":"fn","name":"zeta","def":"function zeta(): void"}\n',
207
+ )
208
+ await writeFile(
209
+ join(typeDefDir, 'a.type'),
210
+ '{"kind":"fn","name":"alpha","def":"function alpha(): void"}\n',
211
+ )
212
+
213
+ const { exports, dts } = await generateTypeDef({
214
+ typeDefDir,
215
+ cwd: projectDir,
216
+ })
217
+
218
+ t.deepEqual(exports, ['alpha', 'zeta'])
219
+ t.true(
220
+ dts.indexOf('function alpha(): void') <
221
+ dts.indexOf('function zeta(): void'),
222
+ )
223
+ })
224
+
225
+ test('should throw on emnapi version mismatch in wasm build', async (t) => {
226
+ const { projectDir } = t.context
227
+ const crateName = 'wasm_version_check'
228
+ const binaryName = 'wasm-version-check'
229
+ const packageName = 'wasm-version-check'
230
+ const version = '0.1.0'
231
+
232
+ const napiPath = posixJoin(repoRoot, 'crates', 'napi').replaceAll(
233
+ win32Sep,
234
+ posixSep,
235
+ )
236
+ const napiDerivePath = posixJoin(repoRoot, 'crates', 'macro').replaceAll(
237
+ win32Sep,
238
+ posixSep,
239
+ )
240
+ const napiBuildPath = posixJoin(repoRoot, 'crates', 'build').replaceAll(
241
+ win32Sep,
242
+ posixSep,
243
+ )
244
+
245
+ await mkdir(join(projectDir, 'src'), { recursive: true })
246
+
247
+ const cargoToml = `[package]
248
+ name = "${crateName}"
249
+ version = "${version}"
250
+ edition = "2021"
251
+
252
+ [lib]
253
+ crate-type = ["cdylib"]
254
+
255
+ [dependencies]
256
+ napi = { path = "${napiPath}", features = ["noop"] }
257
+ napi-derive = { path = "${napiDerivePath}", features = ["noop"] }
258
+
259
+ [build-dependencies]
260
+ napi-build = { path = "${napiBuildPath}" }
261
+ `
262
+
263
+ await writeFile(join(projectDir, 'Cargo.toml'), cargoToml)
264
+ await writeFile(
265
+ join(projectDir, 'package.json'),
266
+ `${JSON.stringify(
267
+ {
268
+ name: packageName,
269
+ version,
270
+ napi: {
271
+ binaryName,
272
+ targets: ['wasm32-wasi-preview1-threads'],
273
+ },
274
+ },
275
+ null,
276
+ 2,
277
+ )}\n`,
278
+ )
279
+ await writeFile(
280
+ join(projectDir, 'build.rs'),
281
+ 'fn main() {\n napi_build::setup();\n}\n',
282
+ )
283
+ await writeFile(
284
+ join(projectDir, 'src', 'lib.rs'),
285
+ 'use napi_derive::napi;\n\n#[napi]\npub fn sum(a: i32, b: i32) -> i32 {\n a + b\n}\n',
286
+ )
287
+
288
+ // Create fake @emnapi/core and @emnapi/runtime with mismatched versions
289
+ const fakeVersion = '0.0.0-fake'
290
+ for (const pkg of ['@emnapi/core', '@emnapi/runtime']) {
291
+ const pkgDir = join(projectDir, 'node_modules', pkg)
292
+ await mkdir(pkgDir, { recursive: true })
293
+ await writeFile(
294
+ join(pkgDir, 'package.json'),
295
+ JSON.stringify({ name: pkg, version: fakeVersion, main: 'index.js' }),
296
+ )
297
+ await writeFile(
298
+ join(pkgDir, 'index.js'),
299
+ `module.exports = { version: "${fakeVersion}" }`,
300
+ )
301
+ }
302
+
303
+ const error = await t.throwsAsync(() =>
304
+ buildProject({
305
+ target: 'wasm32-wasi-preview1-threads',
306
+ cwd: projectDir,
307
+ }),
308
+ )
309
+
310
+ t.truthy(error)
311
+ t.regex(error!.message, /emnapi version mismatch/)
312
+ })
313
+
314
+ test('validateCrossCompileFlags rejects combining two cross-compilation mechanisms', (t) => {
315
+ t.throws(
316
+ () => validateCrossCompileFlags({ useCross: true, crossCompile: true }),
317
+ { message: /`--use-cross`.+`--cross-compile`.+cannot be used together/ },
318
+ )
319
+ t.throws(
320
+ () => validateCrossCompileFlags({ useCross: true, useNapiCross: true }),
321
+ { message: /`--use-cross`.+`--use-napi-cross`.+cannot be used together/ },
322
+ )
323
+ t.throws(
324
+ () => validateCrossCompileFlags({ useNapiCross: true, crossCompile: true }),
325
+ {
326
+ message: /`--use-napi-cross`.+`--cross-compile`.+cannot be used together/,
327
+ },
328
+ )
329
+ t.throws(
330
+ () =>
331
+ validateCrossCompileFlags({
332
+ useCross: true,
333
+ useNapiCross: true,
334
+ crossCompile: true,
335
+ }),
336
+ {
337
+ message:
338
+ /`--use-cross`.+`--use-napi-cross`.+`--cross-compile`.+cannot be used together/,
339
+ },
340
+ )
341
+ })
342
+
343
+ test('validateCrossCompileFlags allows a single cross-compilation mechanism', (t) => {
344
+ t.notThrows(() => validateCrossCompileFlags({}))
345
+ t.notThrows(() => validateCrossCompileFlags({ useCross: true }))
346
+ t.notThrows(() => validateCrossCompileFlags({ crossCompile: true }))
347
+ t.notThrows(() => validateCrossCompileFlags({ useNapiCross: true }))
348
+ })
349
+
350
+ test('validateCrossCompileFlags rejects windows-gnu targets with `--cross-compile`', (t) => {
351
+ const windowsGnuError =
352
+ /`--cross-compile` \(`-x`\) does not support the target x86_64-pc-windows-gnu/
353
+ // `cargo-xwin` is only used on non-Windows hosts, where it silently
354
+ // no-ops for `windows-gnu` targets, so the combination must be rejected.
355
+ const gnuError = t.throws(
356
+ () =>
357
+ validateCrossCompileFlags(
358
+ { crossCompile: true, target: 'x86_64-pc-windows-gnu' },
359
+ 'linux',
360
+ ),
361
+ { message: windowsGnuError },
362
+ )
363
+ // `windows-gnu` links with a mingw-w64 GCC toolchain.
364
+ t.regex(gnuError!.message, /x86_64-w64-mingw32-gcc/)
365
+ t.regex(gnuError!.message, /LIBNODE_PATH/)
366
+ t.regex(gnuError!.message, /x86_64-pc-windows-msvc/)
367
+ t.throws(
368
+ () =>
369
+ validateCrossCompileFlags(
370
+ { crossCompile: true, target: 'x86_64-pc-windows-gnu' },
371
+ 'darwin',
372
+ ),
373
+ { message: windowsGnuError },
374
+ )
375
+ // `gnullvm`-flavored triples take the same broken `cargo-xwin` route, but
376
+ // link with an LLVM toolchain (llvm-mingw), not the mingw-w64 GCC one.
377
+ const gnullvmError = t.throws(
378
+ () =>
379
+ validateCrossCompileFlags(
380
+ { crossCompile: true, target: 'x86_64-pc-windows-gnullvm' },
381
+ 'linux',
382
+ ),
383
+ {
384
+ message:
385
+ /`--cross-compile` \(`-x`\) does not support the target x86_64-pc-windows-gnullvm/,
386
+ },
387
+ )
388
+ t.regex(gnullvmError!.message, /llvm-mingw/)
389
+ t.notRegex(gnullvmError!.message, /mingw32-gcc/)
390
+ t.regex(gnullvmError!.message, /LIBNODE_PATH/)
391
+ t.regex(gnullvmError!.message, /x86_64-pc-windows-msvc/)
392
+ // The target can also come from `CARGO_BUILD_TARGET`; the check is fully
393
+ // synchronous, so the environment is mutated and restored without any
394
+ // interleaving point another concurrently running test could observe.
395
+ const originalCargoBuildTarget = process.env.CARGO_BUILD_TARGET
396
+ try {
397
+ process.env.CARGO_BUILD_TARGET = 'x86_64-pc-windows-gnu'
398
+ t.throws(() => validateCrossCompileFlags({ crossCompile: true }, 'linux'), {
399
+ message: windowsGnuError,
400
+ })
401
+ } finally {
402
+ if (originalCargoBuildTarget === undefined) {
403
+ delete process.env.CARGO_BUILD_TARGET
404
+ } else {
405
+ process.env.CARGO_BUILD_TARGET = originalCargoBuildTarget
406
+ }
407
+ }
408
+ })
409
+
410
+ test('validateCrossCompileFlags allows `--cross-compile` for non windows-gnu targets', (t) => {
411
+ // MSVC targets are exactly what `cargo-xwin` supports.
412
+ for (const target of [
413
+ 'x86_64-pc-windows-msvc',
414
+ 'aarch64-pc-windows-msvc',
415
+ 'i686-pc-windows-msvc',
416
+ 'x86_64-unknown-linux-gnu',
417
+ 'aarch64-apple-darwin',
418
+ ]) {
419
+ t.notThrows(() =>
420
+ validateCrossCompileFlags({ crossCompile: true, target }, 'linux'),
421
+ )
422
+ }
423
+ // On a Windows host `--cross-compile` never routes through `cargo-xwin`
424
+ // (it falls back to a plain `cargo build`), so windows-gnu stays allowed.
425
+ t.notThrows(() =>
426
+ validateCrossCompileFlags(
427
+ { crossCompile: true, target: 'x86_64-pc-windows-gnu' },
428
+ 'win32',
429
+ ),
430
+ )
431
+ // Without `--cross-compile` the target is none of this check's business.
432
+ t.notThrows(() =>
433
+ validateCrossCompileFlags({ target: 'x86_64-pc-windows-gnu' }, 'linux'),
434
+ )
435
+ })
436
+
437
+ test('validateCrossCompileFlags rejects watch mode combined with cross builds', (t) => {
438
+ t.throws(() => validateCrossCompileFlags({ watch: true, useCross: true }), {
439
+ message: /`--watch` cannot be used with `--use-cross`/,
440
+ })
441
+ t.throws(
442
+ () => validateCrossCompileFlags({ watch: true, crossCompile: true }),
443
+ { message: /`--watch` cannot be used with `--cross-compile`/ },
444
+ )
445
+ t.notThrows(() => validateCrossCompileFlags({ watch: true }))
446
+ t.notThrows(() =>
447
+ validateCrossCompileFlags({ watch: true, useNapiCross: true }),
448
+ )
449
+ })
450
+
451
+ test('validateNapiCrossSupport rejects unsupported hosts', (t) => {
452
+ t.throws(
453
+ () =>
454
+ validateNapiCrossSupport('aarch64-unknown-linux-gnu', 'darwin', 'arm64'),
455
+ { message: /`--use-napi-cross` requires a Linux x64 or Linux arm64 host/ },
456
+ )
457
+ t.throws(
458
+ () => validateNapiCrossSupport('aarch64-unknown-linux-gnu', 'win32', 'x64'),
459
+ { message: /`--use-napi-cross` requires a Linux x64 or Linux arm64 host/ },
460
+ )
461
+ t.throws(
462
+ () =>
463
+ validateNapiCrossSupport('aarch64-unknown-linux-gnu', 'linux', 'ia32'),
464
+ { message: /`--use-napi-cross` requires a Linux x64 or Linux arm64 host/ },
465
+ )
466
+ t.notThrows(() =>
467
+ validateNapiCrossSupport('aarch64-unknown-linux-gnu', 'linux', 'x64'),
468
+ )
469
+ t.notThrows(() =>
470
+ validateNapiCrossSupport('x86_64-unknown-linux-gnu', 'linux', 'arm64'),
471
+ )
472
+ })
473
+
474
+ test('validateNapiCrossSupport rejects unsupported target triples', (t) => {
475
+ t.throws(
476
+ () => validateNapiCrossSupport('x86_64-unknown-linux-musl', 'linux', 'x64'),
477
+ {
478
+ message:
479
+ /`--use-napi-cross` does not support the target x86_64-unknown-linux-musl/,
480
+ },
481
+ )
482
+ t.throws(
483
+ () =>
484
+ validateNapiCrossSupport('riscv64gc-unknown-linux-gnu', 'linux', 'arm64'),
485
+ {
486
+ message:
487
+ /`--use-napi-cross` does not support the target riscv64gc-unknown-linux-gnu/,
488
+ },
489
+ )
490
+ for (const triple of [
491
+ 'x86_64-unknown-linux-gnu',
492
+ 'aarch64-unknown-linux-gnu',
493
+ 'armv7-unknown-linux-gnueabihf',
494
+ 's390x-unknown-linux-gnu',
495
+ 'powerpc64le-unknown-linux-gnu',
496
+ ]) {
497
+ t.notThrows(() => validateNapiCrossSupport(triple, 'linux', 'x64'))
498
+ t.notThrows(() => validateNapiCrossSupport(triple, 'linux', 'arm64'))
499
+ }
500
+ })
501
+
502
+ const napiCrossToolchainPath = posixJoin(
503
+ '/home/user/.napi-rs/cross-toolchain/1.0.0',
504
+ 'aarch64-unknown-linux-gnu',
505
+ )
506
+ const napiCrossDownloadedSysroot = join(
507
+ napiCrossToolchainPath,
508
+ 'aarch64-unknown-linux-gnu',
509
+ 'sysroot',
510
+ )
511
+
512
+ test('napiCrossToolchainEnvs points the build at the downloaded toolchain', (t) => {
513
+ const envs = napiCrossToolchainEnvs(
514
+ napiCrossToolchainPath,
515
+ 'aarch64-unknown-linux-gnu',
516
+ { PATH: '/usr/bin' },
517
+ )
518
+
519
+ t.is(
520
+ envs.CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER,
521
+ join(napiCrossToolchainPath, 'bin', 'aarch64-unknown-linux-gnu-gcc'),
522
+ )
523
+ t.is(envs.TARGET_SYSROOT, napiCrossDownloadedSysroot)
524
+ t.is(envs.BINDGEN_EXTRA_CLANG_ARGS, `--sysroot=${napiCrossDownloadedSysroot}`)
525
+ t.is(envs.PATH, `${napiCrossToolchainPath}/bin:/usr/bin`)
526
+ // gcc is the default compiler, so no clang-specific flags are set.
527
+ t.is(envs.TARGET_CFLAGS, undefined)
528
+ t.is(envs.TARGET_CXXFLAGS, undefined)
529
+ })
530
+
531
+ test('napiCrossToolchainEnvs respects a user-provided TARGET_SYSROOT', (t) => {
532
+ const envs = napiCrossToolchainEnvs(
533
+ napiCrossToolchainPath,
534
+ 'aarch64-unknown-linux-gnu',
535
+ { PATH: '/usr/bin', TARGET_SYSROOT: '/opt/custom-sysroot' },
536
+ )
537
+
538
+ // The user's value wins: it is not overridden...
539
+ t.is(envs.TARGET_SYSROOT, undefined)
540
+ // ...and the derived flags use it.
541
+ t.is(envs.BINDGEN_EXTRA_CLANG_ARGS, '--sysroot=/opt/custom-sysroot')
542
+ })
543
+
544
+ test('napiCrossToolchainEnvs treats an empty TARGET_SYSROOT as unset', (t) => {
545
+ // `setEnvIfNotExists` uses falsy semantics (`!process.env[env]`), so a
546
+ // present-but-empty `TARGET_SYSROOT` still gets the downloaded sysroot
547
+ // written to the build environment — the flags derived from the effective
548
+ // sysroot must follow the same rule instead of producing `--sysroot=`.
549
+ const envs = napiCrossToolchainEnvs(
550
+ napiCrossToolchainPath,
551
+ 'aarch64-unknown-linux-gnu',
552
+ { PATH: '/usr/bin', TARGET_SYSROOT: '' },
553
+ )
554
+
555
+ t.is(envs.TARGET_SYSROOT, napiCrossDownloadedSysroot)
556
+ t.is(envs.BINDGEN_EXTRA_CLANG_ARGS, `--sysroot=${napiCrossDownloadedSysroot}`)
557
+ })
558
+
559
+ test('napiCrossToolchainEnvs derives clang flags from the effective sysroot', (t) => {
560
+ const envs = napiCrossToolchainEnvs(
561
+ napiCrossToolchainPath,
562
+ 'aarch64-unknown-linux-gnu',
563
+ {
564
+ PATH: '/usr/bin',
565
+ // Present-but-empty values must fall back to the downloaded sysroot.
566
+ TARGET_SYSROOT: '',
567
+ TARGET_CC: 'clang',
568
+ TARGET_CXX: 'clang++',
569
+ TARGET_CFLAGS: '-O2',
570
+ },
571
+ )
572
+
573
+ t.is(
574
+ envs.TARGET_CFLAGS,
575
+ `--sysroot=${napiCrossDownloadedSysroot} --gcc-toolchain=${napiCrossToolchainPath} -O2`,
576
+ )
577
+ t.is(
578
+ envs.TARGET_CXXFLAGS,
579
+ `--sysroot=${napiCrossDownloadedSysroot} --gcc-toolchain=${napiCrossToolchainPath} `,
580
+ )
581
+ })
582
+
583
+ test('napiCrossToolchainEnvs sets a bare toolchain PATH when the env has none', (t) => {
584
+ const envs = napiCrossToolchainEnvs(
585
+ napiCrossToolchainPath,
586
+ 'aarch64-unknown-linux-gnu',
587
+ {},
588
+ )
589
+
590
+ // No `:undefined` tail when the provided env has no PATH at all.
591
+ t.is(envs.PATH, `${napiCrossToolchainPath}/bin`)
592
+ })
593
+
594
+ test('napiCrossToolchainEnvs recognizes path-qualified, prefixed and versioned clang', (t) => {
595
+ const clangFlags = `--sysroot=${napiCrossDownloadedSysroot} --gcc-toolchain=${napiCrossToolchainPath} `
596
+
597
+ for (const [cc, cxx] of [
598
+ ['/usr/bin/clang', '/opt/llvm/bin/clang++'],
599
+ ['aarch64-linux-gnu-clang', 'aarch64-linux-gnu-clang++'],
600
+ ['clang-18', 'clang++-18'],
601
+ ]) {
602
+ const envs = napiCrossToolchainEnvs(
603
+ napiCrossToolchainPath,
604
+ 'aarch64-unknown-linux-gnu',
605
+ { PATH: '/usr/bin', TARGET_CC: cc, TARGET_CXX: cxx },
606
+ )
607
+
608
+ t.is(envs.TARGET_CFLAGS, clangFlags, `TARGET_CC=${cc}`)
609
+ t.is(envs.TARGET_CXXFLAGS, clangFlags, `TARGET_CXX=${cxx}`)
610
+ }
611
+ })
612
+
613
+ test('napiCrossToolchainEnvs ignores CC/CXX when the toolchain compiler is effective', (t) => {
614
+ // With TARGET_CC/TARGET_CXX unset, the function itself exports the
615
+ // toolchain gcc/g++ as TARGET_CC/TARGET_CXX — and cc-rs prefers TARGET_CC
616
+ // over CC for cross builds, so a clang in CC/CXX never actually runs.
617
+ // Injecting the clang-only `--gcc-toolchain=` flag here would hard-error
618
+ // the gcc that does run.
619
+ const envs = napiCrossToolchainEnvs(
620
+ napiCrossToolchainPath,
621
+ 'aarch64-unknown-linux-gnu',
622
+ { PATH: '/usr/bin', CC: '/usr/bin/clang', CXX: '/opt/llvm/bin/clang++' },
623
+ )
624
+
625
+ t.true(envs.TARGET_CC.endsWith('-gcc'))
626
+ t.true(envs.TARGET_CXX.endsWith('-g++'))
627
+ t.is(envs.TARGET_CFLAGS, undefined)
628
+ t.is(envs.TARGET_CXXFLAGS, undefined)
629
+ })
630
+
631
+ test('napiCrossToolchainEnvs treats an empty TARGET_CC as unset for clang detection', (t) => {
632
+ // Falsy semantics: a present-but-empty TARGET_CC still gets the toolchain
633
+ // gcc written to the build environment, so the CC fallback must not
634
+ // resurrect clang detection for a compiler that will not run.
635
+ const envs = napiCrossToolchainEnvs(
636
+ napiCrossToolchainPath,
637
+ 'aarch64-unknown-linux-gnu',
638
+ { PATH: '/usr/bin', TARGET_CC: '', CC: '/usr/bin/clang' },
639
+ )
640
+
641
+ t.is(
642
+ envs.TARGET_CC,
643
+ join(napiCrossToolchainPath, 'bin', 'aarch64-unknown-linux-gnu-gcc'),
644
+ )
645
+ t.is(envs.TARGET_CFLAGS, undefined)
646
+ })
647
+
648
+ test('napiCrossToolchainEnvs lets a user TARGET_CC=clang win over CC=gcc', (t) => {
649
+ const envs = napiCrossToolchainEnvs(
650
+ napiCrossToolchainPath,
651
+ 'aarch64-unknown-linux-gnu',
652
+ { PATH: '/usr/bin', TARGET_CC: 'clang', CC: 'gcc' },
653
+ )
654
+
655
+ t.is(
656
+ envs.TARGET_CFLAGS,
657
+ `--sysroot=${napiCrossDownloadedSysroot} --gcc-toolchain=${napiCrossToolchainPath} `,
658
+ )
659
+ // The CXX side is untouched, so it stays on the toolchain g++ without
660
+ // clang flags — each language is detected independently.
661
+ t.is(envs.TARGET_CXXFLAGS, undefined)
662
+ })
663
+
664
+ test('napiCrossToolchainEnvs injects no flags on either side when only CC=clang is set', (t) => {
665
+ // Both languages default to the toolchain gcc/g++; neither effective
666
+ // compiler is clang, so neither flag set is injected.
667
+ const envs = napiCrossToolchainEnvs(
668
+ napiCrossToolchainPath,
669
+ 'aarch64-unknown-linux-gnu',
670
+ { PATH: '/usr/bin', CC: 'clang' },
671
+ )
672
+
673
+ t.true(envs.TARGET_CC.endsWith('-gcc'))
674
+ t.is(envs.TARGET_CFLAGS, undefined)
675
+ t.is(envs.TARGET_CXXFLAGS, undefined)
676
+ })
677
+
678
+ test('napiCrossToolchainEnvs does not mistake non-clang tools for clang', (t) => {
679
+ for (const [cc, cxx] of [
680
+ ['gcc', 'g++'],
681
+ ['x86_64-unknown-linux-gnu-gcc', 'x86_64-unknown-linux-gnu-g++'],
682
+ // `clang-format` is a clang-family tool but not a compiler.
683
+ ['clang-format', 'clang-format'],
684
+ ['someclangthing', 'someclangthing'],
685
+ ]) {
686
+ const envs = napiCrossToolchainEnvs(
687
+ napiCrossToolchainPath,
688
+ 'aarch64-unknown-linux-gnu',
689
+ { PATH: '/usr/bin', TARGET_CC: cc, TARGET_CXX: cxx },
690
+ )
691
+
692
+ t.is(envs.TARGET_CFLAGS, undefined, `TARGET_CC=${cc}`)
693
+ t.is(envs.TARGET_CXXFLAGS, undefined, `TARGET_CXX=${cxx}`)
694
+ }
695
+ })
696
+
697
+ test('napiCrossToolchainEnvs detects clang behind cc-rs wrapper and argument forms', (t) => {
698
+ const clangFlags = `--sysroot=${napiCrossDownloadedSysroot} --gcc-toolchain=${napiCrossToolchainPath} `
699
+
700
+ // cc-rs parses the env value before executing it (`env_tool`): the value
701
+ // is split on whitespace, a known wrapper prefix (`sccache clang`) runs
702
+ // the second token, and an argument form (`clang -target …`) runs the
703
+ // first token with the rest as arguments — clang runs in both cases.
704
+ for (const [cc, cxx] of [
705
+ ['sccache clang', 'sccache clang++'],
706
+ ['ccache clang-18', 'ccache clang++-18'],
707
+ ['distcc /usr/bin/clang', 'distcc /usr/bin/clang++'],
708
+ [
709
+ 'clang -target aarch64-unknown-linux-gnu',
710
+ 'clang++ -target aarch64-unknown-linux-gnu',
711
+ ],
712
+ ['/usr/bin/clang --sysroot=/x', '/usr/bin/clang++ --sysroot=/x'],
713
+ ]) {
714
+ const envs = napiCrossToolchainEnvs(
715
+ napiCrossToolchainPath,
716
+ 'aarch64-unknown-linux-gnu',
717
+ { PATH: '/usr/bin', TARGET_CC: cc, TARGET_CXX: cxx },
718
+ )
719
+
720
+ t.is(envs.TARGET_CFLAGS, clangFlags, `TARGET_CC=${cc}`)
721
+ t.is(envs.TARGET_CXXFLAGS, clangFlags, `TARGET_CXX=${cxx}`)
722
+ }
723
+ })
724
+
725
+ // Writes empty files at `relativePaths` under a fresh temp directory whose
726
+ // subdirectories contain spaces, mirroring installs like `/opt/LLVM 18`.
727
+ // Returns the temp directory root; removal is registered on `t.teardown`.
728
+ const makeCompilerFixture = (
729
+ t: ExecutionContext,
730
+ relativePaths: Array<string>,
731
+ ): string => {
732
+ const fixtureRoot = mkdtempSync(join(tmpdir(), 'napi-clang-detect-'))
733
+ t.teardown(() => rmSync(fixtureRoot, { recursive: true, force: true }))
734
+ for (const relativePath of relativePaths) {
735
+ const absolutePath = join(fixtureRoot, relativePath)
736
+ mkdirSync(dirname(absolutePath), { recursive: true })
737
+ writeFileSync(absolutePath, '')
738
+ }
739
+ return fixtureRoot
740
+ }
741
+
742
+ test('napiCrossToolchainEnvs matches clang in space-containing paths that exist on disk', (t) => {
743
+ const clangFlags = `--sysroot=${napiCrossDownloadedSysroot} --gcc-toolchain=${napiCrossToolchainPath} `
744
+
745
+ // cc-rs's `env_tool` treats the WHOLE env value as the compiler when it
746
+ // exists on the filesystem (`check_exe`) before any whitespace splitting,
747
+ // so `TARGET_CC="<tmp>/LLVM 18/bin/clang"` runs clang — splitting it into
748
+ // `<tmp>/LLVM` + `18/bin/clang` would miss the clang detection entirely.
749
+ const fixtureRoot = makeCompilerFixture(t, [
750
+ 'LLVM 18/bin/clang',
751
+ 'LLVM 18/bin/clang++-17',
752
+ ])
753
+ const cc = join(fixtureRoot, 'LLVM 18', 'bin', 'clang')
754
+ const cxx = join(fixtureRoot, 'LLVM 18', 'bin', 'clang++-17')
755
+ const envs = napiCrossToolchainEnvs(
756
+ napiCrossToolchainPath,
757
+ 'aarch64-unknown-linux-gnu',
758
+ { PATH: '/usr/bin', TARGET_CC: cc, TARGET_CXX: cxx },
759
+ )
760
+
761
+ t.is(envs.TARGET_CFLAGS, clangFlags, `TARGET_CC=${cc}`)
762
+ t.is(envs.TARGET_CXXFLAGS, clangFlags, `TARGET_CXX=${cxx}`)
763
+ })
764
+
765
+ test('napiCrossToolchainEnvs does not mistake existing space-containing non-clang paths for clang', (t) => {
766
+ const fixtureRoot = makeCompilerFixture(t, [
767
+ 'app dir/bin/gcc',
768
+ 'app dir/bin/g++',
769
+ // A clang-family tool that is not a compiler, in a space-containing path.
770
+ 'LLVM 18/bin/clang-format',
771
+ ])
772
+ for (const [cc, cxx] of [
773
+ [
774
+ join(fixtureRoot, 'app dir', 'bin', 'gcc'),
775
+ join(fixtureRoot, 'app dir', 'bin', 'g++'),
776
+ ],
777
+ [
778
+ join(fixtureRoot, 'LLVM 18', 'bin', 'clang-format'),
779
+ join(fixtureRoot, 'LLVM 18', 'bin', 'clang-format'),
780
+ ],
781
+ ]) {
782
+ const envs = napiCrossToolchainEnvs(
783
+ napiCrossToolchainPath,
784
+ 'aarch64-unknown-linux-gnu',
785
+ { PATH: '/usr/bin', TARGET_CC: cc, TARGET_CXX: cxx },
786
+ )
787
+
788
+ t.is(envs.TARGET_CFLAGS, undefined, `TARGET_CC=${cc}`)
789
+ t.is(envs.TARGET_CXXFLAGS, undefined, `TARGET_CXX=${cxx}`)
790
+ }
791
+ })
792
+
793
+ test('napiCrossToolchainEnvs splits space-containing paths that do not exist on disk', (t) => {
794
+ // cc-rs only takes the whole value as a compiler path when it exists on
795
+ // the filesystem; otherwise it splits on whitespace, so this value runs
796
+ // `/nonexistent` with `dir/bin/clang` as an argument — never clang.
797
+ const envs = napiCrossToolchainEnvs(
798
+ napiCrossToolchainPath,
799
+ 'aarch64-unknown-linux-gnu',
800
+ {
801
+ PATH: '/usr/bin',
802
+ TARGET_CC: '/nonexistent dir/bin/clang',
803
+ TARGET_CXX: '/nonexistent dir/bin/clang++',
804
+ },
805
+ )
806
+
807
+ t.is(envs.TARGET_CFLAGS, undefined)
808
+ t.is(envs.TARGET_CXXFLAGS, undefined)
809
+ })
810
+
811
+ test('napiCrossToolchainEnvs does not mistake gcc with clang-ending arguments for clang', (t) => {
812
+ // The basename of each WHOLE value below is `clang`, but none of them
813
+ // exists as a file, so cc-rs splits on whitespace and runs gcc — clang
814
+ // flags injected here would hard-error the gcc compile.
815
+ for (const [cc, cxx] of [
816
+ ['gcc --sysroot=/opt/clang', 'g++ --sysroot=/opt/clang'],
817
+ ['gcc -B/opt/LLVM 18/bin/clang', 'g++ -B/opt/LLVM 18/bin/clang'],
818
+ ['sccache gcc --sysroot=/opt/clang', 'sccache g++ --sysroot=/opt/clang'],
819
+ ]) {
820
+ const envs = napiCrossToolchainEnvs(
821
+ napiCrossToolchainPath,
822
+ 'aarch64-unknown-linux-gnu',
823
+ { PATH: '/usr/bin', TARGET_CC: cc, TARGET_CXX: cxx },
824
+ )
825
+
826
+ t.is(envs.TARGET_CFLAGS, undefined, `TARGET_CC=${cc}`)
827
+ t.is(envs.TARGET_CXXFLAGS, undefined, `TARGET_CXX=${cxx}`)
828
+ }
829
+ })
830
+
831
+ test('napiCrossToolchainEnvs does not mistake wrapped or argument-form non-clang for clang', (t) => {
832
+ for (const [cc, cxx] of [
833
+ ['sccache gcc', 'sccache g++'],
834
+ ['ccache gcc', 'ccache g++'],
835
+ ['gcc -B/foo', 'g++ -B/foo'],
836
+ // The compiler token behind the wrapper is still not a compiler.
837
+ ['sccache clang-format', 'sccache clang-format'],
838
+ ]) {
839
+ const envs = napiCrossToolchainEnvs(
840
+ napiCrossToolchainPath,
841
+ 'aarch64-unknown-linux-gnu',
842
+ { PATH: '/usr/bin', TARGET_CC: cc, TARGET_CXX: cxx },
843
+ )
844
+
845
+ t.is(envs.TARGET_CFLAGS, undefined, `TARGET_CC=${cc}`)
846
+ t.is(envs.TARGET_CXXFLAGS, undefined, `TARGET_CXX=${cxx}`)
847
+ }
848
+ })
849
+
850
+ test('buildProject rejects invalid cross flag combinations upfront', async (t) => {
851
+ const { projectDir } = t.context
852
+
853
+ await mkdir(join(projectDir, 'src'), { recursive: true })
854
+ await writeFile(
855
+ join(projectDir, 'Cargo.toml'),
856
+ `[package]
857
+ name = "cross_flags_check"
858
+ version = "0.1.0"
859
+ edition = "2021"
860
+
861
+ [lib]
862
+ crate-type = ["cdylib"]
863
+ `,
864
+ )
865
+ await writeFile(join(projectDir, 'src', 'lib.rs'), '')
866
+ await writeFile(
867
+ join(projectDir, 'package.json'),
868
+ `${JSON.stringify(
869
+ {
870
+ name: 'cross-flags-check',
871
+ version: '0.1.0',
872
+ napi: { binaryName: 'cross-flags-check' },
873
+ },
874
+ null,
875
+ 2,
876
+ )}\n`,
877
+ )
878
+
879
+ const comboError = await t.throwsAsync(() =>
880
+ buildProject({ cwd: projectDir, useCross: true, crossCompile: true }),
881
+ )
882
+ t.regex(comboError!.message, /cannot be used together/)
883
+
884
+ const watchError = await t.throwsAsync(() =>
885
+ buildProject({ cwd: projectDir, watch: true, crossCompile: true }),
886
+ )
887
+ t.regex(
888
+ watchError!.message,
889
+ /`--watch` cannot be used with `--cross-compile`/,
890
+ )
891
+
892
+ const napiCrossError = await t.throwsAsync(() =>
893
+ buildProject({
894
+ cwd: projectDir,
895
+ useNapiCross: true,
896
+ target: 'riscv64gc-unknown-linux-gnu',
897
+ }),
898
+ )
899
+ t.regex(napiCrossError!.message, /`--use-napi-cross`/)
900
+ })
901
+
902
+ test('buildProject validates cross flags before resolving the crate manifest', async (t) => {
903
+ // `projectDir` deliberately contains no `Cargo.toml`: if `buildProject`
904
+ // resolved the manifest (and spawned `cargo metadata`) before validating
905
+ // the cross-compilation flags, these calls would fail with
906
+ // "No crate found in manifest" instead of the validation errors below.
907
+ const { projectDir } = t.context
908
+
909
+ const comboError = await t.throwsAsync(() =>
910
+ buildProject({ cwd: projectDir, useCross: true, crossCompile: true }),
911
+ )
912
+ t.regex(
913
+ comboError!.message,
914
+ /`--use-cross`.+`--cross-compile`.+cannot be used together/,
915
+ )
916
+
917
+ const watchError = await t.throwsAsync(() =>
918
+ buildProject({ cwd: projectDir, watch: true, useCross: true }),
919
+ )
920
+ t.regex(watchError!.message, /`--watch` cannot be used with `--use-cross`/)
921
+
922
+ const watchCrossCompileError = await t.throwsAsync(() =>
923
+ buildProject({ cwd: projectDir, watch: true, crossCompile: true }),
924
+ )
925
+ t.regex(
926
+ watchCrossCompileError!.message,
927
+ /`--watch` cannot be used with `--cross-compile`/,
928
+ )
929
+
930
+ // Rejected either for the unsupported host (non Linux x64/arm64) or for
931
+ // the unsupported target triple (on Linux x64/arm64 hosts) — both are
932
+ // `--use-napi-cross` validation errors, keeping this assertion
933
+ // host-platform independent.
934
+ const napiCrossError = await t.throwsAsync(() =>
935
+ buildProject({
936
+ cwd: projectDir,
937
+ useNapiCross: true,
938
+ target: 'riscv64gc-unknown-linux-gnu',
939
+ }),
940
+ )
941
+ t.regex(napiCrossError!.message, /`--use-napi-cross`/)
942
+ })
943
+
944
+ // On a Windows host `--cross-compile` never routes through `cargo-xwin`,
945
+ // so the windows-gnu rejection only exists on non-Windows hosts.
946
+ ;(process.platform === 'win32' ? test.skip : test)(
947
+ 'buildProject rejects `--cross-compile` with a windows-gnu target before any side effect',
948
+ async (t) => {
949
+ // `projectDir` deliberately contains no `Cargo.toml`: if `buildProject`
950
+ // resolved the manifest (and spawned `cargo metadata`) before validating
951
+ // the target, this call would fail with a manifest error instead of the
952
+ // windows-gnu validation error below.
953
+ const { projectDir } = t.context
954
+
955
+ const error = await t.throwsAsync(() =>
956
+ buildProject({
957
+ cwd: projectDir,
958
+ crossCompile: true,
959
+ target: 'x86_64-pc-windows-gnu',
960
+ }),
961
+ )
962
+ t.regex(
963
+ error!.message,
964
+ /`--cross-compile` \(`-x`\) does not support the target x86_64-pc-windows-gnu/,
965
+ )
966
+ t.regex(error!.message, /cargo-xwin/)
967
+ },
968
+ )
969
+
970
+ // The scenario below only exists on hosts `--use-napi-cross` does not
971
+ // support (anything but Linux x64 / Linux arm64), so skip it elsewhere.
972
+ const isNapiCrossUnsupportedHost =
973
+ process.platform !== 'linux' ||
974
+ (process.arch !== 'x64' && process.arch !== 'arm64')
975
+
976
+ ;(isNapiCrossUnsupportedHost ? test : test.skip)(
977
+ 'buildProject reports the `--use-napi-cross` host error before resolving the target',
978
+ async (t) => {
979
+ const { projectDir } = t.context
980
+
981
+ // Without an explicit `--target` (or `CARGO_BUILD_TARGET`), resolving
982
+ // the target spawns `rustc -vV`. Point `PATH` at an empty directory so
983
+ // that spawn is guaranteed to fail — whether or not Rust is installed on
984
+ // this machine: if `buildProject` resolved the target before validating
985
+ // the host, the error below would be the `rustc` spawn failure instead
986
+ // of the host validation error.
987
+ const emptyPathDir = join(projectDir, 'empty-path')
988
+ await mkdir(emptyPathDir, { recursive: true })
989
+
990
+ const originalPath = process.env.PATH
991
+ const originalCargoBuildTarget = process.env.CARGO_BUILD_TARGET
992
+ process.env.PATH = emptyPathDir
993
+ delete process.env.CARGO_BUILD_TARGET
994
+ // The cross-flag validation runs synchronously at the top of
995
+ // `buildProject`, so the promise below is already settled (rejected)
996
+ // when the environment is restored right after — no other concurrently
997
+ // running test can observe the modified `PATH`.
998
+ let buildPromise: Promise<unknown>
999
+ try {
1000
+ buildPromise = buildProject({ cwd: projectDir, useNapiCross: true })
1001
+ } finally {
1002
+ if (originalPath === undefined) {
1003
+ delete process.env.PATH
1004
+ } else {
1005
+ process.env.PATH = originalPath
1006
+ }
1007
+ if (originalCargoBuildTarget === undefined) {
1008
+ delete process.env.CARGO_BUILD_TARGET
1009
+ } else {
1010
+ process.env.CARGO_BUILD_TARGET = originalCargoBuildTarget
1011
+ }
1012
+ }
1013
+
1014
+ const error = await t.throwsAsync(() => buildPromise)
1015
+ t.regex(
1016
+ error!.message,
1017
+ /`--use-napi-cross` requires a Linux x64 or Linux arm64 host/,
1018
+ )
1019
+ },
1020
+ )