1'use strict' 2 3const fs = require('graceful-fs') 4const path = require('path') 5const log = require('npmlog') 6const os = require('os') 7const mkdirp = require('mkdirp') 8const processRelease = require('./process-release') 9const win = process.platform === 'win32' 10const findNodeDirectory = require('./find-node-directory') 11const msgFormat = require('util').format 12var findPython = require('./find-python') 13if (win) { 14 var findVisualStudio = require('./find-visualstudio') 15} 16 17function configure (gyp, argv, callback) { 18 var python 19 var buildDir = path.resolve('build') 20 var configNames = ['config.gypi', 'common.gypi'] 21 var configs = [] 22 var nodeDir 23 var release = processRelease(argv, gyp, process.version, process.release) 24 25 findPython(gyp.opts.python, function (err, found) { 26 if (err) { 27 callback(err) 28 } else { 29 python = found 30 getNodeDir() 31 } 32 }) 33 34 function getNodeDir () { 35 // 'python' should be set by now 36 process.env.PYTHON = python 37 38 if (gyp.opts.nodedir) { 39 // --nodedir was specified. use that for the dev files 40 nodeDir = gyp.opts.nodedir.replace(/^~/, os.homedir()) 41 42 log.verbose('get node dir', 'compiling against specified --nodedir dev files: %s', nodeDir) 43 createBuildDir() 44 } else { 45 // if no --nodedir specified, ensure node dependencies are installed 46 if ('v' + release.version !== process.version) { 47 // if --target was given, then determine a target version to compile for 48 log.verbose('get node dir', 'compiling against --target node version: %s', release.version) 49 } else { 50 // if no --target was specified then use the current host node version 51 log.verbose('get node dir', 'no --target version specified, falling back to host node version: %s', release.version) 52 } 53 54 if (!release.semver) { 55 // could not parse the version string with semver 56 return callback(new Error('Invalid version number: ' + release.version)) 57 } 58 59 // If the tarball option is set, always remove and reinstall the headers 60 // into devdir. Otherwise only install if they're not already there. 61 gyp.opts.ensure = !gyp.opts.tarball 62 63 gyp.commands.install([release.version], function (err) { 64 if (err) { 65 return callback(err) 66 } 67 log.verbose('get node dir', 'target node version installed:', release.versionDir) 68 nodeDir = path.resolve(gyp.devDir, release.versionDir) 69 createBuildDir() 70 }) 71 } 72 } 73 74 function createBuildDir () { 75 log.verbose('build dir', 'attempting to create "build" dir: %s', buildDir) 76 mkdirp(buildDir, function (err, isNew) { 77 if (err) { 78 return callback(err) 79 } 80 log.verbose('build dir', '"build" dir needed to be created?', isNew) 81 if (win) { 82 findVisualStudio(release.semver, gyp.opts.msvs_version, 83 createConfigFile) 84 } else { 85 createConfigFile() 86 } 87 }) 88 } 89 90 function createConfigFile (err, vsInfo) { 91 if (err) { 92 return callback(err) 93 } 94 95 var configFilename = 'config.gypi' 96 var configPath = path.resolve(buildDir, configFilename) 97 98 log.verbose('build/' + configFilename, 'creating config file') 99 100 var config = process.config || {} 101 var defaults = config.target_defaults 102 var variables = config.variables 103 104 // default "config.variables" 105 if (!variables) { 106 variables = config.variables = {} 107 } 108 109 // default "config.defaults" 110 if (!defaults) { 111 defaults = config.target_defaults = {} 112 } 113 114 // don't inherit the "defaults" from node's `process.config` object. 115 // doing so could cause problems in cases where the `node` executable was 116 // compiled on a different machine (with different lib/include paths) than 117 // the machine where the addon is being built to 118 defaults.cflags = [] 119 defaults.defines = [] 120 defaults.include_dirs = [] 121 defaults.libraries = [] 122 123 // set the default_configuration prop 124 if ('debug' in gyp.opts) { 125 defaults.default_configuration = gyp.opts.debug ? 'Debug' : 'Release' 126 } 127 128 if (!defaults.default_configuration) { 129 defaults.default_configuration = 'Release' 130 } 131 132 // set the target_arch variable 133 variables.target_arch = gyp.opts.arch || process.arch || 'ia32' 134 if (variables.target_arch === 'arm64') { 135 defaults.msvs_configuration_platform = 'ARM64' 136 } 137 138 // set the node development directory 139 variables.nodedir = nodeDir 140 141 // disable -T "thin" static archives by default 142 variables.standalone_static_library = gyp.opts.thin ? 0 : 1 143 144 if (win) { 145 process.env.GYP_MSVS_VERSION = Math.min(vsInfo.versionYear, 2015) 146 process.env.GYP_MSVS_OVERRIDE_PATH = vsInfo.path 147 defaults.msbuild_toolset = vsInfo.toolset 148 if (vsInfo.sdk) { 149 defaults.msvs_windows_target_platform_version = vsInfo.sdk 150 } 151 if (variables.target_arch === 'arm64') { 152 if (vsInfo.versionMajor > 15 || 153 (vsInfo.versionMajor === 15 && vsInfo.versionMajor >= 9)) { 154 defaults.msvs_enable_marmasm = 1 155 } else { 156 log.warn('Compiling ARM64 assembly is only available in\n' + 157 'Visual Studio 2017 version 15.9 and above') 158 } 159 } 160 variables.msbuild_path = vsInfo.msBuild 161 } 162 163 // loop through the rest of the opts and add the unknown ones as variables. 164 // this allows for module-specific configure flags like: 165 // 166 // $ node-gyp configure --shared-libxml2 167 Object.keys(gyp.opts).forEach(function (opt) { 168 if (opt === 'argv') { 169 return 170 } 171 if (opt in gyp.configDefs) { 172 return 173 } 174 variables[opt.replace(/-/g, '_')] = gyp.opts[opt] 175 }) 176 177 // ensures that any boolean values from `process.config` get stringified 178 function boolsToString (k, v) { 179 if (typeof v === 'boolean') { 180 return String(v) 181 } 182 return v 183 } 184 185 log.silly('build/' + configFilename, config) 186 187 // now write out the config.gypi file to the build/ dir 188 var prefix = '# Do not edit. File was generated by node-gyp\'s "configure" step' 189 190 var json = JSON.stringify(config, boolsToString, 2) 191 log.verbose('build/' + configFilename, 'writing out config file: %s', configPath) 192 configs.push(configPath) 193 fs.writeFile(configPath, [prefix, json, ''].join('\n'), findConfigs) 194 } 195 196 function findConfigs (err) { 197 if (err) { 198 return callback(err) 199 } 200 201 var name = configNames.shift() 202 if (!name) { 203 return runGyp() 204 } 205 var fullPath = path.resolve(name) 206 207 log.verbose(name, 'checking for gypi file: %s', fullPath) 208 fs.stat(fullPath, function (err) { 209 if (err) { 210 if (err.code === 'ENOENT') { 211 findConfigs() // check next gypi filename 212 } else { 213 callback(err) 214 } 215 } else { 216 log.verbose(name, 'found gypi file') 217 configs.push(fullPath) 218 findConfigs() 219 } 220 }) 221 } 222 223 function runGyp (err) { 224 if (err) { 225 return callback(err) 226 } 227 228 if (!~argv.indexOf('-f') && !~argv.indexOf('--format')) { 229 if (win) { 230 log.verbose('gyp', 'gyp format was not specified; forcing "msvs"') 231 // force the 'make' target for non-Windows 232 argv.push('-f', 'msvs') 233 } else { 234 log.verbose('gyp', 'gyp format was not specified; forcing "make"') 235 // force the 'make' target for non-Windows 236 argv.push('-f', 'make') 237 } 238 } 239 240 // include all the ".gypi" files that were found 241 configs.forEach(function (config) { 242 argv.push('-I', config) 243 }) 244 245 // For AIX and z/OS we need to set up the path to the exports file 246 // which contains the symbols needed for linking. 247 var nodeExpFile 248 if (process.platform === 'aix' || process.platform === 'os390') { 249 var ext = process.platform === 'aix' ? 'exp' : 'x' 250 var nodeRootDir = findNodeDirectory() 251 var candidates 252 253 if (process.platform === 'aix') { 254 candidates = [ 255 'include/node/node', 256 'out/Release/node', 257 'out/Debug/node', 258 'node' 259 ].map(function (file) { 260 return file + '.' + ext 261 }) 262 } else { 263 candidates = [ 264 'out/Release/obj.target/libnode', 265 'out/Debug/obj.target/libnode', 266 'lib/libnode' 267 ].map(function (file) { 268 return file + '.' + ext 269 }) 270 } 271 272 var logprefix = 'find exports file' 273 nodeExpFile = findAccessibleSync(logprefix, nodeRootDir, candidates) 274 if (nodeExpFile !== undefined) { 275 log.verbose(logprefix, 'Found exports file: %s', nodeExpFile) 276 } else { 277 var msg = msgFormat('Could not find node.%s file in %s', ext, nodeRootDir) 278 log.error(logprefix, 'Could not find exports file') 279 return callback(new Error(msg)) 280 } 281 } 282 283 // this logic ported from the old `gyp_addon` python file 284 var gypScript = path.resolve(__dirname, '..', 'gyp', 'gyp_main.py') 285 var addonGypi = path.resolve(__dirname, '..', 'addon.gypi') 286 var commonGypi = path.resolve(nodeDir, 'include/node/common.gypi') 287 fs.stat(commonGypi, function (err) { 288 if (err) { 289 commonGypi = path.resolve(nodeDir, 'common.gypi') 290 } 291 292 var outputDir = 'build' 293 if (win) { 294 // Windows expects an absolute path 295 outputDir = buildDir 296 } 297 var nodeGypDir = path.resolve(__dirname, '..') 298 299 var nodeLibFile = path.join(nodeDir, 300 !gyp.opts.nodedir ? '<(target_arch)' : '$(Configuration)', 301 release.name + '.lib') 302 303 argv.push('-I', addonGypi) 304 argv.push('-I', commonGypi) 305 argv.push('-Dlibrary=shared_library') 306 argv.push('-Dvisibility=default') 307 argv.push('-Dnode_root_dir=' + nodeDir) 308 if (process.platform === 'aix' || process.platform === 'os390') { 309 argv.push('-Dnode_exp_file=' + nodeExpFile) 310 } 311 argv.push('-Dnode_gyp_dir=' + nodeGypDir) 312 313 // Do this to keep Cygwin environments happy, else the unescaped '\' gets eaten up, 314 // resulting in bad paths, Ex c:parentFolderfolderanotherFolder instead of c:\parentFolder\folder\anotherFolder 315 if (win) { 316 nodeLibFile = nodeLibFile.replace(/\\/g, '\\\\') 317 } 318 argv.push('-Dnode_lib_file=' + nodeLibFile) 319 argv.push('-Dmodule_root_dir=' + process.cwd()) 320 argv.push('-Dnode_engine=' + 321 (gyp.opts.node_engine || process.jsEngine || 'v8')) 322 argv.push('--depth=.') 323 argv.push('--no-parallel') 324 325 // tell gyp to write the Makefile/Solution files into output_dir 326 argv.push('--generator-output', outputDir) 327 328 // tell make to write its output into the same dir 329 argv.push('-Goutput_dir=.') 330 331 // enforce use of the "binding.gyp" file 332 argv.unshift('binding.gyp') 333 334 // execute `gyp` from the current target nodedir 335 argv.unshift(gypScript) 336 337 // make sure python uses files that came with this particular node package 338 var pypath = [path.join(__dirname, '..', 'gyp', 'pylib')] 339 if (process.env.PYTHONPATH) { 340 pypath.push(process.env.PYTHONPATH) 341 } 342 process.env.PYTHONPATH = pypath.join(win ? ';' : ':') 343 344 var cp = gyp.spawn(python, argv) 345 cp.on('exit', onCpExit) 346 }) 347 } 348 349 function onCpExit (code) { 350 if (code !== 0) { 351 callback(new Error('`gyp` failed with exit code: ' + code)) 352 } else { 353 // we're done 354 callback() 355 } 356 } 357} 358 359/** 360 * Returns the first file or directory from an array of candidates that is 361 * readable by the current user, or undefined if none of the candidates are 362 * readable. 363 */ 364function findAccessibleSync (logprefix, dir, candidates) { 365 for (var next = 0; next < candidates.length; next++) { 366 var candidate = path.resolve(dir, candidates[next]) 367 try { 368 var fd = fs.openSync(candidate, 'r') 369 } catch (e) { 370 // this candidate was not found or not readable, do nothing 371 log.silly(logprefix, 'Could not open %s: %s', candidate, e.message) 372 continue 373 } 374 fs.closeSync(fd) 375 log.silly(logprefix, 'Found readable %s', candidate) 376 return candidate 377 } 378 379 return undefined 380} 381 382module.exports = configure 383module.exports.test = { 384 findAccessibleSync: findAccessibleSync 385} 386module.exports.usage = 'Generates ' + (win ? 'MSVC project files' : 'a Makefile') + ' for the current module' 387