diff --git a/.gitignore b/.gitignore index d841f14b..5135cda3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ node_modules build package-lock.json yarn.lock +rewrite/dist # Users Environment Variables .lock-wscript diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..ec6d3cdd --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +package.json diff --git a/Lib.cmake b/Lib.cmake new file mode 100644 index 00000000..7965d4e6 --- /dev/null +++ b/Lib.cmake @@ -0,0 +1,142 @@ +cmake_minimum_required(VERSION 3.15) + +# setup some cmake required config +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +add_definitions("-DBUILDING_NODE_EXTENSION") + +# 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. +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +# Setup some variables +if(NOT CMAKE_BUILD_TYPE) + SET (CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) +endif() +if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}") +endif() + +set(NODE_PATH "node") # TODO - allow setting externally + +# Find versions info +execute_process(COMMAND ${NODE_PATH} "${CMAKE_CURRENT_LIST_DIR}/bin/cmake-js-versions" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE CMAKE_JS_VERSIONS +) +if (NOT DEFINED CMAKE_JS_VERSIONS OR "${CMAKE_JS_VERSIONS}" STREQUAL "") + message(FATAL_ERROR "Failed to find cmake-js and nodejs versions!") +endif() +string(REGEX MATCH "CMAKEJS_VERSION ([0-9a-zA-Z\.]+)" _ ${CMAKE_JS_VERSIONS}) +set(CMAKE_JS_VERSION ${CMAKE_MATCH_1}) +string(REGEX MATCH "NODE_RUNTIME ([0-9a-zA-Z\.]+)" _ ${CMAKE_JS_VERSIONS}) +set(NODE_RUNTIME ${CMAKE_MATCH_1}) +string(REGEX MATCH "NODE_RUNTIMEVERSION ([0-9a-zA-Z\.]+)" _ ${CMAKE_JS_VERSIONS}) +set(NODE_RUNTIMEVERSION ${CMAKE_MATCH_1}) +string(REGEX MATCH "NODE_ARCH ([0-9a-zA-Z\.]+)" _ ${CMAKE_JS_VERSIONS}) +set(NODE_ARCH ${CMAKE_MATCH_1}) + +set(CMAKE_JS_PATH ${CMAKE_CURRENT_LIST_DIR}) # cache value of CMAKE_CURRENT_LIST_DIR, as it changes inside function calls + +# cmake-js version of CMake `add_library` specifically for node addons +FUNCTION (cmake_js_add_node_addon PROJECT_NAME) + cmake_parse_arguments( + PARSED_ARGS # prefix of output variables + "OLD_ADDON_API" # list of names of the boolean arguments (only defined ones will be true) + "" # list of names of mono-valued arguments + "SOURCES" # list of names of multi-valued arguments (output variables are lists) + ${ARGN} # arguments of the function to parse, here we take the all original ones + ) + + # windows delay hook + set(CMAKE_JS_SRC "") + if (MSVC) + list (APPEND CMAKE_JS_SRC "${CMAKE_JS_PATH}/lib/cpp/win_delay_load_hook.cc") + endif() + + # Setup the library and some default config + add_library(${PROJECT_NAME} SHARED ${PARSED_ARGS_SOURCES} ${CMAKE_JS_SRC}) + set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") + + if (OLD_ADDON_API) + # # Try finding nan + # execute_process(COMMAND ${NODE_PATH} -p "require('nan').include" + # WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + # OUTPUT_VARIABLE NODE_NAN_DIR + # OUTPUT_STRIP_TRAILING_WHITESPACE + # ) + # if (DEFINED NODE_ADDON_API_DIR AND NOT "${NODE_NAN_DIR}" STREQUAL "") + # string(REGEX REPLACE "[\"]" "" NODE_NAN_DIR ${NODE_NAN_DIR}) + # message("Found nan: ${NODE_NAN_DIR}") + # target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_NAN_DIR}) + # endif() + + # TODO nan and headers + + # include headers + set (NODE_HEADERS_DIR "") # TODO + target_include_directories(${PROJECT_NAME} PRIVATE + # some runtimes provide tidy headers + "${NODE_HEADERS_DIR}/include/node" + # some runtimes provide a lot more stuff + "${NODE_HEADERS_DIR}/src" + "${NODE_HEADERS_DIR}/deps/v8/include" + "${NODE_HEADERS_DIR}/deps/uv/include" + ) + + else() + # Find node-addon-api + execute_process(COMMAND ${NODE_PATH} -p "require('node-api-headers').include_dir" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE NODE_API_HEADERS_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if (DEFINED NODE_API_HEADERS_DIR AND NOT "${NODE_API_HEADERS_DIR}" STREQUAL "") + message("Found node-api-headers: ${NODE_API_HEADERS_DIR}") + target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_API_HEADERS_DIR}) + else() + message(FATAL_ERROR "Failed to find node-api-headers!") + endif() + + # Try finding node-addon-api + execute_process(COMMAND ${NODE_PATH} -p "require('node-addon-api').include" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE NODE_ADDON_API_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if (DEFINED NODE_ADDON_API_DIR AND NOT "${NODE_ADDON_API_DIR}" STREQUAL "") + string(REGEX REPLACE "[\"]" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR}) + message("Found node-addon-api: ${NODE_ADDON_API_DIR}") + target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_ADDON_API_DIR}) + endif() + + # Generate node.lib if needed + if(MSVC) + # Find node-addon-api + execute_process(COMMAND ${NODE_PATH} -p "require('node-api-headers').def_paths.node_api_def" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE NODE_API_DEF_PATH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if (DEFINED NODE_API_DEF_PATH AND NOT "${NODE_API_DEF_PATH}" STREQUAL "") + # Generate node.lib + set(CMAKE_JS_NODELIB_TARGET "${CMAKE_BINARY_DIR}/node.lib") + execute_process(COMMAND ${CMAKE_AR} /def:${NODE_API_DEF_PATH} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) + target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_NODELIB_TARGET}) + else() + message(FATAL_ERROR "Failed to find node-api-headers node_api_def!") + endif() + + endif() + endif() + + if (MSVC) + # setup delayload + target_link_options(${PROJECT_NAME} PRIVATE "/DELAYLOAD:NODE.EXE") + target_link_libraries(${PROJECT_NAME} PRIVATE delayimp) + + if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)") + target_link_options(${PROJECT_NAME} PUBLIC "/SAFESEH:NO") + endif() + endif() + +ENDFUNCTION () \ No newline at end of file diff --git a/bin/cmake-js-cmake.mjs b/bin/cmake-js-cmake.mjs new file mode 100755 index 00000000..b365d37b --- /dev/null +++ b/bin/cmake-js-cmake.mjs @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +/** + * This script is intended to be a minimal helper script which makes for easy invocation of `cmake`, + * utilising the same logic to locate cmake and potentially fill in some parameters + */ + +// TODO - a helper to replace #GENERATOR# with autoselected generator, to simplify windows + +import { runCommand } from '../rewrite/dist/processHelpers.js' +import { findCmake } from '../rewrite/dist/toolchain.js' + +const cmakePath = await findCmake() + +await runCommand([cmakePath, ...process.argv.slice(2)]) diff --git a/bin/cmake-js-versions b/bin/cmake-js-versions new file mode 100755 index 00000000..04e2cc22 --- /dev/null +++ b/bin/cmake-js-versions @@ -0,0 +1,9 @@ +#!/usr/bin/env node +'use strict' + +const version = require('../package').version + +console.log(`CMAKEJS_VERSION ${version}`) +console.log(`NODE_RUNTIME node`) +console.log(`NODE_RUNTIMEVERSION ${process.version.slice(1)}`) +console.log(`NODE_ARCH ${process.arch}`) diff --git a/lib/import/find-visualstudio.d.ts b/lib/import/find-visualstudio.d.ts new file mode 100644 index 00000000..48c95cba --- /dev/null +++ b/lib/import/find-visualstudio.d.ts @@ -0,0 +1,15 @@ +export interface FoundVisualStudio { + version: string + versionMajor: number + versionMinor: number + versionYear: number + + path: string + msBuild: string + toolset: string + sdk: string +} + +export class FindVisualStudio { + static findVisualStudio(nodeSemver: string, configMsvsVersion?: string): Promise +} diff --git a/package.json b/package.json index 6b9aae0e..f0a6fac8 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "url": "git://github.com/cmake-js/cmake-js.git" }, "bin": { - "cmake-js": "./bin/cmake-js" + "cmake-js": "./bin/cmake-js", + "cmake-js-cmake": "./bin/cmake-js-cmake.mjs" }, "engines": { "node": ">= 14.15.0" @@ -55,12 +56,16 @@ "yargs": "^17.7.2" }, "devDependencies": { + "@tsconfig/node14": "^14.1.0", + "@types/node": "^14.18.63", + "@types/which": "^2.0.2", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "mocha": "*", "nan": "^2.18.0", "node-addon-api": "^6.1.0", - "prettier": "^3.2.2" + "prettier": "^3.2.2", + "typescript": "^5.3.3" }, "scripts": { "test": "mocha tests", diff --git a/rewrite/old/buildSystem.js b/rewrite/old/buildSystem.js new file mode 100644 index 00000000..eaf19156 --- /dev/null +++ b/rewrite/old/buildSystem.js @@ -0,0 +1,96 @@ +'use strict' +const CMake = require('./cMake') +const Dist = require('./dist') +const CMLog = require('./cmLog') +const appCMakeJSConfig = require('./appCMakeJSConfig') +const npmConfig = require('./npmConfig') +const path = require('path') +const Toolset = require('./toolset') + +class BuildSystem { + constructor(options) { + this.options = options || {} + this.options.directory = path.resolve(this.options.directory || process.cwd()) + this.options.out = path.resolve(this.options.out || path.join(this.options.directory, 'build')) + this.log = new CMLog(this.options) + const npmOptions = npmConfig(this.log) + + if (npmOptions && typeof npmOptions === 'object' && Object.keys(npmOptions).length) { + this.options.runtimeDirectory = npmOptions['nodedir'] + this.options.msvsVersion = npmOptions['msvs_version'] + } + + this.log.verbose('CFG', 'Build system options:') + this.log.verbose('CFG', JSON.stringify(this.options)) + this.cmake = new CMake(this.options) + this.dist = new Dist(this.options) + this.toolset = new Toolset(this.options) + } + async _ensureInstalled() { + try { + await this.toolset.initialize(true) + if (!this.options.isNodeApi) { + await this.dist.ensureDownloaded() + } + } catch (e) { + this._showError(e) + throw e + } + } + _showError(e) { + if (this.log === undefined) { + // handle internal errors (init failed) + console.error('OMG', e.stack) + return + } + if (this.log.level === 'verbose' || this.log.level === 'silly') { + this.log.error('OMG', e.stack) + } else { + this.log.error('OMG', e.message) + } + } + install() { + return this._ensureInstalled() + } + async _invokeCMake(method) { + try { + await this._ensureInstalled() + return await this.cmake[method]() + } catch (e) { + this._showError(e) + throw e + } + } + getConfigureCommand() { + return this._invokeCMake('getConfigureCommand') + } + getCmakeJsLibString() { + return this._invokeCMake('getCmakeJsLibString') + } + configure() { + return this._invokeCMake('configure') + } + getBuildCommand() { + return this._invokeCMake('getBuildCommand') + } + build() { + return this._invokeCMake('build') + } + getCleanCommand() { + return this._invokeCMake('getCleanCommand') + } + clean() { + return this._invokeCMake('clean') + } + reconfigure() { + return this._invokeCMake('reconfigure') + } + rebuild() { + return this._invokeCMake('rebuild') + } + compile() { + return this._invokeCMake('compile') + } +} + +module.exports = BuildSystem diff --git a/rewrite/old/cMake.js b/rewrite/old/cMake.js new file mode 100644 index 00000000..c289d1b3 --- /dev/null +++ b/rewrite/old/cMake.js @@ -0,0 +1,292 @@ +'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 npmConfigData = require('rc')('npm') +const Toolset = require('./toolset') + +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 = [] + + + // 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 }) + } + + // 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 (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) { + } else { + libs.push(...this.dist.winLibs) + } + } + return libs.join(';') + } + + 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 diff --git a/rewrite/old/cmLog.js b/rewrite/old/cmLog.js new file mode 100644 index 00000000..58fbdfa6 --- /dev/null +++ b/rewrite/old/cmLog.js @@ -0,0 +1,61 @@ +'use strict' +const log = require('npmlog') + +class CMLog { + get level() { + if (this.options.noLog) { + return 'silly' + } else { + return log.level + } + } + + constructor(options) { + this.options = options || {} + this.debug = require('debug')(this.options.logName || 'cmake-js') + } + silly(cat, msg) { + if (this.options.noLog) { + this.debug(cat + ': ' + msg) + } else { + log.silly(cat, msg) + } + } + verbose(cat, msg) { + if (this.options.noLog) { + this.debug(cat + ': ' + msg) + } else { + log.verbose(cat, msg) + } + } + info(cat, msg) { + if (this.options.noLog) { + this.debug(cat + ': ' + msg) + } else { + log.info(cat, msg) + } + } + warn(cat, msg) { + if (this.options.noLog) { + this.debug(cat + ': ' + msg) + } else { + log.warn(cat, msg) + } + } + http(cat, msg) { + if (this.options.noLog) { + this.debug(cat + ': ' + msg) + } else { + log.http(cat, msg) + } + } + error(cat, msg) { + if (this.options.noLog) { + this.debug(cat + ': ' + msg) + } else { + log.error(cat, msg) + } + } +} + +module.exports = CMLog diff --git a/rewrite/old/dist.js b/rewrite/old/dist.js new file mode 100644 index 00000000..6bfbd320 --- /dev/null +++ b/rewrite/old/dist.js @@ -0,0 +1,176 @@ +'use strict' +const environment = require('./environment') +const path = require('path') +const urljoin = require('url-join') +const fs = require('fs-extra') +const CMLog = require('./cmLog') +const TargetOptions = require('./targetOptions') +const runtimePaths = require('./runtimePaths') +const Downloader = require('./downloader') +const os = require('os') + +function testSum(sums, sum, fPath) { + const serverSum = sums.find(function (s) { + return s.getPath === fPath + }) + if (serverSum && serverSum.sum === sum) { + return + } + throw new Error("SHA sum of file '" + fPath + "' mismatch!") +} + +class Dist { + get internalPath() { + const cacheDirectory = '.cmake-js' + const runtimeArchDirectory = this.targetOptions.runtime + '-' + this.targetOptions.arch + const runtimeVersionDirectory = 'v' + this.targetOptions.runtimeVersion + + return ( + this.options.runtimeDirectory || + path.join(os.homedir(), cacheDirectory, runtimeArchDirectory, runtimeVersionDirectory) + ) + } + get externalPath() { + return runtimePaths.get(this.targetOptions).externalPath + } + get downloaded() { + let headers = false + let libs = true + let stat = getStat(this.internalPath) + if (stat.isDirectory()) { + if (this.headerOnly) { + stat = getStat(path.join(this.internalPath, 'include/node/node.h')) + headers = stat.isFile() + } else { + stat = getStat(path.join(this.internalPath, 'src/node.h')) + if (stat.isFile()) { + stat = getStat(path.join(this.internalPath, 'deps/v8/include/v8.h')) + headers = stat.isFile() + } + } + if (environment.isWin) { + for (const libPath of this.winLibs) { + stat = getStat(libPath) + libs = libs && stat.isFile() + } + } + } + return headers && libs + + function getStat(path) { + try { + return fs.statSync(path) + } catch (e) { + return { + isFile: () => false, + isDirectory: () => false, + } + } + } + } + get winLibs() { + const libs = runtimePaths.get(this.targetOptions).winLibs + const result = [] + for (const lib of libs) { + result.push(path.join(this.internalPath, lib.dir, lib.name)) + } + return result + } + get headerOnly() { + return runtimePaths.get(this.targetOptions).headerOnly + } + + constructor(options) { + this.options = options || {} + this.log = new CMLog(this.options) + this.targetOptions = new TargetOptions(this.options) + this.downloader = new Downloader(this.options) + } + + async ensureDownloaded() { + if (!this.downloaded) { + await this.download() + } + } + async download() { + const log = this.log + log.info('DIST', 'Downloading distribution files to: ' + this.internalPath) + await fs.ensureDir(this.internalPath) + const sums = await this._downloadShaSums() + await Promise.all([this._downloadLibs(sums), this._downloadTar(sums)]) + } + async _downloadShaSums() { + if (this.targetOptions.runtime === 'node') { + const sumUrl = urljoin(this.externalPath, 'SHASUMS256.txt') + const log = this.log + log.http('DIST', '\t- ' + sumUrl) + return (await this.downloader.downloadString(sumUrl)) + .split('\n') + .map(function (line) { + const parts = line.split(/\s+/) + return { + getPath: parts[1], + sum: parts[0], + } + }) + .filter(function (i) { + return i.getPath && i.sum + }) + } else { + return null + } + } + async _downloadTar(sums) { + const log = this.log + const self = this + const tarLocalPath = runtimePaths.get(self.targetOptions).tarPath + const tarUrl = urljoin(self.externalPath, tarLocalPath) + log.http('DIST', '\t- ' + tarUrl) + + const sum = await this.downloader.downloadTgz(tarUrl, { + hash: sums ? 'sha256' : null, + cwd: self.internalPath, + strip: 1, + filter: function (entryPath) { + if (entryPath === self.internalPath) { + return true + } + const ext = path.extname(entryPath) + return ext && ext.toLowerCase() === '.h' + }, + }) + + if (sums) { + testSum(sums, sum, tarLocalPath) + } + } + async _downloadLibs(sums) { + const log = this.log + const self = this + if (!environment.isWin) { + return + } + + const paths = runtimePaths.get(self.targetOptions) + for (const dirs of paths.winLibs) { + const subDir = dirs.dir + const fn = dirs.name + const fPath = subDir ? urljoin(subDir, fn) : fn + const libUrl = urljoin(self.externalPath, fPath) + log.http('DIST', '\t- ' + libUrl) + + await fs.ensureDir(path.join(self.internalPath, subDir)) + + const sum = await this.downloader.downloadFile(libUrl, { + path: path.join(self.internalPath, fPath), + hash: sums ? 'sha256' : null, + }) + + if (sums) { + testSum(sums, sum, fPath) + } + } + } +} + +module.exports = Dist diff --git a/rewrite/old/environment.js b/rewrite/old/environment.js new file mode 100644 index 00000000..0cc7402b --- /dev/null +++ b/rewrite/old/environment.js @@ -0,0 +1,96 @@ +'use strict' +const os = require('os') +const which = require('which') + +const environment = (module.exports = { + cmakeJsVersion: require('../package.json').version, + platform: os.platform(), + isWin: os.platform() === 'win32', + isLinux: os.platform() === 'linux', + isOSX: os.platform() === 'darwin', + arch: os.arch(), + isX86: os.arch() === 'ia32' || os.arch() === 'x86', + isX64: os.arch() === 'x64', + isArm: os.arch() === 'arm', + runtime: 'node', + runtimeVersion: process.versions.node, +}) + +Object.defineProperties(environment, { + _isNinjaAvailable: { + value: null, + writable: true, + }, + isNinjaAvailable: { + get: function () { + if (this._isNinjaAvailable === null) { + this._isNinjaAvailable = false + try { + if (which.sync('ninja')) { + this._isNinjaAvailable = true + } + } catch (e) { + // Ignore + } + } + return this._isNinjaAvailable + }, + }, + _isMakeAvailable: { + value: null, + writable: true, + }, + isMakeAvailable: { + get: function () { + if (this._isMakeAvailable === null) { + this._isMakeAvailable = false + try { + if (which.sync('make')) { + this._isMakeAvailable = true + } + } catch (e) { + // Ignore + } + } + return this._isMakeAvailable + }, + }, + _isGPPAvailable: { + value: null, + writable: true, + }, + isGPPAvailable: { + get: function () { + if (this._isGPPAvailable === null) { + this._isGPPAvailable = false + try { + if (which.sync('g++')) { + this._isGPPAvailable = true + } + } catch (e) { + // Ignore + } + } + return this._isGPPAvailable + }, + }, + _isClangAvailable: { + value: null, + writable: true, + }, + isClangAvailable: { + get: function () { + if (this._isClangAvailable === null) { + this._isClangAvailable = false + try { + if (which.sync('clang++')) { + this._isClangAvailable = true + } + } catch (e) { + // Ignore + } + } + return this._isClangAvailable + }, + }, +}) diff --git a/rewrite/old/npmConfig.js b/rewrite/old/npmConfig.js new file mode 100644 index 00000000..0fb6cf2d --- /dev/null +++ b/rewrite/old/npmConfig.js @@ -0,0 +1,31 @@ +'use strict' + +function getNpmConfig() { + const npmOptions = {} + const npmConfigPrefix = 'npm_config_' + Object.keys(process.env).forEach(function (name) { + if (name.indexOf(npmConfigPrefix) !== 0) { + return + } + const value = process.env[name] + name = name.substring(npmConfigPrefix.length) + if (name) { + npmOptions[name] = value + } + }, this) + + return npmOptions +} + +module.exports = function (log) { + log.verbose('CFG', 'Looking for NPM config.') + const options = getNpmConfig() + + if (options) { + log.silly('CFG', 'NPM options:', options) + } else { + log.verbose('CFG', 'There are no NPM options available.') + } + + return options +} diff --git a/rewrite/old/runtimePaths.js b/rewrite/old/runtimePaths.js new file mode 100644 index 00000000..35fb2a17 --- /dev/null +++ b/rewrite/old/runtimePaths.js @@ -0,0 +1,95 @@ +'use strict' +const assert = require('assert') +const semver = require('semver') + +const NODE_MIRROR = process.env.NVM_NODEJS_ORG_MIRROR || 'https://nodejs.org/dist' +const ELECTRON_MIRROR = process.env.ELECTRON_MIRROR || 'https://artifacts.electronjs.org/headers/dist' + +const runtimePaths = { + node: function (targetOptions) { + if (semver.lt(targetOptions.runtimeVersion, '4.0.0')) { + return { + externalPath: NODE_MIRROR + '/v' + targetOptions.runtimeVersion + '/', + winLibs: [ + { + dir: targetOptions.isX64 ? 'x64' : '', + name: 'node.lib', + }, + ], + tarPath: 'node-v' + targetOptions.runtimeVersion + '.tar.gz', + headerOnly: false, + } + } else { + return { + externalPath: NODE_MIRROR + '/v' + targetOptions.runtimeVersion + '/', + winLibs: [ + { + dir: targetOptions.isX64 ? 'win-x64' : 'win-x86', + name: 'node.lib', + }, + ], + tarPath: 'node-v' + targetOptions.runtimeVersion + '-headers.tar.gz', + headerOnly: true, + } + } + }, + nw: function (targetOptions) { + if (semver.gte(targetOptions.runtimeVersion, '0.13.0')) { + return { + externalPath: 'https://node-webkit.s3.amazonaws.com/v' + targetOptions.runtimeVersion + '/', + winLibs: [ + { + dir: targetOptions.isX64 ? 'x64' : '', + name: 'nw.lib', + }, + { + dir: targetOptions.isX64 ? 'x64' : '', + name: 'node.lib', + }, + ], + tarPath: 'nw-headers-v' + targetOptions.runtimeVersion + '.tar.gz', + headerOnly: false, + } + } + return { + externalPath: 'https://node-webkit.s3.amazonaws.com/v' + targetOptions.runtimeVersion + '/', + winLibs: [ + { + dir: targetOptions.isX64 ? 'x64' : '', + name: 'nw.lib', + }, + ], + tarPath: 'nw-headers-v' + targetOptions.runtimeVersion + '.tar.gz', + headerOnly: false, + } + }, + electron: function (targetOptions) { + return { + externalPath: ELECTRON_MIRROR + '/v' + targetOptions.runtimeVersion + '/', + winLibs: [ + { + dir: targetOptions.isX64 ? 'x64' : '', + name: 'node.lib', + }, + ], + tarPath: 'node' + '-v' + targetOptions.runtimeVersion + '.tar.gz', + headerOnly: semver.gte(targetOptions.runtimeVersion, '4.0.0-alpha'), + } + }, + get: function (targetOptions) { + assert(targetOptions && typeof targetOptions === 'object') + + const runtime = targetOptions.runtime + const func = runtimePaths[runtime] + let paths + if (typeof func === 'function') { + paths = func(targetOptions) + if (paths && typeof paths === 'object') { + return paths + } + } + throw new Error('Unknown runtime: ' + runtime) + }, +} + +module.exports = runtimePaths diff --git a/rewrite/old/targetOptions.js b/rewrite/old/targetOptions.js new file mode 100644 index 00000000..c61bf6fe --- /dev/null +++ b/rewrite/old/targetOptions.js @@ -0,0 +1,30 @@ +'use strict' + +const environment = require('./environment') + +class TargetOptions { + get arch() { + return this.options.arch || environment.arch + } + get isX86() { + return this.arch === 'ia32' || this.arch === 'x86' + } + get isX64() { + return this.arch === 'x64' + } + get isArm() { + return this.arch === 'arm' + } + get runtime() { + return this.options.runtime || environment.runtime + } + get runtimeVersion() { + return this.options.runtimeVersion || environment.runtimeVersion + } + + constructor(options) { + this.options = options || {} + } +} + +module.exports = TargetOptions diff --git a/rewrite/old/toolset.js b/rewrite/old/toolset.js new file mode 100644 index 00000000..636da60e --- /dev/null +++ b/rewrite/old/toolset.js @@ -0,0 +1,214 @@ +'use strict' +const TargetOptions = require('./targetOptions') +const environment = require('./environment') +const assert = require('assert') +const CMLog = require('./cmLog') +const { findVisualStudio } = environment.isWin ? require('./import/find-visualstudio') : {} + +class Toolset { + constructor(options) { + this.options = options || {} + this.targetOptions = new TargetOptions(this.options) + this.generator = options.generator + this.toolset = options.toolset + this.platform = options.platform + this.target = options.target + this.cCompilerPath = options.cCompilerPath + this.cppCompilerPath = options.cppCompilerPath + this.compilerFlags = [] + this.linkerFlags = [] + this.makePath = null + this.log = new CMLog(this.options) + this._initialized = false + } + async initialize(install) { + if (!this._initialized) { + if (environment.isWin) { + await this.initializeWin(install) + } else { + this.initializePosix(install) + } + this._initialized = true + } + } + initializePosix(install) { + if (!this.cCompilerPath || !this.cppCompilerPath) { + // 1: Compiler + if (!environment.isGPPAvailable && !environment.isClangAvailable) { + if (environment.isOSX) { + throw new Error( + "C++ Compiler toolset is not available. Install Xcode Commandline Tools from Apple Dev Center, or install Clang with homebrew by invoking: 'brew install llvm --with-clang --with-asan'.", + ) + } else { + throw new Error( + "C++ Compiler toolset is not available. Install proper compiler toolset with your package manager, eg. 'sudo apt-get install g++'.", + ) + } + } + + if (this.options.preferClang && environment.isClangAvailable) { + if (install) { + this.log.info('TOOL', 'Using clang++ compiler, because preferClang option is set, and clang++ is available.') + } + this.cppCompilerPath = this.cppCompilerPath || 'clang++' + this.cCompilerPath = this.cCompilerPath || 'clang' + } else if (this.options.preferGnu && environment.isGPPAvailable) { + if (install) { + this.log.info('TOOL', 'Using g++ compiler, because preferGnu option is set, and g++ is available.') + } + this.cppCompilerPath = this.cppCompilerPath || 'g++' + this.cCompilerPath = this.cCompilerPath || 'gcc' + } + } + // if it's already set because of options... + if (this.generator) { + if (install) { + this.log.info('TOOL', 'Using ' + this.generator + ' generator, as specified from commandline.') + } + } + + // 2: Generator + else if (environment.isOSX) { + if (this.options.preferXcode) { + if (install) { + this.log.info('TOOL', 'Using Xcode generator, because preferXcode option is set.') + } + this.generator = 'Xcode' + } else if (this.options.preferMake && environment.isMakeAvailable) { + if (install) { + this.log.info( + 'TOOL', + 'Using Unix Makefiles generator, because preferMake option is set, and make is available.', + ) + } + this.generator = 'Unix Makefiles' + } else if (environment.isNinjaAvailable) { + if (install) { + this.log.info('TOOL', 'Using Ninja generator, because ninja is available.') + } + this.generator = 'Ninja' + } else { + if (install) { + this.log.info('TOOL', 'Using Unix Makefiles generator.') + } + this.generator = 'Unix Makefiles' + } + } else { + if (this.options.preferMake && environment.isMakeAvailable) { + if (install) { + this.log.info( + 'TOOL', + 'Using Unix Makefiles generator, because preferMake option is set, and make is available.', + ) + } + this.generator = 'Unix Makefiles' + } else if (environment.isNinjaAvailable) { + if (install) { + this.log.info('TOOL', 'Using Ninja generator, because ninja is available.') + } + this.generator = 'Ninja' + } else { + if (install) { + this.log.info('TOOL', 'Using Unix Makefiles generator.') + } + this.generator = 'Unix Makefiles' + } + } + + // 3: Flags + if (environment.isOSX) { + if (install) { + this.log.verbose('TOOL', 'Setting default OSX compiler flags.') + } + + this.compilerFlags.push('-D_DARWIN_USE_64_BIT_INODE=1') + this.compilerFlags.push('-D_LARGEFILE_SOURCE') + this.compilerFlags.push('-D_FILE_OFFSET_BITS=64') + this.linkerFlags.push('-undefined dynamic_lookup') + } + + // 4: Build target + if (this.options.target) { + this.log.info('TOOL', 'Building only the ' + this.options.target + ' target, as specified from the command line.') + } + } + async initializeWin(install) { + if (!this.generator) { + const foundVsInfo = await this._getTopSupportedVisualStudioGenerator() + if (foundVsInfo) { + if (install) { + this.log.info('TOOL', `Using ${foundVsInfo.generator} generator.`) + } + this.generator = foundVsInfo.generator + + const isAboveVS16 = foundVsInfo.versionMajor >= 16 + + // The CMake Visual Studio Generator does not support the Win64 or ARM suffix on + // the generator name. Instead the generator platform must be set explicitly via + // the platform parameter + if (!this.platform && isAboveVS16) { + switch (this.targetOptions.arch) { + case 'ia32': + case 'x86': + this.platform = 'Win32' + break + case 'x64': + this.platform = 'x64' + break + case 'arm': + this.platform = 'ARM' + break + case 'arm64': + this.platform = 'ARM64' + break + default: + this.log.warn('TOOL', 'Unknown NodeJS architecture: ' + this.targetOptions.arch) + break + } + } + } else { + throw new Error('There is no Visual C++ compiler installed. Install Visual C++ Build Toolset or Visual Studio.') + } + } else { + // if it's already set because of options... + if (install) { + this.log.info('TOOL', 'Using ' + this.options.generator + ' generator, as specified from commandline.') + } + } + + } + async _getTopSupportedVisualStudioGenerator() { + const CMake = require('./cMake') + assert(environment.isWin) + + const selectedVs = await findVisualStudio(environment.runtimeVersion, this.options.msvsVersion) + if (!selectedVs) return null + + const list = await CMake.getGenerators(this.options, this.log) + for (const gen of list) { + const found = gen.startsWith(`Visual Studio ${selectedVs.versionMajor}`) + if (!found) { + continue + } + + // unlike previous versions "Visual Studio 16 2019" and onwards don't end with arch name + const isAboveVS16 = selectedVs.versionMajor >= 16 + if (!isAboveVS16) { + const is64Bit = gen.endsWith('Win64') + if ((this.targetOptions.isX86 && is64Bit) || (this.targetOptions.isX64 && !is64Bit)) { + continue + } + } + + return { + ...selectedVs, + generator: gen, + } + } + + // Nothing matched + return null + } +} + +module.exports = Toolset diff --git a/rewrite/src/processHelpers.ts b/rewrite/src/processHelpers.ts new file mode 100644 index 00000000..699ff7af --- /dev/null +++ b/rewrite/src/processHelpers.ts @@ -0,0 +1,49 @@ +import { spawn, execFile as execFileRaw } from 'child_process' + +export async function runCommand(command: string[], options?: { silent?: boolean }): Promise { + return new Promise(function (resolve, reject) { + if (!options) options = {} + + const env = Object.assign({}, process.env) + if (env.Path && env.PATH) { + if (env.Path !== env.PATH) { + env.PATH = env.Path + ';' + env.PATH + } + delete env.Path + } + + const child = spawn(command[0], command.slice(1), { + stdio: options.silent ? 'ignore' : 'inherit', + env, + }) + let ended = false + child.on('error', function (e) { + if (!ended) { + reject(e) + ended = true + } + }) + child.on('exit', function (code, signal) { + if (!ended) { + if (code === 0) { + resolve() + } else { + reject(new Error('Process terminated: ' + (code || signal))) + } + ended = true + } + }) + }) +} + +export async function execFile(command: string[]): Promise { + return new Promise(function (resolve, reject) { + execFileRaw(command[0], command.slice(1), function (err, stdout, stderr) { + if (err) { + reject(new Error(err.message + '\n' + (stdout || stderr))) + } else { + resolve(stdout) + } + }) + }) +} diff --git a/rewrite/src/toolchain.ts b/rewrite/src/toolchain.ts new file mode 100644 index 00000000..ab424e7c --- /dev/null +++ b/rewrite/src/toolchain.ts @@ -0,0 +1,23 @@ +import fs from 'fs/promises' +import which from 'which' + +export async function findCmake(): Promise { + const overridePath = process.env['CMAKE_JS_CMAKE_PATH'] + if (overridePath) { + try { + const stat = await fs.lstat(overridePath) + if (!stat.isDirectory()) { + return overridePath + } + } catch (e) { + // Ignore + } + throw new Error(`Invalid cmake path: ${overridePath}`) + } else { + const res = which('cmake', { first: true }) + if (!res) { + throw new Error(`cmake not found in PATH`) + } + return res + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..b9ecef54 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@tsconfig/node14/tsconfig.json", + "include": ["rewrite/src/**/*.ts"], + "exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*", "scratch/**"], + "compilerOptions": { + "outDir": "./rewrite/dist", + "baseUrl": "./", + "paths": { + "*": ["./node_modules/*"] + }, + "types": ["node"], + "module": "Node16", + "typeRoots": ["./typings", "./node_modules/@types"] + } +}