@napi-rs/cli 3.7.2 → 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.
- package/dist/cli.js +201 -52
- package/dist/index.cjs +200 -51
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +201 -52
- 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 +346 -105
- 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__/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 +28 -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/version.ts +2 -0
package/src/api/build.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
|
-
import { existsSync, mkdirSync, rmSync } from 'node:fs'
|
|
3
|
+
import { existsSync, mkdirSync, rmSync, statSync } from 'node:fs'
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
5
|
import { homedir } from 'node:os'
|
|
6
|
-
import { parse, join, resolve } from 'node:path'
|
|
6
|
+
import { basename, parse, join, resolve } from 'node:path'
|
|
7
7
|
|
|
8
8
|
import * as colors from 'colorette'
|
|
9
9
|
|
|
@@ -62,6 +62,20 @@ export async function buildProject(rawOptions: BuildOptions) {
|
|
|
62
62
|
cwd: rawOptions.cwd ?? process.cwd(),
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Reject invalid cross-compilation flag combinations before anything with
|
|
66
|
+
// a side effect runs (`cargo metadata`, cargo binary auto-installs,
|
|
67
|
+
// toolchain downloads, ...), so the user always gets the validation error
|
|
68
|
+
// rather than an unrelated failure from those steps.
|
|
69
|
+
validateCrossCompileFlags(options)
|
|
70
|
+
if (options.useNapiCross) {
|
|
71
|
+
// Check the host constraint before resolving the target: without an
|
|
72
|
+
// explicit `--target` (or `CARGO_BUILD_TARGET`) the target resolution
|
|
73
|
+
// spawns `rustc -vV`, and on an unsupported host without Rust on the
|
|
74
|
+
// `PATH` that spawn failure would mask the actual validation error.
|
|
75
|
+
validateNapiCrossHost()
|
|
76
|
+
validateNapiCrossSupport(resolveTarget(options.target).triple)
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
const resolvePath = (...paths: string[]) => resolve(options.cwd, ...paths)
|
|
66
80
|
|
|
67
81
|
const manifestPath = resolvePath(options.manifestPath ?? 'Cargo.toml')
|
|
@@ -91,6 +105,285 @@ export async function buildProject(rawOptions: BuildOptions) {
|
|
|
91
105
|
return builder.build()
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the target triple the build will run against, following the same
|
|
110
|
+
* precedence the build itself uses: the explicit `--target` option, then the
|
|
111
|
+
* `CARGO_BUILD_TARGET` environment variable, then the host default target.
|
|
112
|
+
*/
|
|
113
|
+
function resolveTarget(targetOption?: string): Target {
|
|
114
|
+
return targetOption
|
|
115
|
+
? parseTriple(targetOption)
|
|
116
|
+
: process.env.CARGO_BUILD_TARGET
|
|
117
|
+
? parseTriple(process.env.CARGO_BUILD_TARGET)
|
|
118
|
+
: getSystemDefaultTarget()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate the combination of the cross-compilation related flags.
|
|
123
|
+
*
|
|
124
|
+
* `--use-cross`, `--use-napi-cross` and `--cross-compile` (`-x`) are three
|
|
125
|
+
* mutually exclusive cross-compilation mechanisms; combining any two of them
|
|
126
|
+
* leaves both active at once and produces broken builds, so it is rejected
|
|
127
|
+
* upfront before any side effect (like auto-installing cargo binaries or
|
|
128
|
+
* downloading toolchains) happens.
|
|
129
|
+
*
|
|
130
|
+
* `--cross-compile` with a `windows-gnu` target is rejected as well: on a
|
|
131
|
+
* non-Windows host it routes the build to `cargo xwin build`, but
|
|
132
|
+
* `cargo-xwin` only sets up MSVC toolchains — for `windows-gnu` targets it
|
|
133
|
+
* silently does nothing and the build dies much later with a cryptic
|
|
134
|
+
* ``error: linker `x86_64-w64-mingw32-gcc` not found`` that never mentions
|
|
135
|
+
* `cargo-xwin`. Only an explicitly requested target (`--target` or
|
|
136
|
+
* `CARGO_BUILD_TARGET`) is inspected, so this validation never has to spawn
|
|
137
|
+
* `rustc -vV`; a non-Windows host's default target can never be
|
|
138
|
+
* `windows-gnu` anyway.
|
|
139
|
+
*/
|
|
140
|
+
export function validateCrossCompileFlags(
|
|
141
|
+
options: {
|
|
142
|
+
useCross?: boolean
|
|
143
|
+
crossCompile?: boolean
|
|
144
|
+
useNapiCross?: boolean
|
|
145
|
+
watch?: boolean
|
|
146
|
+
target?: string
|
|
147
|
+
},
|
|
148
|
+
hostPlatform: string = process.platform,
|
|
149
|
+
): void {
|
|
150
|
+
const enabledCrossFlags = [
|
|
151
|
+
options.useCross ? '`--use-cross`' : null,
|
|
152
|
+
options.useNapiCross ? '`--use-napi-cross`' : null,
|
|
153
|
+
options.crossCompile ? '`--cross-compile` (`-x`)' : null,
|
|
154
|
+
].filter((flag): flag is string => flag !== null)
|
|
155
|
+
|
|
156
|
+
if (enabledCrossFlags.length > 1) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`${enabledCrossFlags.join(' and ')} cannot be used together. Please pick exactly one cross-compilation mechanism: \`--use-cross\`, \`--use-napi-cross\`, or \`--cross-compile\` (\`-x\`).`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (options.watch && options.useCross) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
'`--watch` cannot be used with `--use-cross`. `cargo watch` only supports the plain `cargo build` flow, please drop one of the two flags.',
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (options.watch && options.crossCompile) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'`--watch` cannot be used with `--cross-compile` (`-x`). `cargo watch` only supports the plain `cargo build` flow, please drop one of the two flags.',
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// On a Windows host `--cross-compile` never picks `cargo-xwin` (it falls
|
|
175
|
+
// back to a plain `cargo build`), so windows-gnu targets are only broken
|
|
176
|
+
// on non-Windows hosts.
|
|
177
|
+
if (options.crossCompile && hostPlatform !== 'win32') {
|
|
178
|
+
const explicitTarget = options.target ?? process.env.CARGO_BUILD_TARGET
|
|
179
|
+
if (explicitTarget) {
|
|
180
|
+
const target = parseTriple(explicitTarget)
|
|
181
|
+
if (target.platform === 'win32' && target.abi?.startsWith('gnu')) {
|
|
182
|
+
const msvcTriple = explicitTarget.replace(/gnu(llvm)?$/, 'msvc')
|
|
183
|
+
// `*-windows-gnu` links with a mingw-w64 GCC toolchain, while
|
|
184
|
+
// `*-windows-gnullvm` needs an LLVM toolchain (llvm-mingw): `rustc`
|
|
185
|
+
// has no default working linker for it without one on the `PATH`.
|
|
186
|
+
const toolchainHint =
|
|
187
|
+
target.abi === 'gnullvm'
|
|
188
|
+
? `an llvm-mingw (LLVM/Clang) toolchain on the \`PATH\` (\`rustc\` has no default working linker for ${explicitTarget} without one; \`napi-build\` additionally needs \`libnode.dll\` via the \`LIBNODE_PATH\` environment variable)`
|
|
189
|
+
: `a mingw-w64 toolchain (\`rustc\` uses the \`${explicitTarget.split('-')[0]}-w64-mingw32-gcc\` linker; \`napi-build\` additionally needs \`libnode.dll\` via the \`LIBNODE_PATH\` environment variable)`
|
|
190
|
+
throw new Error(
|
|
191
|
+
`\`--cross-compile\` (\`-x\`) does not support the target ${explicitTarget}: \`cargo-xwin\` only handles MSVC targets and the build would fail at link time. Drop \`-x\` and build with ${toolchainHint}, or target ${msvcTriple} instead.`,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Validate that the current host can run the `@napi-rs/cross-toolchain`
|
|
200
|
+
* pre-built toolchains at all: they only run on Linux x64 and Linux arm64
|
|
201
|
+
* hosts. This check is separate from (and must run before) the per-target
|
|
202
|
+
* validation, because resolving the target may need to spawn `rustc`.
|
|
203
|
+
*/
|
|
204
|
+
function validateNapiCrossHost(
|
|
205
|
+
hostPlatform: string = process.platform,
|
|
206
|
+
hostArch: string = process.arch,
|
|
207
|
+
): asserts hostArch is 'x64' | 'arm64' {
|
|
208
|
+
if (
|
|
209
|
+
hostPlatform !== 'linux' ||
|
|
210
|
+
(hostArch !== 'x64' && hostArch !== 'arm64')
|
|
211
|
+
) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`\`--use-napi-cross\` requires a Linux x64 or Linux arm64 host, but the current host is ${hostPlatform}-${hostArch}. Please use \`--cross-compile\` (\`-x\`) or \`--use-cross\` to cross compile on this host.`,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Validate that `--use-napi-cross` can actually handle the requested target
|
|
220
|
+
* on the current host. The supported target set is read from the
|
|
221
|
+
* `@napi-rs/cross-toolchain` package, and the pre-built toolchains only run
|
|
222
|
+
* on Linux x64 and Linux arm64 hosts.
|
|
223
|
+
*/
|
|
224
|
+
export function validateNapiCrossSupport(
|
|
225
|
+
targetTriple: string,
|
|
226
|
+
hostPlatform: string = process.platform,
|
|
227
|
+
hostArch: string = process.arch,
|
|
228
|
+
): void {
|
|
229
|
+
validateNapiCrossHost(hostPlatform, hostArch)
|
|
230
|
+
|
|
231
|
+
const toolchains: Record<
|
|
232
|
+
'x64' | 'arm64',
|
|
233
|
+
Record<string, string | undefined>
|
|
234
|
+
> = require('@napi-rs/cross-toolchain')
|
|
235
|
+
const supportedTargets = Object.keys(toolchains[hostArch])
|
|
236
|
+
|
|
237
|
+
if (!supportedTargets.includes(targetTriple)) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`\`--use-napi-cross\` does not support the target ${targetTriple}. Supported targets: ${supportedTargets.join(', ')}. Please use \`--cross-compile\` (\`-x\`) or \`--use-cross\` for this target.`,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Compute the environment variables that route a `--use-napi-cross` build
|
|
246
|
+
* through the `@napi-rs/cross-toolchain` toolchain extracted at
|
|
247
|
+
* `toolchainPath`.
|
|
248
|
+
*
|
|
249
|
+
* Follows the same rule as `Builder#setEnvIfNotExists`: a variable the user
|
|
250
|
+
* already set in `env` wins and is not returned, and a present-but-empty
|
|
251
|
+
* variable counts as unset (falsy semantics). The only exceptions are the
|
|
252
|
+
* clang-specific `TARGET_CFLAGS`/`TARGET_CXXFLAGS`, which prepend the sysroot
|
|
253
|
+
* flags to the user's value, and `PATH`, which always gets the toolchain
|
|
254
|
+
* `bin` directory prepended.
|
|
255
|
+
*
|
|
256
|
+
* Exported for tests.
|
|
257
|
+
*/
|
|
258
|
+
export function napiCrossToolchainEnvs(
|
|
259
|
+
toolchainPath: string,
|
|
260
|
+
targetTriple: string,
|
|
261
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
262
|
+
): Record<string, string> {
|
|
263
|
+
const alias: Record<string, string> = {
|
|
264
|
+
's390x-unknown-linux-gnu': 's390x-ibm-linux-gnu',
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const envs: Record<string, string> = {}
|
|
268
|
+
const setEnvIfNotExists = (name: string, value: string) => {
|
|
269
|
+
if (!env[name]) {
|
|
270
|
+
envs[name] = value
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const upperCaseTarget = targetToEnvVar(targetTriple)
|
|
275
|
+
const crossTargetName = alias[targetTriple] ?? targetTriple
|
|
276
|
+
setEnvIfNotExists(
|
|
277
|
+
`CARGO_TARGET_${upperCaseTarget}_LINKER`,
|
|
278
|
+
join(toolchainPath, 'bin', `${crossTargetName}-gcc`),
|
|
279
|
+
)
|
|
280
|
+
setEnvIfNotExists(
|
|
281
|
+
'TARGET_SYSROOT',
|
|
282
|
+
join(toolchainPath, crossTargetName, 'sysroot'),
|
|
283
|
+
)
|
|
284
|
+
setEnvIfNotExists(
|
|
285
|
+
'TARGET_AR',
|
|
286
|
+
join(toolchainPath, 'bin', `${crossTargetName}-ar`),
|
|
287
|
+
)
|
|
288
|
+
setEnvIfNotExists(
|
|
289
|
+
'TARGET_RANLIB',
|
|
290
|
+
join(toolchainPath, 'bin', `${crossTargetName}-ranlib`),
|
|
291
|
+
)
|
|
292
|
+
setEnvIfNotExists(
|
|
293
|
+
'TARGET_READELF',
|
|
294
|
+
join(toolchainPath, 'bin', `${crossTargetName}-readelf`),
|
|
295
|
+
)
|
|
296
|
+
setEnvIfNotExists(
|
|
297
|
+
'TARGET_C_INCLUDE_PATH',
|
|
298
|
+
join(toolchainPath, crossTargetName, 'sysroot', 'usr', 'include/'),
|
|
299
|
+
)
|
|
300
|
+
setEnvIfNotExists(
|
|
301
|
+
'TARGET_CC',
|
|
302
|
+
join(toolchainPath, 'bin', `${crossTargetName}-gcc`),
|
|
303
|
+
)
|
|
304
|
+
setEnvIfNotExists(
|
|
305
|
+
'TARGET_CXX',
|
|
306
|
+
join(toolchainPath, 'bin', `${crossTargetName}-g++`),
|
|
307
|
+
)
|
|
308
|
+
// `setEnvIfNotExists` skips `envs` when the user already set the variable
|
|
309
|
+
// in their environment, so read the effective value back from `env` first
|
|
310
|
+
// — with the same falsy semantics: a present-but-empty `TARGET_SYSROOT`
|
|
311
|
+
// counts as unset and falls back to the downloaded sysroot (`??` would
|
|
312
|
+
// keep the empty string and produce a broken `--sysroot=`).
|
|
313
|
+
const targetSysroot = env.TARGET_SYSROOT || envs.TARGET_SYSROOT
|
|
314
|
+
setEnvIfNotExists('BINDGEN_EXTRA_CLANG_ARGS', `--sysroot=${targetSysroot}`)
|
|
315
|
+
|
|
316
|
+
// cc-rs parses the env value before executing it (`env_tool` in cc's
|
|
317
|
+
// lib.rs): when the WHOLE value exists on the filesystem (`check_exe`)
|
|
318
|
+
// it is the compiler as-is — that is how it supports spaces in paths
|
|
319
|
+
// like `/opt/LLVM 18/bin/clang` — and only otherwise is the value split
|
|
320
|
+
// on whitespace; then, when the first token is a known wrapper
|
|
321
|
+
// (`sccache clang`) the second token is the compiler that actually runs,
|
|
322
|
+
// otherwise the first token is and the rest are arguments
|
|
323
|
+
// (`clang -target …`). Mirror that ordering, filesystem probe included:
|
|
324
|
+
// a pure whole-value basename heuristic would misread argument forms
|
|
325
|
+
// that merely END in `clang`, like `gcc --sysroot=/opt/clang`, and
|
|
326
|
+
// inject clang-only flags into a gcc compile. Match on the executable
|
|
327
|
+
// name so path-qualified (`/usr/bin/clang`), triple-prefixed
|
|
328
|
+
// (`aarch64-linux-gnu-clang`) and versioned (`clang-18`) compilers are
|
|
329
|
+
// all recognized — but not clang-family tools that are not compilers
|
|
330
|
+
// (`clang-format`).
|
|
331
|
+
const ccWrappers = new Set([
|
|
332
|
+
'ccache',
|
|
333
|
+
'distcc',
|
|
334
|
+
'sccache',
|
|
335
|
+
'icecc',
|
|
336
|
+
'cachepot',
|
|
337
|
+
'buildcache',
|
|
338
|
+
'kache',
|
|
339
|
+
])
|
|
340
|
+
const clangExecutableName = /(^|-)clang(\+\+)?(-\d+)?$/
|
|
341
|
+
// cc-rs's `check_exe` only probes for existence (plus an `.exe` retry on
|
|
342
|
+
// Windows, where this napi-cross path never runs); requiring a regular
|
|
343
|
+
// file additionally keeps directories from being taken for compilers.
|
|
344
|
+
const isExistingFile = (path: string): boolean => {
|
|
345
|
+
try {
|
|
346
|
+
return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false
|
|
347
|
+
} catch {
|
|
348
|
+
return false
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const isClangCompiler = (value: string | undefined): boolean => {
|
|
352
|
+
if (!value) {
|
|
353
|
+
return false
|
|
354
|
+
}
|
|
355
|
+
const trimmed = value.trim()
|
|
356
|
+
if (isExistingFile(trimmed)) {
|
|
357
|
+
return clangExecutableName.test(basename(trimmed))
|
|
358
|
+
}
|
|
359
|
+
const [first, second] = trimmed.split(/\s+/)
|
|
360
|
+
const compiler = first && ccWrappers.has(basename(first)) ? second : first
|
|
361
|
+
return (
|
|
362
|
+
compiler !== undefined && clangExecutableName.test(basename(compiler))
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Detect clang on the EFFECTIVE target compiler: the user's TARGET_CC if
|
|
367
|
+
// set, otherwise the toolchain gcc exported above (`envs.TARGET_CC`).
|
|
368
|
+
// cc-rs prefers TARGET_CC over CC for cross builds, so a plain `CC=clang`
|
|
369
|
+
// never runs here and must not trigger the clang-only `--gcc-toolchain=`
|
|
370
|
+
// flag (gcc hard-errors on it). The trailing `env.CC`/`env.CXX` terms are
|
|
371
|
+
// unreachable today (exactly one of the first two is always truthy) but
|
|
372
|
+
// keep the check correct if the toolchain default ever becomes conditional.
|
|
373
|
+
if (isClangCompiler(env.TARGET_CC || envs.TARGET_CC || env.CC)) {
|
|
374
|
+
const TARGET_CFLAGS = env.TARGET_CFLAGS || ''
|
|
375
|
+
envs.TARGET_CFLAGS = `--sysroot=${targetSysroot} --gcc-toolchain=${toolchainPath} ${TARGET_CFLAGS}`
|
|
376
|
+
}
|
|
377
|
+
if (isClangCompiler(env.TARGET_CXX || envs.TARGET_CXX || env.CXX)) {
|
|
378
|
+
const TARGET_CXXFLAGS = env.TARGET_CXXFLAGS || ''
|
|
379
|
+
envs.TARGET_CXXFLAGS = `--sysroot=${targetSysroot} --gcc-toolchain=${toolchainPath} ${TARGET_CXXFLAGS}`
|
|
380
|
+
}
|
|
381
|
+
envs.PATH = env.PATH
|
|
382
|
+
? `${toolchainPath}/bin:${env.PATH}`
|
|
383
|
+
: `${toolchainPath}/bin`
|
|
384
|
+
return envs
|
|
385
|
+
}
|
|
386
|
+
|
|
94
387
|
class Builder {
|
|
95
388
|
private readonly args: string[] = []
|
|
96
389
|
private readonly envs: Record<string, string> = {}
|
|
@@ -108,11 +401,7 @@ class Builder {
|
|
|
108
401
|
private readonly config: NapiConfig,
|
|
109
402
|
private readonly options: ParsedBuildOptions,
|
|
110
403
|
) {
|
|
111
|
-
this.target = options.target
|
|
112
|
-
? parseTriple(options.target)
|
|
113
|
-
: process.env.CARGO_BUILD_TARGET
|
|
114
|
-
? parseTriple(process.env.CARGO_BUILD_TARGET)
|
|
115
|
-
: getSystemDefaultTarget()
|
|
404
|
+
this.target = resolveTarget(options.target)
|
|
116
405
|
this.crateDir = parse(crate.manifest_path).dir
|
|
117
406
|
this.outputDir = resolve(
|
|
118
407
|
this.options.cwd,
|
|
@@ -149,6 +438,9 @@ class Builder {
|
|
|
149
438
|
}
|
|
150
439
|
|
|
151
440
|
get cdyLibName() {
|
|
441
|
+
if (this.options.bin) {
|
|
442
|
+
return
|
|
443
|
+
}
|
|
152
444
|
return this.crate.targets.find((t) => t.crate_types.includes('cdylib'))
|
|
153
445
|
?.name
|
|
154
446
|
}
|
|
@@ -164,7 +456,19 @@ class Builder {
|
|
|
164
456
|
}
|
|
165
457
|
|
|
166
458
|
build() {
|
|
167
|
-
|
|
459
|
+
// Backstop only: `buildProject()` already validated these before running
|
|
460
|
+
// anything with a side effect (see the top of `buildProject`). Kept here
|
|
461
|
+
// so a directly constructed `Builder` cannot skip the validation.
|
|
462
|
+
validateCrossCompileFlags(this.options)
|
|
463
|
+
if (this.options.useNapiCross) {
|
|
464
|
+
validateNapiCrossSupport(this.target.triple)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (this.options.bin) {
|
|
468
|
+
debug.warn(
|
|
469
|
+
`Building Cargo binary target ${this.binName}; the result will be an executable, not a Node.js addon.`,
|
|
470
|
+
)
|
|
471
|
+
} else if (!this.cdyLibName) {
|
|
168
472
|
const warning =
|
|
169
473
|
'Missing `crate-type = ["cdylib"]` in [lib] config. The build result will not be available as node addon.'
|
|
170
474
|
|
|
@@ -189,25 +493,10 @@ class Builder {
|
|
|
189
493
|
if (!this.options.useNapiCross) {
|
|
190
494
|
return this
|
|
191
495
|
}
|
|
192
|
-
if (this.options.useCross) {
|
|
193
|
-
debug.warn(
|
|
194
|
-
'You are trying to use both `--cross` and `--use-napi-cross` options, `--use-cross` will be ignored.',
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (this.options.crossCompile) {
|
|
199
|
-
debug.warn(
|
|
200
|
-
'You are trying to use both `--cross-compile` and `--use-napi-cross` options, `--cross-compile` will be ignored.',
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
496
|
|
|
204
497
|
try {
|
|
205
498
|
const { version, download } = require('@napi-rs/cross-toolchain')
|
|
206
499
|
|
|
207
|
-
const alias: Record<string, string> = {
|
|
208
|
-
's390x-unknown-linux-gnu': 's390x-ibm-linux-gnu',
|
|
209
|
-
}
|
|
210
|
-
|
|
211
500
|
const toolchainPath = join(
|
|
212
501
|
homedir(),
|
|
213
502
|
'.napi-rs',
|
|
@@ -222,66 +511,15 @@ class Builder {
|
|
|
222
511
|
const tarArchive = download(process.arch, this.target.triple)
|
|
223
512
|
tarArchive.unpack(toolchainPath)
|
|
224
513
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.setEnvIfNotExists(
|
|
229
|
-
linkerEnv,
|
|
230
|
-
join(toolchainPath, 'bin', `${crossTargetName}-gcc`),
|
|
231
|
-
)
|
|
232
|
-
this.setEnvIfNotExists(
|
|
233
|
-
'TARGET_SYSROOT',
|
|
234
|
-
join(toolchainPath, crossTargetName, 'sysroot'),
|
|
514
|
+
Object.assign(
|
|
515
|
+
this.envs,
|
|
516
|
+
napiCrossToolchainEnvs(toolchainPath, this.target.triple),
|
|
235
517
|
)
|
|
236
|
-
this.setEnvIfNotExists(
|
|
237
|
-
'TARGET_AR',
|
|
238
|
-
join(toolchainPath, 'bin', `${crossTargetName}-ar`),
|
|
239
|
-
)
|
|
240
|
-
this.setEnvIfNotExists(
|
|
241
|
-
'TARGET_RANLIB',
|
|
242
|
-
join(toolchainPath, 'bin', `${crossTargetName}-ranlib`),
|
|
243
|
-
)
|
|
244
|
-
this.setEnvIfNotExists(
|
|
245
|
-
'TARGET_READELF',
|
|
246
|
-
join(toolchainPath, 'bin', `${crossTargetName}-readelf`),
|
|
247
|
-
)
|
|
248
|
-
this.setEnvIfNotExists(
|
|
249
|
-
'TARGET_C_INCLUDE_PATH',
|
|
250
|
-
join(toolchainPath, crossTargetName, 'sysroot', 'usr', 'include/'),
|
|
251
|
-
)
|
|
252
|
-
this.setEnvIfNotExists(
|
|
253
|
-
'TARGET_CC',
|
|
254
|
-
join(toolchainPath, 'bin', `${crossTargetName}-gcc`),
|
|
255
|
-
)
|
|
256
|
-
this.setEnvIfNotExists(
|
|
257
|
-
'TARGET_CXX',
|
|
258
|
-
join(toolchainPath, 'bin', `${crossTargetName}-g++`),
|
|
259
|
-
)
|
|
260
|
-
this.setEnvIfNotExists(
|
|
261
|
-
'BINDGEN_EXTRA_CLANG_ARGS',
|
|
262
|
-
`--sysroot=${this.envs.TARGET_SYSROOT}}`,
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
process.env.TARGET_CC?.startsWith('clang') ||
|
|
267
|
-
(process.env.CC?.startsWith('clang') && !process.env.TARGET_CC)
|
|
268
|
-
) {
|
|
269
|
-
const TARGET_CFLAGS = process.env.TARGET_CFLAGS ?? ''
|
|
270
|
-
this.envs.TARGET_CFLAGS = `--sysroot=${this.envs.TARGET_SYSROOT} --gcc-toolchain=${toolchainPath} ${TARGET_CFLAGS}`
|
|
271
|
-
}
|
|
272
|
-
if (
|
|
273
|
-
(process.env.CXX?.startsWith('clang++') && !process.env.TARGET_CXX) ||
|
|
274
|
-
process.env.TARGET_CXX?.startsWith('clang++')
|
|
275
|
-
) {
|
|
276
|
-
const TARGET_CXXFLAGS = process.env.TARGET_CXXFLAGS ?? ''
|
|
277
|
-
this.envs.TARGET_CXXFLAGS = `--sysroot=${this.envs.TARGET_SYSROOT} --gcc-toolchain=${toolchainPath} ${TARGET_CXXFLAGS}`
|
|
278
|
-
}
|
|
279
|
-
this.envs.PATH = this.envs.PATH
|
|
280
|
-
? `${toolchainPath}/bin:${this.envs.PATH}:${process.env.PATH}`
|
|
281
|
-
: `${toolchainPath}/bin:${process.env.PATH}`
|
|
282
518
|
} catch (e) {
|
|
283
|
-
|
|
284
|
-
|
|
519
|
+
throw new Error(
|
|
520
|
+
`Failed to set up the \`--use-napi-cross\` toolchain for ${this.target.triple}: ${(e as Error).message}. Check filesystem permissions and network connectivity to the npm registry, then retry, or use \`--cross-compile\` (\`-x\`) / \`--use-cross\` instead.`,
|
|
521
|
+
{ cause: e },
|
|
522
|
+
)
|
|
285
523
|
}
|
|
286
524
|
return this
|
|
287
525
|
}
|
|
@@ -294,13 +532,21 @@ class Builder {
|
|
|
294
532
|
|
|
295
533
|
const watch = this.options.watch
|
|
296
534
|
const buildTask = new Promise<void>((resolve, reject) => {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
535
|
+
const cargoOverride = process.env.CARGO
|
|
536
|
+
if (
|
|
537
|
+
cargoOverride &&
|
|
538
|
+
(this.options.useCross || this.options.crossCompile)
|
|
539
|
+
) {
|
|
540
|
+
const expectedBinary = this.options.useCross ? 'cross' : 'cargo'
|
|
541
|
+
const requestedFlag = this.options.useCross
|
|
542
|
+
? '`--use-cross`'
|
|
543
|
+
: '`--cross-compile` (`-x`)'
|
|
544
|
+
debug.warn(
|
|
545
|
+
`The \`CARGO\` environment variable is set to \`${cargoOverride}\`; it will be spawned instead of the \`${expectedBinary}\` binary that ${requestedFlag} relies on. Unset \`CARGO\` if this is not intended.`,
|
|
300
546
|
)
|
|
301
547
|
}
|
|
302
548
|
const command =
|
|
303
|
-
|
|
549
|
+
cargoOverride ?? (this.options.useCross ? 'cross' : 'cargo')
|
|
304
550
|
const buildProcess = spawn(command, this.args, {
|
|
305
551
|
env: { ...process.env, ...this.envs },
|
|
306
552
|
stdio: watch ? ['inherit', 'inherit', 'pipe'] : 'inherit',
|
|
@@ -345,10 +591,9 @@ class Builder {
|
|
|
345
591
|
} else {
|
|
346
592
|
debug('Use %i', 'cargo-watch')
|
|
347
593
|
tryInstallCargoBinary('cargo-watch', 'watch')
|
|
348
|
-
// yarn napi watch --target x86_64-unknown-linux-gnu
|
|
594
|
+
// yarn napi watch --target x86_64-unknown-linux-gnu
|
|
349
595
|
// ===>
|
|
350
|
-
// cargo watch [...] -- build --target x86_64-unknown-linux-gnu
|
|
351
|
-
// cargo watch [...] -- zigbuild --target x86_64-unknown-linux-gnu
|
|
596
|
+
// cargo watch [...] -- cargo build --target x86_64-unknown-linux-gnu
|
|
352
597
|
this.args.push(
|
|
353
598
|
'watch',
|
|
354
599
|
'--why',
|
|
@@ -502,20 +747,20 @@ class Builder {
|
|
|
502
747
|
}
|
|
503
748
|
|
|
504
749
|
private setAndroidEnv() {
|
|
750
|
+
// Native Android hosts and `cross` provide their own Android toolchains.
|
|
751
|
+
if (process.platform === 'android' || this.options.useCross) {
|
|
752
|
+
return
|
|
753
|
+
}
|
|
754
|
+
|
|
505
755
|
const { ANDROID_NDK_LATEST_HOME } = process.env
|
|
506
756
|
if (!ANDROID_NDK_LATEST_HOME) {
|
|
507
|
-
|
|
757
|
+
throw new Error(
|
|
508
758
|
`${colors.red(
|
|
509
759
|
'ANDROID_NDK_LATEST_HOME',
|
|
510
|
-
)} environment variable is
|
|
760
|
+
)} environment variable is required when building an Android target from a non-Android host`,
|
|
511
761
|
)
|
|
512
762
|
}
|
|
513
763
|
|
|
514
|
-
// skip cross compile setup if host is android
|
|
515
|
-
if (process.platform === 'android') {
|
|
516
|
-
return
|
|
517
|
-
}
|
|
518
|
-
|
|
519
764
|
const targetArch = this.target.arch === 'arm' ? 'armv7a' : 'aarch64'
|
|
520
765
|
const targetPlatform =
|
|
521
766
|
this.target.arch === 'arm' ? 'androideabi24' : 'android24'
|
|
@@ -1075,18 +1320,14 @@ export async function generateTypeDef(
|
|
|
1075
1320
|
|
|
1076
1321
|
if (!options.noDtsHeader) {
|
|
1077
1322
|
const dtsHeader = options.dtsHeader ?? options.configDtsHeader
|
|
1078
|
-
|
|
1079
|
-
|
|
1323
|
+
const dtsHeaderFile = options.dtsHeaderFile ?? options.configDtsHeaderFile
|
|
1324
|
+
// An explicit API header file takes precedence over the config file;
|
|
1325
|
+
// either file takes precedence over inline header text.
|
|
1326
|
+
if (dtsHeaderFile) {
|
|
1080
1327
|
try {
|
|
1081
|
-
header = await readFileAsync(
|
|
1082
|
-
join(options.cwd, options.configDtsHeaderFile),
|
|
1083
|
-
'utf-8',
|
|
1084
|
-
)
|
|
1328
|
+
header = await readFileAsync(join(options.cwd, dtsHeaderFile), 'utf-8')
|
|
1085
1329
|
} catch (e) {
|
|
1086
|
-
debug.warn(
|
|
1087
|
-
`Failed to read dts header file ${options.configDtsHeaderFile}`,
|
|
1088
|
-
e,
|
|
1089
|
-
)
|
|
1330
|
+
debug.warn(`Failed to read dts header file ${dtsHeaderFile}`, e)
|
|
1090
1331
|
}
|
|
1091
1332
|
} else if (dtsHeader) {
|
|
1092
1333
|
header = dtsHeader
|
package/src/api/new.ts
CHANGED
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
statAsync,
|
|
22
22
|
type SupportedPackageManager,
|
|
23
23
|
} from '../utils/index.js'
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
napiEngineRequirement,
|
|
26
|
+
SUPPORTED_NAPI_VERSIONS,
|
|
27
|
+
} from '../utils/version.js'
|
|
25
28
|
import { renameProject } from './rename.js'
|
|
26
29
|
|
|
27
30
|
// Template imports removed as we're now using external templates
|
|
@@ -210,6 +213,52 @@ async function updateCargoTomlTypeDef(
|
|
|
210
213
|
await fs.writeFile(filePath, stringifyToml(cargoToml))
|
|
211
214
|
}
|
|
212
215
|
|
|
216
|
+
export async function updateCargoTomlNodeApiVersion(
|
|
217
|
+
filePath: string,
|
|
218
|
+
minNodeApiVersion: number,
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
221
|
+
const cargoToml = parseToml(content) as Record<string, any>
|
|
222
|
+
const dependencies = cargoToml.dependencies
|
|
223
|
+
|
|
224
|
+
if (!dependencies || !dependencies.napi) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const napiDependency = dependencies.napi
|
|
229
|
+
const dependencyConfig =
|
|
230
|
+
typeof napiDependency === 'string'
|
|
231
|
+
? { version: napiDependency }
|
|
232
|
+
: { ...napiDependency }
|
|
233
|
+
const usedDefaultFeatures = dependencyConfig['default-features'] !== false
|
|
234
|
+
const existingFeatures: string[] = Array.isArray(dependencyConfig.features)
|
|
235
|
+
? dependencyConfig.features.filter(
|
|
236
|
+
(feature: unknown): feature is string => typeof feature === 'string',
|
|
237
|
+
)
|
|
238
|
+
: []
|
|
239
|
+
|
|
240
|
+
dependencyConfig.features = [
|
|
241
|
+
`napi${minNodeApiVersion}`,
|
|
242
|
+
...existingFeatures.filter((feature) => !/^napi\d+$/.test(feature)),
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
// napi's default features include napi4. Disable them for lower requested
|
|
246
|
+
// levels, while retaining the default dynamic Node-API symbol loading mode.
|
|
247
|
+
if (minNodeApiVersion < 4) {
|
|
248
|
+
dependencyConfig['default-features'] = false
|
|
249
|
+
if (
|
|
250
|
+
usedDefaultFeatures &&
|
|
251
|
+
!dependencyConfig.features.includes('dyn-symbols')
|
|
252
|
+
) {
|
|
253
|
+
dependencyConfig.features.push('dyn-symbols')
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
dependencies.napi = dependencyConfig
|
|
258
|
+
|
|
259
|
+
await fs.writeFile(filePath, stringifyToml(cargoToml))
|
|
260
|
+
}
|
|
261
|
+
|
|
213
262
|
async function filterTargetsInGithubActions(
|
|
214
263
|
filePath: string,
|
|
215
264
|
enabledTargets: string[],
|
|
@@ -341,6 +390,17 @@ async function filterTargetsInGithubActions(
|
|
|
341
390
|
|
|
342
391
|
function processOptions(options: RawNewOptions) {
|
|
343
392
|
debug('Processing options...')
|
|
393
|
+
const minNodeApiVersion = options.minNodeApiVersion ?? 4
|
|
394
|
+
if (
|
|
395
|
+
!Number.isInteger(minNodeApiVersion) ||
|
|
396
|
+
!SUPPORTED_NAPI_VERSIONS.some((version) => version === minNodeApiVersion)
|
|
397
|
+
) {
|
|
398
|
+
throw new RangeError(
|
|
399
|
+
`Unsupported Node-API version ${minNodeApiVersion}. Expected one of: ${SUPPORTED_NAPI_VERSIONS.join(', ')}`,
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
options.minNodeApiVersion = minNodeApiVersion
|
|
403
|
+
|
|
344
404
|
if (!options.path) {
|
|
345
405
|
throw new Error('Please provide the path as the argument')
|
|
346
406
|
}
|
|
@@ -424,6 +484,10 @@ export async function newProject(userOptions: RawNewOptions) {
|
|
|
424
484
|
const cargoTomlPath = path.join(options.path, 'Cargo.toml')
|
|
425
485
|
if (existsSync(cargoTomlPath)) {
|
|
426
486
|
await updateCargoTomlTypeDef(cargoTomlPath, options.enableTypeDef)
|
|
487
|
+
await updateCargoTomlNodeApiVersion(
|
|
488
|
+
cargoTomlPath,
|
|
489
|
+
options.minNodeApiVersion,
|
|
490
|
+
)
|
|
427
491
|
}
|
|
428
492
|
|
|
429
493
|
// Filter targets in package.json
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
|
|
3
|
+
import { getNapiVersionChoices } from '../new.js'
|
|
4
|
+
import { SUPPORTED_NAPI_VERSIONS } from '../../utils/version.js'
|
|
5
|
+
|
|
6
|
+
test('napi version prompt choices track supported versions', (t) => {
|
|
7
|
+
const choices = getNapiVersionChoices()
|
|
8
|
+
|
|
9
|
+
t.deepEqual(
|
|
10
|
+
choices.map((choice) => choice.value),
|
|
11
|
+
SUPPORTED_NAPI_VERSIONS,
|
|
12
|
+
)
|
|
13
|
+
})
|