2020-10-15 21:16:52 +09:00
|
|
|
import { execSync } from 'child_process'
|
2020-09-04 17:22:15 +09:00
|
|
|
import { join, parse, sep } from 'path'
|
2020-07-27 00:53:09 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
import { Instance } from 'chalk'
|
2021-08-07 01:22:53 +09:00
|
|
|
import { Command, Option } from 'clipanion'
|
2020-07-27 00:53:09 +09:00
|
|
|
import toml from 'toml'
|
|
|
|
|
2020-09-04 17:22:15 +09:00
|
|
|
import { getNapiConfig } from './consts'
|
|
|
|
import { debugFactory } from './debug'
|
2021-11-09 21:10:08 +09:00
|
|
|
import { createJsBinding } from './js-binding-template'
|
2020-10-15 21:16:52 +09:00
|
|
|
import { getDefaultTargetTriple, parseTriple } from './parse-triple'
|
2021-08-09 15:07:34 +09:00
|
|
|
import {
|
|
|
|
copyFileAsync,
|
|
|
|
existsAsync,
|
|
|
|
mkdirAsync,
|
|
|
|
readFileAsync,
|
|
|
|
unlinkAsync,
|
2021-09-23 02:29:09 +09:00
|
|
|
writeFileAsync,
|
2021-08-09 15:07:34 +09:00
|
|
|
} from './utils'
|
2020-09-04 17:22:15 +09:00
|
|
|
|
|
|
|
const debug = debugFactory('build')
|
2021-11-09 21:10:08 +09:00
|
|
|
const chalk = new Instance({ level: 1 })
|
2020-07-27 00:53:09 +09:00
|
|
|
|
|
|
|
export class BuildCommand extends Command {
|
|
|
|
static usage = Command.Usage({
|
2020-12-23 23:43:43 +09:00
|
|
|
description: 'Build and copy native module into specified dir',
|
2020-07-27 00:53:09 +09:00
|
|
|
})
|
|
|
|
|
2021-08-07 01:22:53 +09:00
|
|
|
static paths = [['build']]
|
2020-07-27 00:53:09 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
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, {
|
|
|
|
description: `Bypass to ${chalk.green('cargo --release')}`,
|
|
|
|
})
|
2020-07-27 00:53:09 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
configFileName?: string = Option.String('--config,-c', {
|
|
|
|
description: `napi config path, only JSON format accepted. Default to ${chalk.underline(
|
|
|
|
chalk.green('package.json'),
|
|
|
|
)}`,
|
|
|
|
})
|
2020-09-04 17:22:15 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
cargoName?: string = Option.String('--cargo-name', {
|
|
|
|
description: `Override the ${chalk.green(
|
|
|
|
'name',
|
|
|
|
)} field in ${chalk.underline(chalk.yellowBright('Cargo.toml'))}`,
|
|
|
|
})
|
2020-09-05 02:20:30 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
targetTripleDir = Option.String('--target', process.env.RUST_TARGET ?? '', {
|
|
|
|
description: `Bypass to ${chalk.green('cargo --target')}`,
|
|
|
|
})
|
2020-10-15 17:14:11 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
features?: string = Option.String('--features', {
|
|
|
|
description: `Bypass to ${chalk.green('cargo --features')}`,
|
|
|
|
})
|
2020-10-15 21:16:52 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
dts?: string = Option.String('--dts', 'index.d.ts', {
|
|
|
|
description: `The filename and path of ${chalk.green(
|
|
|
|
'.d.ts',
|
|
|
|
)} file, relative to cwd`,
|
|
|
|
})
|
2020-10-15 21:16:52 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
cargoFlags = Option.String('--cargo-flags', '', {
|
|
|
|
description: `All the others flag passed to ${chalk.yellow('cargo')}`,
|
|
|
|
})
|
2021-10-01 15:41:52 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
jsBinding = Option.String('--js', 'index.js', {
|
|
|
|
description: `Path to the JS binding file, pass ${chalk.underline(
|
|
|
|
chalk.yellow('false'),
|
|
|
|
)} to disable it`,
|
|
|
|
})
|
2020-12-22 16:02:57 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
cargoCwd?: string = Option.String('--cargo-cwd', {
|
|
|
|
description: `The cwd of ${chalk.underline(
|
|
|
|
chalk.yellow('Cargo.toml'),
|
|
|
|
)} file`,
|
|
|
|
})
|
2021-08-07 01:22:53 +09:00
|
|
|
|
|
|
|
destDir = Option.String({
|
2020-09-04 17:22:15 +09:00
|
|
|
required: false,
|
|
|
|
})
|
2020-07-27 00:53:09 +09:00
|
|
|
|
|
|
|
async execute() {
|
2020-12-22 16:02:57 +09:00
|
|
|
const cwd = this.cargoCwd
|
|
|
|
? join(process.cwd(), this.cargoCwd)
|
|
|
|
: process.cwd()
|
2020-10-15 21:16:52 +09:00
|
|
|
const releaseFlag = this.isRelease ? `--release` : ''
|
2021-09-23 02:29:09 +09:00
|
|
|
const targetFlag = this.targetTripleDir
|
2020-10-15 21:16:52 +09:00
|
|
|
? `--target ${this.targetTripleDir}`
|
|
|
|
: ''
|
|
|
|
const featuresFlag = this.features ? `--features ${this.features}` : ''
|
|
|
|
const triple = this.targetTripleDir
|
|
|
|
? parseTriple(this.targetTripleDir)
|
|
|
|
: getDefaultTargetTriple(
|
|
|
|
execSync('rustup show active-toolchain', {
|
|
|
|
env: process.env,
|
|
|
|
}).toString('utf8'),
|
|
|
|
)
|
|
|
|
debug(`Current triple is: ${chalk.green(triple.raw)}`)
|
|
|
|
const externalFlags = [
|
|
|
|
releaseFlag,
|
2021-09-23 02:29:09 +09:00
|
|
|
targetFlag,
|
2020-10-15 21:16:52 +09:00
|
|
|
featuresFlag,
|
|
|
|
this.cargoFlags,
|
|
|
|
]
|
|
|
|
.filter((flag) => Boolean(flag))
|
|
|
|
.join(' ')
|
|
|
|
const cargoCommand = `cargo build ${externalFlags}`
|
2021-09-23 02:29:09 +09:00
|
|
|
const intermediateTypeFile = join(__dirname, `type_def.${Date.now()}.tmp`)
|
2020-10-15 21:16:52 +09:00
|
|
|
debug(`Run ${chalk.green(cargoCommand)}`)
|
|
|
|
execSync(cargoCommand, {
|
2021-09-23 02:29:09 +09:00
|
|
|
env: {
|
|
|
|
...process.env,
|
|
|
|
TYPE_DEF_TMP_PATH: intermediateTypeFile,
|
|
|
|
},
|
2020-10-15 21:16:52 +09:00
|
|
|
stdio: 'inherit',
|
2020-12-22 16:02:57 +09:00
|
|
|
cwd,
|
2020-10-15 21:16:52 +09:00
|
|
|
})
|
2021-11-09 21:10:08 +09:00
|
|
|
const { binaryName, packageName } = getNapiConfig(this.configFileName)
|
2020-09-05 02:20:30 +09:00
|
|
|
let dylibName = this.cargoName
|
|
|
|
if (!dylibName) {
|
|
|
|
let tomlContentString: string
|
|
|
|
let tomlContent: any
|
|
|
|
try {
|
|
|
|
debug('Start read toml')
|
|
|
|
tomlContentString = await readFileAsync(
|
2020-12-22 16:02:57 +09:00
|
|
|
join(cwd, 'Cargo.toml'),
|
2020-09-05 02:20:30 +09:00
|
|
|
'utf-8',
|
|
|
|
)
|
|
|
|
} catch {
|
2020-12-22 16:02:57 +09:00
|
|
|
throw new TypeError(`Could not find Cargo.toml in ${cwd}`)
|
2020-09-05 02:20:30 +09:00
|
|
|
}
|
2020-07-27 00:53:09 +09:00
|
|
|
|
2020-09-05 02:20:30 +09:00
|
|
|
try {
|
|
|
|
debug('Start parse toml')
|
|
|
|
tomlContent = toml.parse(tomlContentString)
|
|
|
|
} catch {
|
|
|
|
throw new TypeError('Could not parse the Cargo.toml')
|
|
|
|
}
|
2020-09-04 17:22:15 +09:00
|
|
|
|
2020-10-10 01:14:09 +09:00
|
|
|
if (tomlContent.package?.name) {
|
2020-09-05 02:20:30 +09:00
|
|
|
dylibName = tomlContent.package.name.replace(/-/g, '_')
|
|
|
|
} else {
|
|
|
|
throw new TypeError('No package.name field in Cargo.toml')
|
|
|
|
}
|
2020-12-22 16:14:29 +09:00
|
|
|
|
|
|
|
if (!tomlContent.lib?.['crate-type']?.includes?.('cdylib')) {
|
|
|
|
throw new TypeError(
|
|
|
|
`Missing ${chalk.green('create-type = ["cdylib"]')} in ${chalk.green(
|
|
|
|
'[lib]',
|
|
|
|
)}`,
|
|
|
|
)
|
|
|
|
}
|
2020-07-27 00:53:09 +09:00
|
|
|
}
|
|
|
|
|
2020-09-04 17:22:15 +09:00
|
|
|
debug(`Dylib name: ${chalk.greenBright(dylibName)}`)
|
|
|
|
|
2020-12-09 16:49:05 +09:00
|
|
|
const platform = triple.platform
|
2020-07-27 00:53:09 +09:00
|
|
|
let libExt
|
2020-09-04 17:22:15 +09:00
|
|
|
|
|
|
|
debug(`Platform: ${chalk.greenBright(platform)}`)
|
2020-07-27 00:53:09 +09:00
|
|
|
|
|
|
|
// Platform based massaging for build commands
|
|
|
|
switch (platform) {
|
|
|
|
case 'darwin':
|
|
|
|
libExt = '.dylib'
|
2020-09-04 17:22:15 +09:00
|
|
|
dylibName = `lib${dylibName}`
|
2020-07-27 00:53:09 +09:00
|
|
|
break
|
|
|
|
case 'win32':
|
|
|
|
libExt = '.dll'
|
|
|
|
break
|
|
|
|
case 'linux':
|
2020-10-14 01:08:22 +09:00
|
|
|
case 'freebsd':
|
2020-10-15 21:16:52 +09:00
|
|
|
case 'openbsd':
|
2020-12-09 16:49:05 +09:00
|
|
|
case 'android':
|
2020-10-15 21:16:52 +09:00
|
|
|
case 'sunos':
|
2020-09-04 17:22:15 +09:00
|
|
|
dylibName = `lib${dylibName}`
|
2020-07-27 00:53:09 +09:00
|
|
|
libExt = '.so'
|
|
|
|
break
|
|
|
|
default:
|
2020-09-04 17:22:15 +09:00
|
|
|
throw new TypeError(
|
2020-07-27 00:53:09 +09:00
|
|
|
'Operating system not currently supported or recognized by the build script',
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-08-07 01:22:53 +09:00
|
|
|
const targetRootDir = await findUp(cwd)
|
|
|
|
|
|
|
|
if (!targetRootDir) {
|
|
|
|
throw new TypeError('No target dir found')
|
|
|
|
}
|
|
|
|
|
2020-10-15 17:14:11 +09:00
|
|
|
const targetDir = join(
|
|
|
|
this.targetTripleDir,
|
|
|
|
this.isRelease ? 'release' : 'debug',
|
|
|
|
)
|
2020-07-27 00:53:09 +09:00
|
|
|
|
2020-08-21 22:51:14 +09:00
|
|
|
const platformName = this.appendPlatformToFilename
|
2020-10-15 21:16:52 +09:00
|
|
|
? `.${triple.platformArchABI}`
|
2020-07-27 00:53:09 +09:00
|
|
|
: ''
|
|
|
|
|
2020-10-15 21:16:52 +09:00
|
|
|
debug(`Platform name: ${platformName || chalk.green('[Empty]')}`)
|
2021-08-07 01:22:53 +09:00
|
|
|
const distFileName = `${binaryName}${platformName}.node`
|
|
|
|
|
2021-08-09 15:07:34 +09:00
|
|
|
const distModulePath = join(this.destDir ?? '.', distFileName)
|
2020-07-27 00:53:09 +09:00
|
|
|
|
2020-09-04 17:22:15 +09:00
|
|
|
const parsedDist = parse(distModulePath)
|
2020-07-27 00:53:09 +09:00
|
|
|
|
2021-08-09 15:07:34 +09:00
|
|
|
if (parsedDist.dir && !(await existsAsync(parsedDist.dir))) {
|
|
|
|
await mkdirAsync(parsedDist.dir, { recursive: true }).catch((e) => {
|
|
|
|
console.warn(
|
|
|
|
chalk.bgYellowBright(
|
|
|
|
`Create dir [${parsedDist.dir}] failed, reason: ${e.message}`,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
})
|
2020-07-27 00:53:09 +09:00
|
|
|
}
|
|
|
|
|
2021-08-07 01:22:53 +09:00
|
|
|
const sourcePath = join(
|
|
|
|
targetRootDir,
|
|
|
|
'target',
|
|
|
|
targetDir,
|
|
|
|
`${dylibName}${libExt}`,
|
|
|
|
)
|
2020-09-04 17:22:15 +09:00
|
|
|
|
2021-01-08 00:26:41 +09:00
|
|
|
if (await existsAsync(distModulePath)) {
|
2021-08-07 01:22:53 +09:00
|
|
|
debug(`remove old binary [${chalk.yellowBright(distModulePath)}]`)
|
2021-01-08 00:26:41 +09:00
|
|
|
await unlinkAsync(distModulePath)
|
|
|
|
}
|
2020-09-04 17:22:15 +09:00
|
|
|
|
|
|
|
debug(`Write binary content to [${chalk.yellowBright(distModulePath)}]`)
|
2021-01-08 00:26:41 +09:00
|
|
|
await copyFileAsync(sourcePath, distModulePath)
|
2021-09-23 02:29:09 +09:00
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
const idents = await processIntermediateTypeFile(
|
2021-09-23 02:29:09 +09:00
|
|
|
intermediateTypeFile,
|
2021-10-01 15:41:52 +09:00
|
|
|
join(this.destDir ?? '.', this.dts ?? 'index.d.ts'),
|
2021-09-23 02:29:09 +09:00
|
|
|
)
|
2021-11-09 21:10:08 +09:00
|
|
|
await writeJsBinding(
|
|
|
|
binaryName,
|
|
|
|
packageName,
|
|
|
|
this.jsBinding && this.jsBinding !== 'false'
|
|
|
|
? join(process.cwd(), this.jsBinding)
|
|
|
|
: null,
|
|
|
|
idents,
|
|
|
|
)
|
2020-07-27 00:53:09 +09:00
|
|
|
}
|
|
|
|
}
|
2020-09-04 17:22:15 +09:00
|
|
|
|
|
|
|
async function findUp(dir = process.cwd()): Promise<string | null> {
|
|
|
|
const dist = join(dir, 'target')
|
|
|
|
if (await existsAsync(dist)) {
|
|
|
|
return dir
|
|
|
|
}
|
|
|
|
const dirs = dir.split(sep)
|
|
|
|
if (dirs.length < 2) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
dirs.pop()
|
|
|
|
return findUp(dirs.join(sep))
|
|
|
|
}
|
2021-09-23 02:29:09 +09:00
|
|
|
|
|
|
|
interface TypeDef {
|
2021-09-28 01:01:19 +09:00
|
|
|
kind: 'fn' | 'struct' | 'impl' | 'enum' | 'interface'
|
2021-09-23 02:29:09 +09:00
|
|
|
name: string
|
|
|
|
def: string
|
|
|
|
}
|
|
|
|
|
2021-11-09 21:10:08 +09:00
|
|
|
async function processIntermediateTypeFile(
|
|
|
|
source: string,
|
|
|
|
target: string,
|
|
|
|
): Promise<string[]> {
|
|
|
|
const idents: string[] = []
|
2021-09-23 02:29:09 +09:00
|
|
|
if (!(await existsAsync(source))) {
|
|
|
|
debug(`do not find tmp type file. skip type generation`)
|
2021-11-09 21:10:08 +09:00
|
|
|
return idents
|
2021-09-23 02:29:09 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
const tmpFile = await readFileAsync(source, 'utf8')
|
|
|
|
const lines = tmpFile
|
|
|
|
.split('\n')
|
|
|
|
.map((line) => line.trim())
|
|
|
|
.filter(Boolean)
|
|
|
|
let dts = ''
|
|
|
|
const classes = new Map<string, string>()
|
|
|
|
const impls = new Map<string, string>()
|
|
|
|
|
|
|
|
lines.forEach((line) => {
|
|
|
|
const def = JSON.parse(line) as TypeDef
|
|
|
|
|
|
|
|
switch (def.kind) {
|
|
|
|
case 'struct':
|
2021-11-09 21:10:08 +09:00
|
|
|
idents.push(def.name)
|
2021-09-23 02:29:09 +09:00
|
|
|
classes.set(def.name, def.def)
|
|
|
|
break
|
|
|
|
case 'impl':
|
|
|
|
impls.set(def.name, def.def)
|
2021-09-28 01:01:19 +09:00
|
|
|
break
|
|
|
|
case 'interface':
|
|
|
|
dts += `interface ${def.name} {\n${indentLines(def.def, 2)}\n}\n`
|
|
|
|
break
|
|
|
|
default:
|
2021-11-09 21:10:08 +09:00
|
|
|
idents.push(def.name)
|
2021-09-28 01:01:19 +09:00
|
|
|
dts += def.def + '\n'
|
2021-09-23 02:29:09 +09:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-09-24 18:01:54 +09:00
|
|
|
for (const [name, classDef] of classes.entries()) {
|
|
|
|
const implDef = impls.get(name)
|
2021-09-23 02:29:09 +09:00
|
|
|
|
2021-09-28 01:01:19 +09:00
|
|
|
dts += `export class ${name} {\n${indentLines(classDef, 2)}`
|
2021-09-24 18:01:54 +09:00
|
|
|
|
|
|
|
if (implDef) {
|
2021-09-28 01:01:19 +09:00
|
|
|
dts += `\n${indentLines(implDef, 2)}`
|
2021-09-24 18:01:54 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
dts += '\n}\n'
|
2021-09-23 02:29:09 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
await unlinkAsync(source)
|
|
|
|
await writeFileAsync(target, dts, 'utf8')
|
2021-11-09 21:10:08 +09:00
|
|
|
return idents
|
2021-09-23 02:29:09 +09:00
|
|
|
}
|
2021-09-28 01:01:19 +09:00
|
|
|
|
|
|
|
function indentLines(input: string, spaces: number) {
|
|
|
|
return input
|
|
|
|
.split('\n')
|
|
|
|
.map((line) => ''.padEnd(spaces, ' ') + line.trim())
|
|
|
|
.join('\n')
|
|
|
|
}
|
2021-11-09 21:10:08 +09:00
|
|
|
|
|
|
|
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',
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|