@dry-software/cmake-js
Version:
CMake.js - a Node.js native addon build tool
363 lines (330 loc) • 10.1 kB
JavaScript
'use strict'
const which = require('which')
const fs = require('fs-extra')
const path = require('path')
const environment = require('./environment')
const Dist = require('./dist')
const CMLog = require('./cmLog')
const TargetOptions = require('./targetOptions')
const processHelpers = require('./processHelpers')
const locateNAN = require('./locateNAN')
const locateNodeApi = require('./locateNodeApi')
const npmConfigData = require('rc')('npm')
const Toolset = require('./toolset')
const headers = require('node-api-headers')
class CMake {
get path() {
return this.options.cmakePath || 'cmake'
}
get isAvailable() {
if (this._isAvailable === null) {
this._isAvailable = CMake.isAvailable(this.options)
}
return this._isAvailable
}
constructor(options) {
this.options = options || {}
this.log = new CMLog(this.options)
this.dist = new Dist(this.options)
this.projectRoot = path.resolve(this.options.directory || process.cwd())
this.workDir = path.resolve(this.options.out || path.join(this.projectRoot, 'build'))
this.config = this.options.config || (this.options.debug ? 'Debug' : 'Release')
this.buildDir = path.join(this.workDir, this.config)
this._isAvailable = null
this.targetOptions = new TargetOptions(this.options)
this.toolset = new Toolset(this.options)
this.cMakeOptions = this.options.cMakeOptions || {}
this.extraCMakeArgs = this.options.extraCMakeArgs || []
this.silent = !!options.silent
}
static isAvailable(options) {
options = options || {}
try {
if (options.cmakePath) {
const stat = fs.lstatSync(options.cmakePath)
return !stat.isDirectory()
} else {
which.sync('cmake')
return true
}
} catch (e) {
// Ignore
}
return false
}
static async getGenerators(options, log) {
const arch = ' [arch]'
options = options || {}
const gens = []
if (CMake.isAvailable(options)) {
// try parsing machine-readable capabilities (available since CMake 3.7)
try {
const stdout = await processHelpers.execFile([options.cmakePath || 'cmake', '-E', 'capabilities'])
const capabilities = JSON.parse(stdout)
return capabilities.generators.map((x) => x.name)
} catch (error) {
if (log) {
log.verbose('TOOL', 'Failed to query CMake capabilities (CMake is probably older than 3.7)')
}
}
// fall back to parsing help text
const stdout = await processHelpers.execFile([options.cmakePath || 'cmake', '--help'])
const hasCr = stdout.includes('\r\n')
const output = hasCr ? stdout.split('\r\n') : stdout.split('\n')
let on = false
output.forEach(function (line, i) {
if (on) {
const parts = line.split('=')
if (
(parts.length === 2 && parts[0].trim()) ||
(parts.length === 1 && i !== output.length - 1 && output[i + 1].trim()[0] === '=')
) {
let gen = parts[0].trim()
if (gen.endsWith(arch)) {
gen = gen.substr(0, gen.length - arch.length)
}
gens.push(gen)
}
}
if (line.trim() === 'Generators') {
on = true
}
})
} else {
throw new Error('CMake is not installed. Install CMake.')
}
return gens
}
verifyIfAvailable() {
if (!this.isAvailable) {
throw new Error(
"CMake executable is not found. Please use your system's package manager to install it, or you can get installers from there: http://cmake.org.",
)
}
}
async getConfigureCommand() {
// Create command:
let command = [this.path, this.projectRoot, '--no-warn-unused-cli']
const D = []
// CMake.js watermark
D.push({ CMAKE_JS_VERSION: environment.cmakeJsVersion })
// Build configuration:
D.push({ CMAKE_BUILD_TYPE: this.config })
if (environment.isWin) {
D.push({ CMAKE_RUNTIME_OUTPUT_DIRECTORY: this.workDir })
} else if (this.workDir.endsWith(this.config)) {
D.push({ CMAKE_LIBRARY_OUTPUT_DIRECTORY: this.workDir })
} else {
D.push({ CMAKE_LIBRARY_OUTPUT_DIRECTORY: this.buildDir })
}
// In some configurations MD builds will crash upon attempting to free memory.
// This tries to encourage MT builds which are larger but less likely to have this crash.
D.push({ CMAKE_MSVC_RUNTIME_LIBRARY: 'MultiThreaded$<$<CONFIG:Debug>:Debug>' })
// Includes:
const includesString = await this.getCmakeJsIncludeString()
D.push({ CMAKE_JS_INC: includesString })
// Sources:
const srcsString = this.getCmakeJsSrcString()
D.push({ CMAKE_JS_SRC: srcsString })
// Runtime:
D.push({ NODE_RUNTIME: this.targetOptions.runtime })
D.push({ NODE_RUNTIMEVERSION: this.targetOptions.runtimeVersion })
D.push({ NODE_ARCH: this.targetOptions.arch })
if (environment.isOSX) {
if (this.targetOptions.arch) {
let xcodeArch = this.targetOptions.arch
if (xcodeArch === 'x64') xcodeArch = 'x86_64'
D.push({ CMAKE_OSX_ARCHITECTURES: xcodeArch })
}
}
// Custom options
for (const [key, value] of Object.entries(this.cMakeOptions)) {
D.push({ [key]: value })
}
// Toolset:
await this.toolset.initialize(false)
const libsString = this.getCmakeJsLibString()
D.push({ CMAKE_JS_LIB: libsString })
if (environment.isWin) {
const nodeLibDefPath = this.getNodeLibDefPath()
if (nodeLibDefPath) {
const nodeLibPath = path.join(this.workDir, 'node.lib')
D.push({ CMAKE_JS_NODELIB_DEF: nodeLibDefPath })
D.push({ CMAKE_JS_NODELIB_TARGET: nodeLibPath })
}
}
if (this.toolset.generator) {
command.push('-G', this.toolset.generator)
}
if (this.toolset.platform) {
command.push('-A', this.toolset.platform)
}
if (this.toolset.toolset) {
command.push('-T', this.toolset.toolset)
}
if (this.toolset.cppCompilerPath) {
D.push({ CMAKE_CXX_COMPILER: this.toolset.cppCompilerPath })
}
if (this.toolset.cCompilerPath) {
D.push({ CMAKE_C_COMPILER: this.toolset.cCompilerPath })
}
if (this.toolset.compilerFlags.length) {
D.push({ CMAKE_CXX_FLAGS: this.toolset.compilerFlags.join(' ') })
}
if (this.toolset.linkerFlags.length) {
D.push({ CMAKE_SHARED_LINKER_FLAGS: this.toolset.linkerFlags.join(' ') })
}
if (this.toolset.makePath) {
D.push({ CMAKE_MAKE_PROGRAM: this.toolset.makePath })
}
// Load NPM config
for (const [key, value] of Object.entries(npmConfigData)) {
if (key.startsWith('cmake_')) {
const sk = key.substr(6)
if (sk && value) {
D.push({ [sk]: value })
}
}
}
command = command.concat(
D.map(function (p) {
return '-D' + Object.keys(p)[0] + '=' + Object.values(p)[0]
}),
)
return command.concat(this.extraCMakeArgs)
}
getCmakeJsLibString() {
const libs = []
if (environment.isWin) {
const nodeLibDefPath = this.getNodeLibDefPath()
if (nodeLibDefPath) {
libs.push(path.join(this.workDir, 'node.lib'))
} else {
libs.push(...this.dist.winLibs)
}
}
return libs.join(';')
}
async getCmakeJsIncludeString() {
let incPaths = []
if (!this.options.isNodeApi) {
// Include and lib:
if (this.dist.headerOnly) {
incPaths = [path.join(this.dist.internalPath, '/include/node')]
} else {
const nodeH = path.join(this.dist.internalPath, '/src')
const v8H = path.join(this.dist.internalPath, '/deps/v8/include')
const uvH = path.join(this.dist.internalPath, '/deps/uv/include')
incPaths = [nodeH, v8H, uvH]
}
// NAN
const nanH = await locateNAN(this.projectRoot)
if (nanH) {
incPaths.push(nanH)
}
} else {
// Base headers
const apiHeaders = require('node-api-headers')
incPaths.push(apiHeaders.include_dir)
// Node-api
const napiH = await locateNodeApi(this.projectRoot)
if (napiH) {
incPaths.push(napiH)
}
}
return incPaths.join(';')
}
getCmakeJsSrcString() {
const srcPaths = []
if (environment.isWin) {
const delayHook = path.normalize(path.join(__dirname, 'cpp', 'win_delay_load_hook.cc'))
srcPaths.push(delayHook.replace(/\\/gm, '/'))
}
return srcPaths.join(';')
}
getNodeLibDefPath() {
return environment.isWin && this.options.isNodeApi ? headers.def_paths.node_api_def : undefined
}
async configure() {
this.verifyIfAvailable()
this.log.info('CMD', 'CONFIGURE')
const listPath = path.join(this.projectRoot, 'CMakeLists.txt')
const command = await this.getConfigureCommand()
try {
await fs.lstat(listPath)
} catch (e) {
throw new Error("'" + listPath + "' not found.")
}
try {
await fs.ensureDir(this.workDir)
} catch (e) {
// Ignore
}
const cwd = process.cwd()
process.chdir(this.workDir)
try {
await this._run(command)
} finally {
process.chdir(cwd)
}
}
async ensureConfigured() {
try {
await fs.lstat(path.join(this.workDir, 'CMakeCache.txt'))
} catch (e) {
await this.configure()
}
}
getBuildCommand() {
const command = [this.path, '--build', this.workDir, '--config', this.config]
if (this.options.target) {
command.push('--target', this.options.target)
}
if (this.options.parallel) {
command.push('--parallel', this.options.parallel)
}
return Promise.resolve(command.concat(this.extraCMakeArgs))
}
async build() {
this.verifyIfAvailable()
await this.ensureConfigured()
const buildCommand = await this.getBuildCommand()
this.log.info('CMD', 'BUILD')
await this._run(buildCommand)
}
getCleanCommand() {
return [this.path, '-E', 'remove_directory', this.workDir].concat(this.extraCMakeArgs)
}
clean() {
this.verifyIfAvailable()
this.log.info('CMD', 'CLEAN')
return this._run(this.getCleanCommand())
}
async reconfigure() {
this.extraCMakeArgs = []
await this.clean()
await this.configure()
}
async rebuild() {
this.extraCMakeArgs = []
await this.clean()
await this.build()
}
async compile() {
this.extraCMakeArgs = []
try {
await this.build()
} catch (e) {
this.log.info('REP', 'Build has been failed, trying to do a full rebuild.')
await this.rebuild()
}
}
_run(command) {
this.log.info('RUN', command)
return processHelpers.run(command, { silent: this.silent })
}
async getGenerators() {
return CMake.getGenerators(this.options, this.log)
}
}
module.exports = CMake