feat(cli): generate js binding to avoid dynamic require logic

This commit is contained in:
LongYinan 2021-11-09 20:10:08 +08:00
parent b7a98d2c7a
commit 179f20a7c5
No known key found for this signature in database
GPG key ID: C3666B7FC82ADAD7
9 changed files with 325 additions and 70 deletions

View file

@ -1,12 +1,13 @@
import { execSync } from 'child_process' import { execSync } from 'child_process'
import { join, parse, sep } from 'path' import { join, parse, sep } from 'path'
import chalk from 'chalk' import { Instance } from 'chalk'
import { Command, Option } from 'clipanion' import { Command, Option } from 'clipanion'
import toml from 'toml' import toml from 'toml'
import { getNapiConfig } from './consts' import { getNapiConfig } from './consts'
import { debugFactory } from './debug' import { debugFactory } from './debug'
import { createJsBinding } from './js-binding-template'
import { getDefaultTargetTriple, parseTriple } from './parse-triple' import { getDefaultTargetTriple, parseTriple } from './parse-triple'
import { import {
copyFileAsync, copyFileAsync,
@ -18,6 +19,7 @@ import {
} from './utils' } from './utils'
const debug = debugFactory('build') const debug = debugFactory('build')
const chalk = new Instance({ level: 1 })
export class BuildCommand extends Command { export class BuildCommand extends Command {
static usage = Command.Usage({ static usage = Command.Usage({
@ -26,23 +28,57 @@ export class BuildCommand extends Command {
static paths = [['build']] static paths = [['build']]
appendPlatformToFilename = Option.Boolean(`--platform`, false) appendPlatformToFilename = Option.Boolean(`--platform`, false, {
description: `Add platform triple to the .node file. ${chalk.green(
'[name].linux-x64-gnu.node',
)} for example`,
})
isRelease = Option.Boolean(`--release`, false) isRelease = Option.Boolean(`--release`, false, {
description: `Bypass to ${chalk.green('cargo --release')}`,
})
configFileName?: string = Option.String('--config,-c') configFileName?: string = Option.String('--config,-c', {
description: `napi config path, only JSON format accepted. Default to ${chalk.underline(
chalk.green('package.json'),
)}`,
})
cargoName?: string = Option.String('--cargo-name') cargoName?: string = Option.String('--cargo-name', {
description: `Override the ${chalk.green(
'name',
)} field in ${chalk.underline(chalk.yellowBright('Cargo.toml'))}`,
})
targetTripleDir = Option.String('--target', process.env.RUST_TARGET ?? '') targetTripleDir = Option.String('--target', process.env.RUST_TARGET ?? '', {
description: `Bypass to ${chalk.green('cargo --target')}`,
})
features?: string = Option.String('--features') features?: string = Option.String('--features', {
description: `Bypass to ${chalk.green('cargo --features')}`,
})
dts?: string = Option.String('--dts') dts?: string = Option.String('--dts', 'index.d.ts', {
description: `The filename and path of ${chalk.green(
'.d.ts',
)} file, relative to cwd`,
})
cargoFlags = Option.String('--cargo-flags', '') cargoFlags = Option.String('--cargo-flags', '', {
description: `All the others flag passed to ${chalk.yellow('cargo')}`,
})
cargoCwd?: string = Option.String('--cargo-cwd') jsBinding = Option.String('--js', 'index.js', {
description: `Path to the JS binding file, pass ${chalk.underline(
chalk.yellow('false'),
)} to disable it`,
})
cargoCwd?: string = Option.String('--cargo-cwd', {
description: `The cwd of ${chalk.underline(
chalk.yellow('Cargo.toml'),
)} file`,
})
destDir = Option.String({ destDir = Option.String({
required: false, required: false,
@ -84,7 +120,7 @@ export class BuildCommand extends Command {
stdio: 'inherit', stdio: 'inherit',
cwd, cwd,
}) })
const { binaryName } = getNapiConfig(this.configFileName) const { binaryName, packageName } = getNapiConfig(this.configFileName)
let dylibName = this.cargoName let dylibName = this.cargoName
if (!dylibName) { if (!dylibName) {
let tomlContentString: string let tomlContentString: string
@ -198,10 +234,18 @@ export class BuildCommand extends Command {
debug(`Write binary content to [${chalk.yellowBright(distModulePath)}]`) debug(`Write binary content to [${chalk.yellowBright(distModulePath)}]`)
await copyFileAsync(sourcePath, distModulePath) await copyFileAsync(sourcePath, distModulePath)
await processIntermediateTypeFile( const idents = await processIntermediateTypeFile(
intermediateTypeFile, intermediateTypeFile,
join(this.destDir ?? '.', this.dts ?? 'index.d.ts'), join(this.destDir ?? '.', this.dts ?? 'index.d.ts'),
) )
await writeJsBinding(
binaryName,
packageName,
this.jsBinding && this.jsBinding !== 'false'
? join(process.cwd(), this.jsBinding)
: null,
idents,
)
} }
} }
@ -224,10 +268,14 @@ interface TypeDef {
def: string def: string
} }
async function processIntermediateTypeFile(source: string, target: string) { async function processIntermediateTypeFile(
source: string,
target: string,
): Promise<string[]> {
const idents: string[] = []
if (!(await existsAsync(source))) { if (!(await existsAsync(source))) {
debug(`do not find tmp type file. skip type generation`) debug(`do not find tmp type file. skip type generation`)
return return idents
} }
const tmpFile = await readFileAsync(source, 'utf8') const tmpFile = await readFileAsync(source, 'utf8')
@ -244,6 +292,7 @@ async function processIntermediateTypeFile(source: string, target: string) {
switch (def.kind) { switch (def.kind) {
case 'struct': case 'struct':
idents.push(def.name)
classes.set(def.name, def.def) classes.set(def.name, def.def)
break break
case 'impl': case 'impl':
@ -253,6 +302,7 @@ async function processIntermediateTypeFile(source: string, target: string) {
dts += `interface ${def.name} {\n${indentLines(def.def, 2)}\n}\n` dts += `interface ${def.name} {\n${indentLines(def.def, 2)}\n}\n`
break break
default: default:
idents.push(def.name)
dts += def.def + '\n' dts += def.def + '\n'
} }
}) })
@ -271,6 +321,7 @@ async function processIntermediateTypeFile(source: string, target: string) {
await unlinkAsync(source) await unlinkAsync(source)
await writeFileAsync(target, dts, 'utf8') await writeFileAsync(target, dts, 'utf8')
return idents
} }
function indentLines(input: string, spaces: number) { function indentLines(input: string, spaces: number) {
@ -279,3 +330,23 @@ function indentLines(input: string, spaces: number) {
.map((line) => ''.padEnd(spaces, ' ') + line.trim()) .map((line) => ''.padEnd(spaces, ' ') + line.trim())
.join('\n') .join('\n')
} }
async function writeJsBinding(
localName: string,
packageName: string,
distFileName: string | null,
idents: string[],
) {
if (distFileName) {
const template = createJsBinding(localName, packageName)
const declareCodes = `const { ${idents.join(', ')} } = nativeBinding\n`
const exportsCode = idents.reduce((acc, cur) => {
return `${acc}\nmodule.exports.${cur} = ${cur}`
}, '')
await writeFileAsync(
distFileName,
template + declareCodes + exportsCode,
'utf8',
)
}
}

View file

@ -0,0 +1,216 @@
export const createJsBinding = (
localName: string,
pkgName: string,
) => `const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
let nativeBinding = null
let localFileExisted = false
let isMusl = false
let loadError = null
switch (platform) {
case 'android':
if (arch !== 'arm64') {
throw new Error(\`Unsupported architecture on Android \${arch}\`)
}
localFileExisted = existsSync(join(__dirname, '${localName}.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.android-arm64.node')
} else {
nativeBinding = require('${pkgName}-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, '${localName}.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.win32-x64-msvc.node')
} else {
nativeBinding = require('${pkgName}-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, '${localName}.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.win32-ia32-msvc.node')
} else {
nativeBinding = require('${pkgName}-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, '${localName}.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.win32-arm64-msvc.node')
} else {
nativeBinding = require('${pkgName}-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(\`Unsupported architecture on Windows: \${arch}\`)
}
break
case 'darwin':
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, '${localName}.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.darwin-x64.node')
} else {
nativeBinding = require('${pkgName}-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, '${localName}.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.darwin-arm64.node')
} else {
nativeBinding = require('${pkgName}-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(\`Unsupported architecture on macOS: \${arch}\`)
}
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(\`Unsupported architecture on FreeBSD: \${arch}\`)
}
localFileExisted = existsSync(join(__dirname, '${localName}.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.freebsd-x64.node')
} else {
nativeBinding = require('${pkgName}-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'linux':
switch (arch) {
case 'x64':
isMusl = readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
if (isMusl) {
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-x64-musl.node')
} else {
nativeBinding = require('${pkgName}-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-x64-gnu.node')
} else {
nativeBinding = require('${pkgName}-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
isMusl = readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
if (isMusl) {
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-arm64-musl.node')
} else {
nativeBinding = require('${pkgName}-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-arm64-gnu.node')
} else {
nativeBinding = require('${pkgName}-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('${pkgName}-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(\`Unsupported architecture on Linux: \${arch}\`)
}
break
default:
throw new Error(\`Unsupported OS: \${platform}, architecture: \${arch}\`)
}
if (!nativeBinding) {
if (loadError) {
throw loadError
}
throw new Error(\`Failed to load native binding\`)
}
`

View file

@ -38,7 +38,7 @@ jobs:
target: 'x86_64-pc-windows-msvc' target: 'x86_64-pc-windows-msvc'
- host: windows-latest - host: windows-latest
build: | build: |
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=32; export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=64;
export CARGO_PROFILE_RELEASE_LTO=false export CARGO_PROFILE_RELEASE_LTO=false
yarn build --target i686-pc-windows-msvc yarn build --target i686-pc-windows-msvc
yarn test yarn test

View file

@ -12,7 +12,6 @@ import { DefaultPlatforms } from '../parse-triple'
import { createCargoContent } from './cargo' import { createCargoContent } from './cargo'
import { createCargoConfig } from './cargo-config' import { createCargoConfig } from './cargo-config'
import { createGithubActionsCIYml } from './ci-yml' import { createGithubActionsCIYml } from './ci-yml'
import { createIndexJs } from './indexjs'
import { LibRs } from './lib-rs' import { LibRs } from './lib-rs'
import { NPMIgnoreFiles } from './npmignore' import { NPMIgnoreFiles } from './npmignore'
import { createPackageJson } from './package' import { createPackageJson } from './package'
@ -125,7 +124,6 @@ export class NewProjectCommand extends Command {
this.writeFile('Cargo.toml', createCargoContent(this.name!)) this.writeFile('Cargo.toml', createCargoContent(this.name!))
this.writeFile('.npmignore', NPMIgnoreFiles) this.writeFile('.npmignore', NPMIgnoreFiles)
this.writeFile('build.rs', BUILD_RS) this.writeFile('build.rs', BUILD_RS)
this.writeFile('index.js', createIndexJs(this.name!, binaryName))
this.writeFile( this.writeFile(
'package.json', 'package.json',
JSON.stringify( JSON.stringify(

View file

@ -1,15 +0,0 @@
export const createIndexJs = (
pkgName: string,
name: string,
) => `const { loadBinding } = require('@node-rs/helper')
/**
* __dirname means load native addon from current dir
* '${name}' is the name of native addon
* the second arguments was decided by \`napi.name\` field in \`package.json\`
* the third arguments was decided by \`name\` field in \`package.json\`
* \`loadBinding\` helper will load \`${name}.[PLATFORM].node\` from \`__dirname\` first
* If failed to load addon, it will fallback to load from \`${pkgName}-[PLATFORM]\`
*/
module.exports = loadBinding(__dirname, '${name}', '${pkgName}')
`

View file

@ -2,13 +2,13 @@
"name": "test-module", "name": "test-module",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"build": "node ../../cli/scripts/index.js build --features \"latest\"", "build": "node ../../cli/scripts/index.js build --js false --features \"latest\"",
"build-napi3": "node ../../cli/scripts/index.js build --features \"napi3\"", "build-napi3": "node ../../cli/scripts/index.js build --js false --features \"napi3\"",
"build-aarch64": "node ../../cli/scripts/index.js build --features \"latest\" --target aarch64-unknown-linux-gnu", "build-aarch64": "node ../../cli/scripts/index.js build --js false --features \"latest\" --target aarch64-unknown-linux-gnu",
"build-armv7": "node ../../cli/scripts/index.js build --features \"latest\" --target armv7-unknown-linux-gnueabihf", "build-armv7": "node ../../cli/scripts/index.js build --js false --features \"latest\" --target armv7-unknown-linux-gnueabihf",
"build-i686": "node ../../cli/scripts/index.js build --features \"latest\" --target i686-pc-windows-msvc", "build-i686": "node ../../cli/scripts/index.js build --js false --features \"latest\" --target i686-pc-windows-msvc",
"build-i686-release": "node ../../cli/scripts/index.js build --release --features \"latest\" --target i686-pc-windows-msvc", "build-i686-release": "node ../../cli/scripts/index.js build --js false --release --features \"latest\" --target i686-pc-windows-msvc",
"build-release": "node ../../cli/scripts/index.js build --features \"latest\" --release", "build-release": "node ../../cli/scripts/index.js build --js false --features \"latest\" --release",
"test": "node ./index.js" "test": "node ./index.js"
} }
} }

View file

@ -4,9 +4,7 @@ export function sumNums(nums: Array<number>): number
export function readFileAsync(path: string): Promise<Buffer> export function readFileAsync(path: string): Promise<Buffer>
export function asyncMultiTwo(arg: number): Promise<number> export function asyncMultiTwo(arg: number): Promise<number>
export function getCwd(callback: (arg0: string) => void): void export function getCwd(callback: (arg0: string) => void): void
export function readFile( export function readFile(callback: (arg0: Error | undefined, arg1: string | null) => void): void
callback: (arg0: Error | undefined, arg1: string | null) => void,
): void
export function eitherStringOrNumber(input: string | number): number export function eitherStringOrNumber(input: string | number): number
export function returnEither(input: number): string | number export function returnEither(input: number): string | number
export function either3(input: string | number | boolean): number export function either3(input: string | number | boolean): number
@ -14,21 +12,8 @@ interface Obj {
v: string | number v: string | number
} }
export function either4(input: string | number | boolean | Obj): number export function either4(input: string | number | boolean | Obj): number
export enum Kind { export enum Kind { Dog = 0, Cat = 1, Duck = 2 }
Dog = 0, export enum CustomNumEnum { One = 1, Two = 2, Three = 3, Four = 4, Six = 6, Eight = 8, Nine = 9, Ten = 10 }
Cat = 1,
Duck = 2,
}
export enum CustomNumEnum {
One = 1,
Two = 2,
Three = 3,
Four = 4,
Six = 6,
Eight = 8,
Nine = 9,
Ten = 10,
}
export function enumToI32(e: CustomNumEnum): number export function enumToI32(e: CustomNumEnum): number
export function throwError(): void export function throwError(): void
export function mapOption(val: number | null): number | null export function mapOption(val: number | null): number | null
@ -49,11 +34,7 @@ export function concatStr(mutS: string): string
export function concatUtf16(s: string): string export function concatUtf16(s: string): string
export function concatLatin1(s: string): string export function concatLatin1(s: string): string
export function withoutAbortController(a: number, b: number): Promise<number> export function withoutAbortController(a: number, b: number): Promise<number>
export function withAbortController( export function withAbortController(a: number, b: number, signal: AbortSignal): Promise<number>
a: number,
b: number,
signal: AbortSignal,
): Promise<number>
export function getBuffer(): Buffer export function getBuffer(): Buffer
export class Animal { export class Animal {
readonly kind: Kind readonly kind: Kind
@ -65,10 +46,14 @@ export class Animal {
static getDogKind(): Kind static getDogKind(): Kind
} }
export class Blake2BHasher { export class Blake2BHasher {
static withKey(key: Blake2bKey): Blake2BHasher static withKey(key: Blake2bKey): Blake2BHasher
} }
export class Blake2BKey {} export class Blake2BKey {
}
export class Context { export class Context {
constructor() constructor()
static withData(data: string): Context static withData(data: string): Context
method(): string method(): string

View file

@ -4,11 +4,11 @@
"main": "./index.node", "main": "./index.node",
"types": "./index.d.ts", "types": "./index.d.ts",
"scripts": { "scripts": {
"build": "node ../../cli/scripts/index.js build", "build": "node ../../cli/scripts/index.js build --js false",
"build-aarch64": "node ../../cli/scripts/index.js build --target aarch64-unknown-linux-gnu", "build-aarch64": "node ../../cli/scripts/index.js build --js false --target aarch64-unknown-linux-gnu",
"build-armv7": "node ../../cli/scripts/index.js build --target armv7-unknown-linux-gnueabihf", "build-armv7": "node ../../cli/scripts/index.js build --js false --target armv7-unknown-linux-gnueabihf",
"build-i686": "node ../../cli/scripts/index.js build --target i686-pc-windows-msvc", "build-i686": "node ../../cli/scripts/index.js build --js false --target i686-pc-windows-msvc",
"build-i686-release": "node ../../cli/scripts/index.js build --release --target i686-pc-windows-msvc", "build-i686-release": "node ../../cli/scripts/index.js build --js false --release --target i686-pc-windows-msvc",
"build-release": "node ../../cli/scripts/index.js build --release" "build-release": "node ../../cli/scripts/index.js build --js false --release"
} }
} }

View file

@ -6,5 +6,5 @@
"rootDir": "__test__", "rootDir": "__test__",
"target": "ES2015" "target": "ES2015"
}, },
"exclude": ["dist"] "exclude": ["dist", "index.d.ts"]
} }