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