@napi-rs/cli
Version:
Cli tools for napi-rs
1,518 lines (1,401 loc) • 81.9 kB
text/typescript
import { spawn } from 'node:child_process'
import { createHash } from 'node:crypto'
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'
import { mkdtemp as mkdtempAsync, rm } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { homedir } from 'node:os'
import {
basename,
dirname,
isAbsolute,
join,
parse,
relative,
resolve,
sep,
} from 'node:path'
import * as colors from 'colorette'
import type { BuildOptions as RawBuildOptions } from '../def/build.js'
import {
CLI_VERSION,
commitFileSystemTransaction,
copyFileAtomic,
type Crate,
debugFactory,
DEFAULT_TYPE_DEF_HEADER,
fileExists,
getSystemDefaultTarget,
getNapiDeriveDependentCrates,
getPackageReconciliationRoot,
getTargetLinker,
mkdirAsync,
type NapiConfig,
parseMetadata,
parseTriple,
processTypeDefs,
readFileAsync,
readNapiConfig,
rebaseDeclarationSpecifiers,
removeNodeStreamWebTypeImports,
rewriteUnboundNodeGlobalTypeQueries,
rewriteTypeImportReferences,
type Target,
targetToEnvVar,
tryInstallCargoBinary,
unlinkAsync,
wasiLoaderSuffix,
wasiTargetHasThreads,
writeFileAtomic,
withFileSystemReconciliation,
dirExistsAsync,
readdirAsync,
type CargoWorkspaceMetadata,
} from '../utils/index.js'
import { createCjsBinding, createEsmBinding } from './templates/index.js'
import {
createWasiBinding,
createWasiBrowserBinding,
createWasiDeferredBrowserBinding,
createWasiDeferredBrowserBindingTypeDef,
} from './templates/load-wasi-template.js'
import {
createWasiBrowserWorkerBinding,
WASI_WORKER_TEMPLATE,
} from './templates/wasi-worker-template.js'
const debug = debugFactory('build')
const require = createRequire(import.meta.url)
const MANAGED_WASI_FLAVORS = [
{ platformArchABI: 'wasm32-wasi', loaderSuffix: 'wasi', hasThreads: true },
{
platformArchABI: 'wasm32-wasip1',
loaderSuffix: 'wasip1',
hasThreads: false,
},
] as const
type OutputKind = 'js' | 'dts' | 'node' | 'exe' | 'wasm'
type Output = { kind: OutputKind; path: string }
type WasiBindingMetadata = {
exports: string[]
bindingTypeDef?: string
}
type BuildOptions = RawBuildOptions & { cargoOptions?: string[] }
type ParsedBuildOptions = Omit<BuildOptions, 'cwd'> & { cwd: string }
export const WASI_ARTIFACT_METADATA_PREFIX = '// napi-rs-artifact-metadata:'
type CargoConfigFingerprint = readonly [path: string, hash: string]
export function getCargoDependencyGraphFingerprint(
metadata: CargoWorkspaceMetadata,
rootPackageId: string,
): string {
const packages = new Map(metadata.packages.map((pkg) => [pkg.id, pkg]))
const nodes = new Map(
(metadata.resolve?.nodes ?? []).map((node) => [node.id, node]),
)
const pending = [rootPackageId]
const visited = new Set<string>()
const graph = []
while (pending.length > 0) {
const id = pending.pop()!
if (visited.has(id)) {
continue
}
visited.add(id)
const node = nodes.get(id)
const pkg = packages.get(id)
const dependencies = (node?.deps ?? [])
.map((dependency) => ({
name: dependency.name,
pkg: dependency.pkg,
kinds: dependency.dep_kinds
.map(({ kind, target }) => ({ kind, target }))
.sort(
(left, right) =>
(left.kind ?? '').localeCompare(right.kind ?? '') ||
(left.target ?? '').localeCompare(right.target ?? ''),
),
}))
.sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
)
for (const dependency of dependencies) {
pending.push(dependency.pkg)
}
graph.push({
id,
manifestPath: pkg?.manifest_path,
resolvedFeatures: [...(node?.features ?? [])].sort(),
dependencies,
declaredDependencies: (pkg?.dependencies ?? [])
.map((dependency) => ({
name: dependency.name,
source: dependency.source,
requirement: dependency.req,
kind: dependency.kind,
rename: dependency.rename,
optional: dependency.optional,
usesDefaultFeatures: dependency.uses_default_features,
features: [...dependency.features].sort(),
target: dependency.target,
registry: dependency.registry,
}))
.sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
),
declaredFeatures: Object.entries(pkg?.features ?? {})
.map(([name, features]) => [name, [...features].sort()] as const)
.sort(([left], [right]) => left.localeCompare(right)),
})
}
graph.sort((left, right) => left.id.localeCompare(right.id))
return createHash('sha256').update(JSON.stringify(graph)).digest('hex')
}
export function getTypeDefCacheFolder(options: {
targetDir: string
crateName: string
manifestPath: string
targetTriple: string
profile: string
features?: string[]
allFeatures?: boolean
noDefaultFeatures?: boolean
cargoOptions?: string[]
rustFlags?: Record<string, string | undefined>
cargoProfileEnv?: Record<string, string | undefined>
cargoConfig?: CargoConfigFingerprint[]
cargoDependencyGraph?: string
}) {
const features = [
...new Set(
(options.features ?? []).flatMap((feature) =>
feature.split(/[,\s]+/).filter(Boolean),
),
),
].sort()
const rustFlags = Object.entries(options.rustFlags ?? {})
.filter((entry): entry is [string, string] => entry[1] !== undefined)
.sort(([left], [right]) => left.localeCompare(right))
const cargoConfig = [...(options.cargoConfig ?? [])].sort(([left], [right]) =>
left.localeCompare(right),
)
const cargoProfileEnv = Object.entries(options.cargoProfileEnv ?? {})
.filter((entry): entry is [string, string] => entry[1] !== undefined)
.sort(([left], [right]) => left.localeCompare(right))
const identity = JSON.stringify({
version: 4,
cliVersion: CLI_VERSION,
manifestPath: options.manifestPath,
cargoDependencyGraph: options.cargoDependencyGraph,
targetTriple: options.targetTriple,
profile: options.profile,
features: {
selected: features,
all: options.allFeatures === true,
noDefault: options.noDefaultFeatures === true,
},
cargoOptions: options.cargoOptions ?? [],
rustFlags,
cargoProfileEnv,
cargoConfig,
})
const hash = createHash('sha256')
.update(identity)
.digest('hex')
.substring(0, 16)
return join(options.targetDir, 'napi-rs', `${options.crateName}-${hash}`)
}
export function createWasiCompilerFlags(
wasiSdkPath: string,
wasiTarget: string,
hasThreads: boolean,
shellEscapedFlags = true,
) {
const compileArguments = [
`--target=${wasiTarget}`,
`--sysroot=${join(wasiSdkPath, 'share', 'wasi-sysroot')}`,
...(hasThreads ? ['-pthread'] : []),
'-mllvm',
'-wasm-enable-sjlj',
]
const linkerArguments = [
`-fuse-ld=${join(wasiSdkPath, 'bin', 'wasm-ld')}`,
`--target=${wasiTarget}`,
]
return {
compileFlags: joinCompilerArguments(compileArguments, shellEscapedFlags),
linkerFlags: joinCompilerArguments(linkerArguments, shellEscapedFlags),
}
}
export function createArtifactDestinationName(
binaryName: string,
target: Target,
sourceName: string,
platform: boolean,
) {
let destinationName = binaryName
if (platform || target.platform === 'wasi') {
destinationName += `.${target.platformArchABI}`
}
return `${destinationName}.${sourceName.endsWith('.wasm') ? 'wasm' : 'node'}`
}
function joinCompilerArguments(
arguments_: string[],
shellEscapedFlags: boolean,
) {
if (!shellEscapedFlags) {
const argumentWithWhitespace = arguments_.find((argument) =>
/\s/.test(argument),
)
if (argumentWithWhitespace) {
throw new Error(
`CC_SHELL_ESCAPED_FLAGS disables shell parsing, so the WASI SDK argument cannot contain whitespace: ${argumentWithWhitespace}`,
)
}
return arguments_.join(' ')
}
return arguments_
.map((argument) => `'${argument.replaceAll("'", "'\\''")}'`)
.join(' ')
}
function envBoolean(value: string | undefined) {
return (
value !== undefined &&
value !== '' &&
value !== '0' &&
value !== 'false' &&
value !== 'no'
)
}
function createWasiArtifactMetadata(
rootEntry: string | null,
binaryName: string,
exports: string[],
) {
return `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
version: 2,
rootEntry,
exports,
managedRootEntries: [
'browser.js',
...(rootEntry ? [rootEntry] : []),
`${binaryName}.wasm`,
`${binaryName}.debug.wasm`,
],
})}\n`
}
function parseExistingWasiArtifactMetadata(content: string) {
const firstLine = content.split(/\r?\n/, 1)[0]
if (!firstLine.startsWith(WASI_ARTIFACT_METADATA_PREFIX)) {
return undefined
}
try {
const metadata = JSON.parse(
firstLine.slice(WASI_ARTIFACT_METADATA_PREFIX.length),
) as Record<string, unknown>
if (
(metadata.version !== 1 && metadata.version !== 2) ||
(metadata.rootEntry !== null && typeof metadata.rootEntry !== 'string')
) {
return undefined
}
const managedRootEntries =
metadata.version === 1
? ['browser.js', ...(metadata.rootEntry ? [metadata.rootEntry] : [])]
: metadata.managedRootEntries
if (
!Array.isArray(managedRootEntries) ||
!managedRootEntries.every((entry) => typeof entry === 'string')
) {
return undefined
}
const exports = metadata.exports
return {
managedRootEntries,
exports:
Array.isArray(exports) &&
exports.every((entry) => typeof entry === 'string')
? exports
: undefined,
}
} catch {
return undefined
}
}
function resolveManagedOutputPath(
outputDir: string,
entry: string,
description: string,
) {
if (!entry || isAbsolute(entry)) {
throw new Error(
`${description} must be a non-empty relative path: ${entry}`,
)
}
const outputRoot = resolve(outputDir)
const path = resolve(outputRoot, entry)
const relativePath = relative(outputRoot, path)
if (
relativePath === '' ||
relativePath === '..' ||
relativePath.startsWith(`..${sep}`) ||
isAbsolute(relativePath)
) {
throw new Error(`${description} escapes its output directory: ${entry}`)
}
return path
}
export function selectWasiBrowserTarget(
buildTarget: Target,
configuredTargets: Target[],
emittedTargets: Target[],
) {
if (buildTarget.platform === 'wasi') {
return buildTarget
}
const configuredWasiTargets = configuredTargets.filter(
(target) => target.platform === 'wasi',
)
return (
configuredWasiTargets.find((target) => !wasiTargetHasThreads(target)) ??
configuredWasiTargets[0] ??
emittedTargets.find((target) => !wasiTargetHasThreads(target)) ??
emittedTargets[0]
)
}
export function createWasiBrowserEntry(
packageName: string,
platformArchABI: string,
idents: string[],
) {
const packageSpecifier = `${packageName}-${platformArchABI}`
return (
`export * from '${packageSpecifier}'\n` +
(idents.length === 0
? `export { default } from '${packageSpecifier}'\n`
: '')
)
}
export function createWasiDeferredBindingTypeDef(
bindingModuleSpecifier: string,
hasTypeDef: boolean,
) {
const typeDef = createWasiDeferredBrowserBindingTypeDef(
bindingModuleSpecifier,
)
if (hasTypeDef) {
return typeDef
}
const rootBindingType = `typeof import('${bindingModuleSpecifier}')`
if (!typeDef.includes(rootBindingType)) {
throw new Error(
'The deferred WASI type definition no longer contains its root binding type',
)
}
return typeDef.replace(rootBindingType, 'Record<string, unknown>')
}
export function prepareWasiBindingTypeDef(
source: string,
sourcePath: string,
destinationPath: string,
hasThreads: boolean,
packageType?: 'module' | 'commonjs',
) {
if (
sourcePath.endsWith('.d.mts') ||
(sourcePath.endsWith('.d.ts') && packageType === 'module')
) {
throw new Error(
`Cannot emit the CommonJS WASI declaration ${destinationPath} from the ESM declaration ${sourcePath}. Use a .d.cts --dts path for WASI builds in module packages.`,
)
}
const targetSource = hasThreads
? source
: rewriteUnboundNodeGlobalTypeQueries(
removeNodeStreamWebTypeImports(source),
)
return rebaseDeclarationSpecifiers(targetSource, sourcePath, destinationPath)
}
export function collectStaleWasiBuildOutputNames(
binaryName: string,
buildTarget: Target,
configuredTargets: Target[],
) {
const staleNames = new Set<string>()
const retainedFlavors = new Set(
[
...configuredTargets.filter((target) => target.platform === 'wasi'),
buildTarget,
].map((target) => target.platformArchABI),
)
for (const flavor of MANAGED_WASI_FLAVORS) {
const regenerated = flavor.platformArchABI === buildTarget.platformArchABI
if (!regenerated && retainedFlavors.has(flavor.platformArchABI)) {
continue
}
for (const suffix of [
`${flavor.loaderSuffix}.cjs`,
`${flavor.loaderSuffix}.d.cts`,
`${flavor.loaderSuffix}-browser.js`,
`${flavor.loaderSuffix}-deferred.js`,
`${flavor.loaderSuffix}-deferred.d.ts`,
...(!regenerated
? [
`${flavor.platformArchABI}.wasm`,
`${flavor.platformArchABI}.debug.wasm`,
]
: []),
]) {
staleNames.add(`${binaryName}.${suffix}`)
}
}
const regeneratesWorkers = wasiTargetHasThreads(buildTarget)
const retainsThreadedFlavor = MANAGED_WASI_FLAVORS.some(
(flavor) =>
flavor.hasThreads && retainedFlavors.has(flavor.platformArchABI),
)
if (regeneratesWorkers || !retainsThreadedFlavor) {
staleNames.add('wasi-worker.mjs')
staleNames.add('wasi-worker-browser.mjs')
}
return staleNames
}
export function removeWasmCustomSection(
binary: Uint8Array,
sectionName: string,
): Uint8Array {
if (
binary.length < 8 ||
binary[0] !== 0x00 ||
binary[1] !== 0x61 ||
binary[2] !== 0x73 ||
binary[3] !== 0x6d
) {
throw new Error('Invalid WebAssembly module header')
}
const retainedChunks: Uint8Array[] = []
let retainedStart = 0
let offset = 8
let removed = false
while (offset < binary.length) {
const sectionStart = offset
const sectionId = binary[offset++]
const sectionSize = readWasmU32(binary, offset)
const payloadStart = sectionSize.nextOffset
const sectionEnd = payloadStart + sectionSize.value
if (sectionEnd > binary.length) {
throw new Error('Invalid WebAssembly section size')
}
let shouldRemove = false
if (sectionId === 0) {
const nameSize = readWasmU32(binary, payloadStart)
const nameEnd = nameSize.nextOffset + nameSize.value
if (nameEnd > sectionEnd) {
throw new Error('Invalid WebAssembly custom section name')
}
shouldRemove =
Buffer.from(binary.subarray(nameSize.nextOffset, nameEnd)).toString(
'utf8',
) === sectionName
}
if (shouldRemove) {
retainedChunks.push(binary.subarray(retainedStart, sectionStart))
retainedStart = sectionEnd
removed = true
}
offset = sectionEnd
}
if (!removed) {
return binary
}
retainedChunks.push(binary.subarray(retainedStart))
const outputLength = retainedChunks.reduce(
(length, chunk) => length + chunk.length,
0,
)
const output = new Uint8Array(outputLength)
let outputOffset = 0
for (const chunk of retainedChunks) {
output.set(chunk, outputOffset)
outputOffset += chunk.length
}
return output
}
function readWasmU32(binary: Uint8Array, start: number) {
let value = 0
let shift = 0
let offset = start
while (offset < binary.length && shift <= 28) {
const byte = binary[offset++]
value += (byte & 0x7f) * 2 ** shift
if ((byte & 0x80) === 0) {
return { value, nextOffset: offset }
}
shift += 7
}
throw new Error('Invalid WebAssembly unsigned LEB128 value')
}
export async function buildProject(rawOptions: BuildOptions) {
debug('napi build command receive options: %O', rawOptions)
const options: ParsedBuildOptions = {
dtsCache: true,
...rawOptions,
cwd: rawOptions.cwd ?? process.cwd(),
}
// Reject invalid cross-compilation flag combinations before anything with
// a side effect runs (`cargo metadata`, cargo binary auto-installs,
// toolchain downloads, ...), so the user always gets the validation error
// rather than an unrelated failure from those steps.
validateCrossCompileFlags(options)
if (options.useNapiCross) {
// Check the host constraint before resolving the target: without an
// explicit `--target` (or `CARGO_BUILD_TARGET`) the target resolution
// spawns `rustc -vV`, and on an unsupported host without Rust on the
// `PATH` that spawn failure would mask the actual validation error.
validateNapiCrossHost()
validateNapiCrossSupport(resolveTarget(options.target).triple)
}
const resolvePath = (...paths: string[]) => resolve(options.cwd, ...paths)
const manifestPath = resolvePath(options.manifestPath ?? 'Cargo.toml')
const metadataTarget = options.target ?? process.env.CARGO_BUILD_TARGET
const metadata = await parseMetadata(manifestPath, {
cwd: options.cwd,
featurePackage: options.package,
features: options.features,
allFeatures: options.allFeatures,
noDefaultFeatures: options.noDefaultFeatures,
cargoOptions: options.cargoOptions,
filterPlatform: metadataTarget
? parseTriple(metadataTarget).triple
: undefined,
})
const crate = metadata.packages.find((p) => {
// package with given name
if (options.package) {
return p.name === options.package
} else {
return p.manifest_path === manifestPath
}
})
if (!crate) {
throw new Error(
'Unable to find crate to build. It seems you are trying to build a crate in a workspace, try using `--package` option to specify the package to build.',
)
}
const config = await readNapiConfig(
resolvePath(options.packageJsonPath ?? 'package.json'),
options.configPath ? resolvePath(options.configPath) : undefined,
)
const builder = new Builder(metadata, crate, config, options)
return builder.build()
}
/**
* Resolve the target triple the build will run against, following the same
* precedence the build itself uses: the explicit `--target` option, then the
* `CARGO_BUILD_TARGET` environment variable, then the host default target.
*/
function resolveTarget(targetOption?: string): Target {
return targetOption
? parseTriple(targetOption)
: process.env.CARGO_BUILD_TARGET
? parseTriple(process.env.CARGO_BUILD_TARGET)
: getSystemDefaultTarget()
}
/**
* Validate the combination of the cross-compilation related flags.
*
* `--use-cross`, `--use-napi-cross` and `--cross-compile` (`-x`) are three
* mutually exclusive cross-compilation mechanisms; combining any two of them
* leaves both active at once and produces broken builds, so it is rejected
* upfront before any side effect (like auto-installing cargo binaries or
* downloading toolchains) happens.
*
* `--cross-compile` with a `windows-gnu` target is rejected as well: on a
* non-Windows host it routes the build to `cargo xwin build`, but
* `cargo-xwin` only sets up MSVC toolchains — for `windows-gnu` targets it
* silently does nothing and the build dies much later with a cryptic
* ``error: linker `x86_64-w64-mingw32-gcc` not found`` that never mentions
* `cargo-xwin`. Only an explicitly requested target (`--target` or
* `CARGO_BUILD_TARGET`) is inspected, so this validation never has to spawn
* `rustc -vV`; a non-Windows host's default target can never be
* `windows-gnu` anyway.
*/
export function validateCrossCompileFlags(
options: {
useCross?: boolean
crossCompile?: boolean
useNapiCross?: boolean
watch?: boolean
target?: string
},
hostPlatform: string = process.platform,
): void {
const enabledCrossFlags = [
options.useCross ? '`--use-cross`' : null,
options.useNapiCross ? '`--use-napi-cross`' : null,
options.crossCompile ? '`--cross-compile` (`-x`)' : null,
].filter((flag): flag is string => flag !== null)
if (enabledCrossFlags.length > 1) {
throw new Error(
`${enabledCrossFlags.join(' and ')} cannot be used together. Please pick exactly one cross-compilation mechanism: \`--use-cross\`, \`--use-napi-cross\`, or \`--cross-compile\` (\`-x\`).`,
)
}
if (options.watch && options.useCross) {
throw new Error(
'`--watch` cannot be used with `--use-cross`. `cargo watch` only supports the plain `cargo build` flow, please drop one of the two flags.',
)
}
if (options.watch && options.crossCompile) {
throw new Error(
'`--watch` cannot be used with `--cross-compile` (`-x`). `cargo watch` only supports the plain `cargo build` flow, please drop one of the two flags.',
)
}
// On a Windows host `--cross-compile` never picks `cargo-xwin` (it falls
// back to a plain `cargo build`), so windows-gnu targets are only broken
// on non-Windows hosts.
if (options.crossCompile && hostPlatform !== 'win32') {
const explicitTarget = options.target ?? process.env.CARGO_BUILD_TARGET
if (explicitTarget) {
const target = parseTriple(explicitTarget)
if (target.platform === 'win32' && target.abi?.startsWith('gnu')) {
const msvcTriple = explicitTarget.replace(/gnu(llvm)?$/, 'msvc')
// `*-windows-gnu` links with a mingw-w64 GCC toolchain, while
// `*-windows-gnullvm` needs an LLVM toolchain (llvm-mingw): `rustc`
// has no default working linker for it without one on the `PATH`.
const toolchainHint =
target.abi === 'gnullvm'
? `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)`
: `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)`
throw new Error(
`\`--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.`,
)
}
}
}
}
/**
* Validate that the current host can run the `@napi-rs/cross-toolchain`
* pre-built toolchains at all: they only run on Linux x64 and Linux arm64
* hosts. This check is separate from (and must run before) the per-target
* validation, because resolving the target may need to spawn `rustc`.
*/
function validateNapiCrossHost(
hostPlatform: string = process.platform,
hostArch: string = process.arch,
): asserts hostArch is 'x64' | 'arm64' {
if (
hostPlatform !== 'linux' ||
(hostArch !== 'x64' && hostArch !== 'arm64')
) {
throw new Error(
`\`--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.`,
)
}
}
/**
* Validate that `--use-napi-cross` can actually handle the requested target
* on the current host. The supported target set is read from the
* `@napi-rs/cross-toolchain` package, and the pre-built toolchains only run
* on Linux x64 and Linux arm64 hosts.
*/
export function validateNapiCrossSupport(
targetTriple: string,
hostPlatform: string = process.platform,
hostArch: string = process.arch,
): void {
validateNapiCrossHost(hostPlatform, hostArch)
const toolchains: Record<
'x64' | 'arm64',
Record<string, string | undefined>
> = require('@napi-rs/cross-toolchain')
const supportedTargets = Object.keys(toolchains[hostArch])
if (!supportedTargets.includes(targetTriple)) {
throw new Error(
`\`--use-napi-cross\` does not support the target ${targetTriple}. Supported targets: ${supportedTargets.join(', ')}. Please use \`--cross-compile\` (\`-x\`) or \`--use-cross\` for this target.`,
)
}
}
/**
* Compute the environment variables that route a `--use-napi-cross` build
* through the `@napi-rs/cross-toolchain` toolchain extracted at
* `toolchainPath`.
*
* Follows the same rule as `Builder#setEnvIfNotExists`: a variable the user
* already set in `env` wins and is not returned, and a present-but-empty
* variable counts as unset (falsy semantics). The only exceptions are the
* clang-specific `TARGET_CFLAGS`/`TARGET_CXXFLAGS`, which prepend the sysroot
* flags to the user's value, and `PATH`, which always gets the toolchain
* `bin` directory prepended.
*
* Exported for tests.
*/
export function napiCrossToolchainEnvs(
toolchainPath: string,
targetTriple: string,
env: NodeJS.ProcessEnv = process.env,
): Record<string, string> {
const alias: Record<string, string> = {
's390x-unknown-linux-gnu': 's390x-ibm-linux-gnu',
}
const envs: Record<string, string> = {}
const setEnvIfNotExists = (name: string, value: string) => {
if (!env[name]) {
envs[name] = value
}
}
const upperCaseTarget = targetToEnvVar(targetTriple)
const crossTargetName = alias[targetTriple] ?? targetTriple
setEnvIfNotExists(
`CARGO_TARGET_${upperCaseTarget}_LINKER`,
join(toolchainPath, 'bin', `${crossTargetName}-gcc`),
)
setEnvIfNotExists(
'TARGET_SYSROOT',
join(toolchainPath, crossTargetName, 'sysroot'),
)
setEnvIfNotExists(
'TARGET_AR',
join(toolchainPath, 'bin', `${crossTargetName}-ar`),
)
setEnvIfNotExists(
'TARGET_RANLIB',
join(toolchainPath, 'bin', `${crossTargetName}-ranlib`),
)
setEnvIfNotExists(
'TARGET_READELF',
join(toolchainPath, 'bin', `${crossTargetName}-readelf`),
)
setEnvIfNotExists(
'TARGET_C_INCLUDE_PATH',
join(toolchainPath, crossTargetName, 'sysroot', 'usr', 'include/'),
)
setEnvIfNotExists(
'TARGET_CC',
join(toolchainPath, 'bin', `${crossTargetName}-gcc`),
)
setEnvIfNotExists(
'TARGET_CXX',
join(toolchainPath, 'bin', `${crossTargetName}-g++`),
)
// `setEnvIfNotExists` skips `envs` when the user already set the variable
// in their environment, so read the effective value back from `env` first
// — with the same falsy semantics: a present-but-empty `TARGET_SYSROOT`
// counts as unset and falls back to the downloaded sysroot (`??` would
// keep the empty string and produce a broken `--sysroot=`).
const targetSysroot = env.TARGET_SYSROOT || envs.TARGET_SYSROOT
setEnvIfNotExists('BINDGEN_EXTRA_CLANG_ARGS', `--sysroot=${targetSysroot}`)
// cc-rs parses the env value before executing it (`env_tool` in cc's
// lib.rs): when the WHOLE value exists on the filesystem (`check_exe`)
// it is the compiler as-is — that is how it supports spaces in paths
// like `/opt/LLVM 18/bin/clang` — and only otherwise is the value split
// on whitespace; then, when the first token is a known wrapper
// (`sccache clang`) the second token is the compiler that actually runs,
// otherwise the first token is and the rest are arguments
// (`clang -target …`). Mirror that ordering, filesystem probe included:
// a pure whole-value basename heuristic would misread argument forms
// that merely END in `clang`, like `gcc --sysroot=/opt/clang`, and
// inject clang-only flags into a gcc compile. Match on the executable
// name so path-qualified (`/usr/bin/clang`), triple-prefixed
// (`aarch64-linux-gnu-clang`) and versioned (`clang-18`) compilers are
// all recognized — but not clang-family tools that are not compilers
// (`clang-format`).
const ccWrappers = new Set([
'ccache',
'distcc',
'sccache',
'icecc',
'cachepot',
'buildcache',
'kache',
])
const clangExecutableName = /(^|-)clang(\+\+)?(-\d+)?$/
// cc-rs's `check_exe` only probes for existence (plus an `.exe` retry on
// Windows, where this napi-cross path never runs); requiring a regular
// file additionally keeps directories from being taken for compilers.
const isExistingFile = (path: string): boolean => {
try {
return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false
} catch {
return false
}
}
const isClangCompiler = (value: string | undefined): boolean => {
if (!value) {
return false
}
const trimmed = value.trim()
if (isExistingFile(trimmed)) {
return clangExecutableName.test(basename(trimmed))
}
const [first, second] = trimmed.split(/\s+/)
const compiler = first && ccWrappers.has(basename(first)) ? second : first
return (
compiler !== undefined && clangExecutableName.test(basename(compiler))
)
}
// Detect clang on the EFFECTIVE target compiler: the user's TARGET_CC if
// set, otherwise the toolchain gcc exported above (`envs.TARGET_CC`).
// cc-rs prefers TARGET_CC over CC for cross builds, so a plain `CC=clang`
// never runs here and must not trigger the clang-only `--gcc-toolchain=`
// flag (gcc hard-errors on it). The trailing `env.CC`/`env.CXX` terms are
// unreachable today (exactly one of the first two is always truthy) but
// keep the check correct if the toolchain default ever becomes conditional.
if (isClangCompiler(env.TARGET_CC || envs.TARGET_CC || env.CC)) {
const TARGET_CFLAGS = env.TARGET_CFLAGS || ''
envs.TARGET_CFLAGS = `--sysroot=${targetSysroot} --gcc-toolchain=${toolchainPath} ${TARGET_CFLAGS}`
}
if (isClangCompiler(env.TARGET_CXX || envs.TARGET_CXX || env.CXX)) {
const TARGET_CXXFLAGS = env.TARGET_CXXFLAGS || ''
envs.TARGET_CXXFLAGS = `--sysroot=${targetSysroot} --gcc-toolchain=${toolchainPath} ${TARGET_CXXFLAGS}`
}
envs.PATH = env.PATH
? `${toolchainPath}/bin:${env.PATH}`
: `${toolchainPath}/bin`
return envs
}
class Builder {
private readonly args: string[] = []
private readonly envs: Record<string, string> = {}
private readonly outputs: Output[] = []
private readonly target: Target
private readonly crateDir: string
private readonly finalOutputDir: string
private outputDir: string
private readonly reconciliationRoot: string
private readonly targetDir: string
private readonly enableTypeDef: boolean = false
private readonly stagedOutputDestinations = new Map<string, string>()
private typeDefWithTypeImports: string | undefined
constructor(
private readonly metadata: CargoWorkspaceMetadata,
private readonly crate: Crate,
private readonly config: NapiConfig,
private readonly options: ParsedBuildOptions,
) {
this.target = resolveTarget(options.target)
this.crateDir = parse(crate.manifest_path).dir
this.finalOutputDir = resolve(
this.options.cwd,
options.outputDir ?? this.crateDir,
)
this.outputDir = this.finalOutputDir
this.reconciliationRoot = getPackageReconciliationRoot(
this.options.cwd,
this.options.packageJsonPath,
)
this.targetDir =
options.targetDir ??
process.env.CARGO_BUILD_TARGET_DIR ??
metadata.target_directory
this.enableTypeDef = this.crate.dependencies.some(
(dep) =>
dep.name === 'napi-derive' &&
(dep.uses_default_features || dep.features.includes('type-def')),
)
if (!this.enableTypeDef) {
const requirementWarning =
'`napi-derive` crate is not used or `type-def` feature is not enabled for `napi-derive` crate'
debug.warn(
`${requirementWarning}. Will skip binding generation for \`.node\`, \`.wasi\` and \`.d.ts\` files.`,
)
if (
this.options.dts ||
this.options.dtsHeader ||
this.config.dtsHeader ||
this.config.dtsHeaderFile
) {
debug.warn(
`${requirementWarning}. \`dts\` related options are enabled but will be ignored.`,
)
}
}
}
get cdyLibName() {
if (this.options.bin) {
return
}
return this.crate.targets.find((t) => t.crate_types.includes('cdylib'))
?.name
}
get binName() {
return (
this.options.bin ??
// only available if not cdylib or bin name specified
(this.cdyLibName
? null
: this.crate.targets.find((t) => t.crate_types.includes('bin'))?.name)
)
}
async build() {
// Backstop only: `buildProject()` already validated these before running
// anything with a side effect (see the top of `buildProject`). Kept here
// so a directly constructed `Builder` cannot skip the validation.
validateCrossCompileFlags(this.options)
if (this.options.useNapiCross) {
validateNapiCrossSupport(this.target.triple)
}
if (this.options.bin) {
debug.warn(
`Building Cargo binary target ${this.binName}; the result will be an executable, not a Node.js addon.`,
)
} else if (!this.cdyLibName) {
const warning =
'Missing `crate-type = ["cdylib"]` in [lib] config. The build result will not be available as node addon.'
if (this.binName) {
debug.warn(warning)
} else {
throw new Error(warning)
}
}
this.pickBinary()
.setPackage()
.setFeatures()
.setTarget()
.pickCrossToolchain()
await this.setEnvs()
return this.setBypassArgs().exec()
}
private pickCrossToolchain() {
if (!this.options.useNapiCross) {
return this
}
try {
const { version, download } = require('@napi-rs/cross-toolchain')
const toolchainPath = join(
homedir(),
'.napi-rs',
'cross-toolchain',
version,
this.target.triple,
)
mkdirSync(toolchainPath, { recursive: true })
if (existsSync(join(toolchainPath, 'package.json'))) {
debug(`Toolchain ${toolchainPath} exists, skip extracting`)
} else {
const tarArchive = download(process.arch, this.target.triple)
tarArchive.unpack(toolchainPath)
}
Object.assign(
this.envs,
napiCrossToolchainEnvs(toolchainPath, this.target.triple),
)
} catch (e) {
throw new Error(
`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.`,
{ cause: e },
)
}
return this
}
private exec() {
debug(`Start building crate: ${this.crate.name}`)
debug(' %i', `cargo ${this.args.join(' ')}`)
const controller = new AbortController()
const watch = this.options.watch
const buildTask = new Promise<void>((resolve, reject) => {
const cargoOverride = process.env.CARGO
if (
cargoOverride &&
(this.options.useCross || this.options.crossCompile)
) {
const expectedBinary = this.options.useCross ? 'cross' : 'cargo'
const requestedFlag = this.options.useCross
? '`--use-cross`'
: '`--cross-compile` (`-x`)'
debug.warn(
`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.`,
)
}
const command =
cargoOverride ?? (this.options.useCross ? 'cross' : 'cargo')
const buildProcess = spawn(command, this.args, {
env: { ...process.env, ...this.envs },
stdio: watch ? ['inherit', 'inherit', 'pipe'] : 'inherit',
cwd: this.options.cwd,
signal: controller.signal,
})
buildProcess.once('exit', (code) => {
if (code === 0) {
debug('%i', `Build crate ${this.crate.name} successfully!`)
resolve()
} else {
reject(new Error(`Build failed with exit code ${code}`))
}
})
buildProcess.once('error', (e) => {
reject(new Error(`Build failed with error: ${e.message}`, { cause: e }))
})
// watch mode only, they are piped through stderr
buildProcess.stderr?.on('data', (data) => {
const output = data.toString()
console.error(output)
if (/Finished\s(`dev`|`release`)/.test(output)) {
this.postBuild().catch(() => {})
}
})
})
return {
task: buildTask.then(() => this.postBuild()),
abort: () => controller.abort(),
}
}
private async collectStaleBuildOutputPaths() {
const stalePaths = new Set<string>()
const managedRootEntries = await this.readManagedWasiRootEntries()
for (const name of collectStaleWasiBuildOutputNames(
this.config.binaryName,
this.target,
this.config.targets,
)) {
stalePaths.add(join(this.outputDir, name))
}
const hasWasiOutput =
this.target.platform === 'wasi' ||
this.config.targets.some((target) => target.platform === 'wasi') ||
managedRootEntries.size > 0
if (hasWasiOutput) {
stalePaths.add(join(this.outputDir, 'browser.js'))
stalePaths.add(join(this.outputDir, `${this.config.binaryName}.wasm`))
stalePaths.add(
join(this.outputDir, `${this.config.binaryName}.debug.wasm`),
)
if (this.options.platform && !this.options.noJsBinding) {
stalePaths.add(
join(this.outputDir, this.options.jsBinding ?? 'index.js'),
)
}
}
for (const entry of managedRootEntries) {
stalePaths.add(
resolveManagedOutputPath(this.outputDir, entry, 'WASI root entry'),
)
}
return stalePaths
}
private async unlinkIfExists(path: string) {
try {
await unlinkAsync(path)
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error
}
}
}
private async readManagedWasiRootEntries() {
const entries = new Set<string>()
for (const { loaderSuffix } of MANAGED_WASI_FLAVORS) {
const path = join(
this.outputDir,
`${this.config.binaryName}.${loaderSuffix}.cjs`,
)
if (!(await fileExists(path))) {
continue
}
const metadata = parseExistingWasiArtifactMetadata(
await readFileAsync(path, 'utf8'),
)
for (const entry of metadata?.managedRootEntries ?? []) {
entries.add(entry)
}
}
return entries
}
private async readExistingWasiBindingMetadata(wasiTarget: Target) {
const loaderSuffix = wasiLoaderSuffix(wasiTarget.platformArchABI)
const bindingPath = join(
this.finalOutputDir,
`${this.config.binaryName}.${loaderSuffix}.cjs`,
)
const bindingTypeDefPath = join(
this.finalOutputDir,
`${this.config.binaryName}.${loaderSuffix}.d.cts`,
)
if (
!(await fileExists(bindingPath)) ||
!(await fileExists(bindingTypeDefPath))
) {
return undefined
}
const artifactMetadata = parseExistingWasiArtifactMetadata(
await readFileAsync(bindingPath, 'utf8'),
)
if (artifactMetadata?.exports === undefined) {
return undefined
}
return {
exports: artifactMetadata.exports,
bindingTypeDef: await readFileAsync(bindingTypeDefPath, 'utf8'),
}
}
private pickBinary() {
let set = false
if (this.options.watch) {
if (process.env.CI) {
debug.warn('Watch mode is not supported in CI environment')
} else {
debug('Use %i', 'cargo-watch')
tryInstallCargoBinary('cargo-watch', 'watch')
// yarn napi watch --target x86_64-unknown-linux-gnu
// ===>
// cargo watch [...] -- cargo build --target x86_64-unknown-linux-gnu
this.args.push(
'watch',
'--why',
'-i',
'*.{js,ts,node}',
'-w',
this.crateDir,
'--',
'cargo',
'build',
)
set = true
}
}
if (this.options.crossCompile) {
if (this.target.platform === 'win32') {
if (process.platform === 'win32') {
debug.warn(
'You are trying to cross compile to win32 platform on win32 platform which is unnecessary.',
)
} else {
// use cargo-xwin to cross compile to win32 platform
debug('Use %i', 'cargo-xwin')
tryInstallCargoBinary('cargo-xwin', 'xwin')
this.args.push('xwin', 'build')
if (this.target.arch === 'ia32') {
this.envs.XWIN_ARCH = 'x86'
}
set = true
}
} else {
// use cargo-zigbuild to cross compile to other platforms
debug('Use %i', 'cargo-zigbuild')
tryInstallCargoBinary('cargo-zigbuild', 'zigbuild')
this.args.push('zigbuild')
set = true
}
}
if (!set) {
this.args.push('build')
}
return this
}
private setPackage() {
const args = []
if (this.options.package) {
args.push('--package', this.options.package)
}
if (this.binName) {
args.push('--bin', this.binName)
}
if (args.length) {
debug('Set package flags: ')
debug(' %O', args)
this.args.push(...args)
}
return this
}
private setTarget() {
debug('Set compiling target to: ')
debug(' %i', this.target.triple)
this.args.push('--target', this.target.triple)
return this
}
private async setEnvs() {
// RUSTFLAGS
let rustflags =
process.env.RUSTFLAGS ?? process.env.CARGO_BUILD_RUSTFLAGS ?? ''
if (
this.target.abi?.includes('musl') &&
!rustflags.includes('target-feature=-crt-static')
) {
rustflags += ' -C target-feature=-crt-static'
}
if (this.options.strip && !rustflags.includes('link-arg=-s')) {
rustflags += ' -C link-arg=-s'
}
// TYPE DEF
if (this.enableTypeDef) {
this.envs.NAPI_TYPE_DEF_TMP_FOLDER =
await this.generateIntermediateTypeDefFolder(rustflags)
this.setForceBuildEnvs(this.envs.NAPI_TYPE_DEF_TMP_FOLDER)
}
if (rustflags.length) {
this.envs.RUSTFLAGS = rustflags
}
// END RUSTFLAGS
// LINKER
const linker = this.options.crossCompile
? void 0
: getTargetLinker(this.target.triple)
// TODO:
// directly set CARGO_TARGET_<target>_LINKER will cover .cargo/config.toml
// will detect by cargo config when it becomes stable
// see: https://github.com/rust-lang/cargo/issues/9301
const linkerEnv = `CARGO_TARGET_${targetToEnvVar(
this.target.triple,
)}_LINKER`
if (linker && !process.env[linkerEnv] && !this.envs[linkerEnv]) {
this.envs[linkerEnv] = linker
}
if (this.target.platform === 'android') {
this.setAndroidEnv()
}
if (this.target.platform === 'wasi') {
this.setWasiEnv()
}
if (this.target.platform === 'openharmony') {
this.setOpenHarmonyEnv()
}
debug('Set envs: ')
Object.entries(this.envs).forEach(([k, v]) => {
debug(' %i', `${k}=${v}`)
})
return this
}
private setForceBuildEnvs(typeDefTmpFolder: string) {
// dynamically check all napi-rs deps and set `NAPI_FORCE_BUILD_{uppercase(snake_case(name))} = timestamp`
getNapiDeriveDependentCrates(this.metadata).forEach((crate) => {
if (!existsSync(join(typeDefTmpFolder, crate.name))) {
this.envs[
`NAPI_FORCE_BUILD_${crate.name.replace(/-/g, '_').toUpperCase()}`
] = Date.now().toString()
}
})
}
private setAndroidEnv() {
// Native Android hosts and `cross` provide their own Android toolchains.
if (process.platform === 'android' || this.options.useCross) {
return
}
const { ANDROID_NDK_LATEST_HOME } = process.env
if (!ANDROID_NDK_LATEST_HOME) {
throw new Error(
`${colors.red(
'ANDROID_NDK_LATEST_HOME',
)} environment variable is required when building an Android target from a non-Android host`,
)
}
const targetArch = this.target.arch === 'arm' ? 'armv7a' : 'aarch64'
const targetPlatform =
this.target.arch === 'arm' ? 'androideabi24' : 'android24'
const hostPlatform =
process.platform === 'darwin'
? 'darwin'
: process.platform === 'win32'
? 'windows'
: 'linux'
Object.assign(this.envs, {
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-android24-clang`,
CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-androideabi24-clang`,
TARGET_CC: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang`,
TARGET_CXX: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang++`,
TARGET_AR: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/llvm-ar`,
TARGET_RANLIB: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/llvm-ranlib`,
ANDROID_NDK: ANDROID_NDK_LATEST_HOME,
PATH: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin${process.platform === 'win32' ? ';' : ':'}${process.env.PATH}`,
})
}
private setWasiEnv() {
const hasThreads = wasiTargetHasThreads(this.target)
const wasiTarget = hasThreads ? 'wasm32-wasip1-threads' : 'wasm32-wasip1'
const emnapi = join(require.resolve('emnapi'), '..', 'lib', wasiTarget)
const emnapiVersion = require('emnapi/package.json').version
// Keep this in sync with `emnapi_link_library` in `crates/build/src/wasi.rs`.
const emnapiArchive = join(
emnapi,
hasThreads ? 'libemnapi-napi-rs-mt.a' : 'libemnapi-basic-napi-rs.a',
)
if (!existsSync(emnapiArchive)) {
throw new Error(
`emnapi@${emnapiVersion} is missing the ${wasiTarget} archive required by napi-rs at ${emnapiArchive}. Install emnapi v2 with support for this target.`,
)
}
this.envs.EMNAPI_LINK_DIR = emnapi
const projectRequire = createRequire(
resolve(this.options.cwd, 'package.json'),
)
const projectPackageVersion = (name: string): string => {
try {
return projectRequire(`${name}/package.json`).version
} catch {
// emnapi v1 packages do not list `./package.json` in their export
// maps, so fall back to the module's own `version` export to keep the
// descriptive version-mismatch error below reachable for stale
// installs instead of a bare ERR_PACKAGE_PATH_NOT_EXPORTED.
return projectRequire(name).version
}
}
const emnapiCoreVersion = projectPackageVersion('@emnapi/core')
const emnapiRuntimeVersion = projectPackageVersion('@emnapi/runtime')
if (
emnapiVersion !== emnapiCoreVersion ||
emnapiVersion !== emnapiRuntimeVersion
) {
throw new Error(
`emnapi version mismatch: emnapi@${emnapiVersion}, @emnapi/core@${emnapiCoreVersion}, @emnapi/runtime@${emnapiRuntimeVersion}. Please ensure all emnapi packages are the same version.`,
)
}
const { WASI_SDK_PATH } = process.env
if (WASI_SDK_PATH && existsSync(WASI_SDK_PATH)) {
this.envs.CARGO_TARGET_WASM32_WASI_PREVIEW1_THREADS_LINKER = join(
WASI_SDK_PATH,
'bin',
'wasm-ld',
)
this.envs.CARGO_TARGET_WASM32_WASIP1_LINKER = join(
WASI_SDK_PATH,
'bin',
'wasm-ld',
)
this.envs.CARGO_TARGET_WASM32_WASIP1_THREADS_LINKER = join(
WASI_SDK_PATH,
'bin',
'wasm-ld',
)
this.envs.CARGO_TARGET_WASM32_WASIP2_LINKER = join(
WASI_SDK_PATH,
'bin',
'wasm-ld',
)
this.setEnvIfNotExists('TARGET_CC', join(WASI_SDK_PATH, 'bin', 'clang'))
this.setEnvIfNotExists(
'TARGET_CXX',
join(WASI_SDK_PATH, 'bin', 'clang++'),
)
this.setEnvIfNotExists('TARGET_AR', join(WASI_SDK_PATH, 'bin', 'ar'))
this.setEnvIfNotExists(
'TARGET_RANLIB',
join(WASI_SDK_PATH, 'bin', 'ranlib'),
)
const { compileFlags, linkerFlags } = createWasiCompilerFlags(
WASI_SDK_PATH,
wasiTarget,
hasThreads,
process.env.CC_SHELL_ESCAPED_FLAGS
? envBoolean(process.env.CC_SHELL_ESCAPED_FLAGS)
: true,
)
this.setEnvIfNotExists('CC_SHELL_ESCAPED_FLAGS', '1')
this.setEnvIfNotExists('TARGET_CFLAGS', compileFlags)
this.setEnvIfNotExists('TARGET_CXXFLAGS', compileFlags)
this.setEnvIfNotExists(`TARGET_LDFLAGS`, linkerFlags)
}
}
private setOpenHarmonyEnv() {
const { OHOS_SDK_PATH, OHOS_SDK_NATIVE } = process.env
const ndkPath = OHOS_SDK_PATH ? `${OHOS_SDK_PATH}/native` : OHOS_SDK_NATIVE
// @ts-expect-error
if (!ndkPath && process.platform !== 'openharmony') {
debug.warn(
`${colors.red('OHOS_SDK_PATH')} or ${colors.red('OHOS_SDK_NATIVE')} environment variable is missing`,
)
return
}
const linkerName = `CARGO_TARGET_${this.target.triple.toUpperCase().replace(/-/g, '_')}_LINKER`
const ranPath = `${ndkPath}/llvm/bin/llvm-ranlib`
const arPath = `${ndkPath}/llvm/bin/llvm-ar`
const ccPath = `${ndkPath}/llvm/bin/${this.target.triple}-clang`
const cxxPath = `${ndkPath}/llvm/bin/${this.target.triple}-clang++`
const asPath = `${ndkPath}/llvm/bin/llvm-as`
const ldPath = `${ndkPath}/llvm/bin/ld.lld`
const stripPath = `${ndkPath}/llvm/bin/llvm-strip`
const objDumpPath = `${ndkPath}/llvm/bin/llvm-ob