@napi-rs/cli 3.7.2 → 3.7.4
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.
- package/dist/cli.js +240 -64
- package/dist/index.cjs +239 -63
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +240 -64
- package/package.json +3 -3
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +353 -0
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/android-build.spec.ts +113 -0
- package/src/api/__tests__/artifacts.spec.ts +95 -0
- package/src/api/__tests__/build-regressions.spec.ts +128 -0
- package/src/api/__tests__/build.spec.ts +1020 -0
- package/src/api/__tests__/create-npm-dirs.spec.ts +561 -0
- package/src/api/__tests__/new-node-api-version.spec.ts +87 -0
- package/src/api/__tests__/new.spec.ts +527 -0
- package/src/api/__tests__/rename.spec.ts +181 -0
- package/src/api/__tests__/templates.spec.ts +164 -0
- package/src/api/__tests__/universalize.spec.ts +128 -0
- package/src/api/artifacts.ts +7 -16
- package/src/api/build.ts +354 -111
- package/src/api/new.ts +65 -1
- package/src/commands/__tests__/new.spec.ts +13 -0
- package/src/def/build.ts +6 -6
- package/src/utils/__tests__/__fixtures__/napi_type_def +202 -0
- package/src/utils/__tests__/__fixtures__/optional-napi-derive/Cargo.toml +3 -0
- package/src/utils/__tests__/__fixtures__/optional-napi-derive/main-crate/Cargo.toml +8 -0
- package/src/utils/__tests__/__fixtures__/optional-napi-derive/main-crate/src/lib.rs +1 -0
- package/src/utils/__tests__/__fixtures__/optional-napi-derive/napi-derive/Cargo.toml +5 -0
- package/src/utils/__tests__/__fixtures__/optional-napi-derive/napi-derive/src/lib.rs +1 -0
- package/src/utils/__tests__/__fixtures__/optional-napi-derive/with-optional-derive/Cargo.toml +11 -0
- package/src/utils/__tests__/__fixtures__/optional-napi-derive/with-optional-derive/src/lib.rs +1 -0
- package/src/utils/__tests__/__fixtures__/runtime_string_enum_flag +2 -0
- package/src/utils/__tests__/__snapshots__/config.spec.ts.md +97 -0
- package/src/utils/__tests__/__snapshots__/config.spec.ts.snap +0 -0
- package/src/utils/__tests__/__snapshots__/target.spec.ts.md +180 -0
- package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +1197 -0
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
- package/src/utils/__tests__/__snapshots__/version.spec.ts.md +22 -0
- package/src/utils/__tests__/__snapshots__/version.spec.ts.snap +0 -0
- package/src/utils/__tests__/config.spec.ts +81 -0
- package/src/utils/__tests__/metadata.spec.ts +69 -0
- package/src/utils/__tests__/misc.spec.ts +56 -0
- package/src/utils/__tests__/target.spec.ts +19 -0
- package/src/utils/__tests__/typegen.spec.ts +85 -0
- package/src/utils/__tests__/version.spec.ts +7 -0
- package/src/utils/metadata.ts +94 -3
- 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
|
+
)
|