1'use strict' 2 3const fs = require('graceful-fs') 4const path = require('path') 5const log = require('npmlog') 6const os = require('os') 7const processRelease = require('./process-release') 8const win = process.platform === 'win32' 9const findNodeDirectory = require('./find-node-directory') 10const createConfigGypi = require('./create-config-gypi') 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 buildBinsDir = path.join(buildDir, 'node_gyp_bins') 21 var configNames = ['config.gypi', 'common.gypi'] 22 var configs = [] 23 var nodeDir 24 var release = processRelease(argv, gyp, process.version, process.release) 25 26 findPython(gyp.opts.python, function (err, found) { 27 if (err) { 28 callback(err) 29 } else { 30 python = found 31 getNodeDir() 32 } 33 }) 34 35 function getNodeDir () { 36 // 'python' should be set by now 37 process.env.PYTHON = python 38 39 if (gyp.opts.nodedir) { 40 // --nodedir was specified. use that for the dev files 41 nodeDir = gyp.opts.nodedir.replace(/^~/, os.homedir()) 42 43 log.verbose('get node dir', 'compiling against specified --nodedir dev files: %s', nodeDir) 44 createBuildDir() 45 } else { 46 // if no --nodedir specified, ensure node dependencies are installed 47 if ('v' + release.version !== process.version) { 48 // if --target was given, then determine a target version to compile for 49 log.verbose('get node dir', 'compiling against --target node version: %s', release.version) 50 } else { 51 // if no --target was specified then use the current host node version 52 log.verbose('get node dir', 'no --target version specified, falling back to host node version: %s', release.version) 53 } 54 55 if (!release.semver) { 56 // could not parse the version string with semver 57 return callback(new Error('Invalid version number: ' + release.version)) 58 } 59 60 // If the tarball option is set, always remove and reinstall the headers 61 // into devdir. Otherwise only install if they're not already there. 62 gyp.opts.ensure = !gyp.opts.tarball 63 64 gyp.commands.install([release.version], function (err) { 65 if (err) { 66 return callback(err) 67 } 68 log.verbose('get node dir', 'target node version installed:', release.versionDir) 69 nodeDir = path.resolve(gyp.devDir, release.versionDir) 70 createBuildDir() 71 }) 72 } 73 } 74 75 function createBuildDir () { 76 log.verbose('build dir', 'attempting to create "build" dir: %s', buildDir) 77 78 const deepestBuildDirSubdirectory = win ? buildDir : buildBinsDir 79 fs.mkdir(deepestBuildDirSubdirectory, { recursive: true }, function (err, isNew) { 80 if (err) { 81 return callback(err) 82 } 83 log.verbose( 84 'build dir', '"build" dir needed to be created?', isNew ? 'Yes' : 'No' 85 ) 86 if (win) { 87 findVisualStudio(release.semver, gyp.opts.msvs_version, 88 createConfigFile) 89 } else { 90 createPythonSymlink() 91 createConfigFile() 92 } 93 }) 94 } 95 96 function createPythonSymlink () { 97 const symlinkDestination = path.join(buildBinsDir, 'python3') 98 99 log.verbose('python symlink', `creating symlink to "${python}" at "${symlinkDestination}"`) 100 101 fs.unlink(symlinkDestination, function (err) { 102 if (err && err.code !== 'ENOENT') { 103 log.verbose('python symlink', 'error when attempting to remove existing symlink') 104 log.verbose('python symlink', err.stack, 'errno: ' + err.errno) 105 } 106 fs.symlink(python, symlinkDestination, function (err) { 107 if (err) { 108 log.verbose('python symlink', 'error when attempting to create Python symlink') 109 log.verbose('python symlink', err.stack, 'errno: ' + err.errno) 110 } 111 }) 112 }) 113 } 114 115 function createConfigFile (err, vsInfo) { 116 if (err) { 117 return callback(err) 118 } 119 if (process.platform === 'win32') { 120 process.env.GYP_MSVS_VERSION = Math.min(vsInfo.versionYear, 2015) 121 process.env.GYP_MSVS_OVERRIDE_PATH = vsInfo.path 122 } 123 createConfigGypi({ gyp, buildDir, nodeDir, vsInfo }).then(configPath => { 124 configs.push(configPath) 125 findConfigs() 126 }).catch(err => { 127 callback(err) 128 }) 129 } 130 131 function findConfigs () { 132 var name = configNames.shift() 133 if (!name) { 134 return runGyp() 135 } 136 var fullPath = path.resolve(name) 137 138 log.verbose(name, 'checking for gypi file: %s', fullPath) 139 fs.stat(fullPath, function (err) { 140 if (err) { 141 if (err.code === 'ENOENT') { 142 findConfigs() // check next gypi filename 143 } else { 144 callback(err) 145 } 146 } else { 147 log.verbose(name, 'found gypi file') 148 configs.push(fullPath) 149 findConfigs() 150 } 151 }) 152 } 153 154 function runGyp (err) { 155 if (err) { 156 return callback(err) 157 } 158 159 if (!~argv.indexOf('-f') && !~argv.indexOf('--format')) { 160 if (win) { 161 log.verbose('gyp', 'gyp format was not specified; forcing "msvs"') 162 // force the 'make' target for non-Windows 163 argv.push('-f', 'msvs') 164 } else { 165 log.verbose('gyp', 'gyp format was not specified; forcing "make"') 166 // force the 'make' target for non-Windows 167 argv.push('-f', 'make') 168 } 169 } 170 171 // include all the ".gypi" files that were found 172 configs.forEach(function (config) { 173 argv.push('-I', config) 174 }) 175 176 // For AIX and z/OS we need to set up the path to the exports file 177 // which contains the symbols needed for linking. 178 var nodeExpFile 179 if (process.platform === 'aix' || process.platform === 'os390' || process.platform === 'os400') { 180 var ext = process.platform === 'os390' ? 'x' : 'exp' 181 var nodeRootDir = findNodeDirectory() 182 var candidates 183 184 if (process.platform === 'aix' || process.platform === 'os400') { 185 candidates = [ 186 'include/node/node', 187 'out/Release/node', 188 'out/Debug/node', 189 'node' 190 ].map(function (file) { 191 return file + '.' + ext 192 }) 193 } else { 194 candidates = [ 195 'out/Release/lib.target/libnode', 196 'out/Debug/lib.target/libnode', 197 'out/Release/obj.target/libnode', 198 'out/Debug/obj.target/libnode', 199 'lib/libnode' 200 ].map(function (file) { 201 return file + '.' + ext 202 }) 203 } 204 205 var logprefix = 'find exports file' 206 nodeExpFile = findAccessibleSync(logprefix, nodeRootDir, candidates) 207 if (nodeExpFile !== undefined) { 208 log.verbose(logprefix, 'Found exports file: %s', nodeExpFile) 209 } else { 210 var msg = msgFormat('Could not find node.%s file in %s', ext, nodeRootDir) 211 log.error(logprefix, 'Could not find exports file') 212 return callback(new Error(msg)) 213 } 214 } 215 216 // For z/OS we need to set up the path to zoslib include directory, 217 // which contains headers included in v8config.h. 218 var zoslibIncDir 219 if (process.platform === 'os390') { 220 logprefix = "find zoslib's zos-base.h:" 221 let msg 222 var zoslibIncPath = process.env.ZOSLIB_INCLUDES 223 if (zoslibIncPath) { 224 zoslibIncPath = findAccessibleSync(logprefix, zoslibIncPath, ['zos-base.h']) 225 if (zoslibIncPath === undefined) { 226 msg = msgFormat('Could not find zos-base.h file in the directory set ' + 227 'in ZOSLIB_INCLUDES environment variable: %s; set it ' + 228 'to the correct path, or unset it to search %s', process.env.ZOSLIB_INCLUDES, nodeRootDir) 229 } 230 } else { 231 candidates = [ 232 'include/node/zoslib/zos-base.h', 233 'include/zoslib/zos-base.h', 234 'zoslib/include/zos-base.h', 235 'install/include/node/zoslib/zos-base.h' 236 ] 237 zoslibIncPath = findAccessibleSync(logprefix, nodeRootDir, candidates) 238 if (zoslibIncPath === undefined) { 239 msg = msgFormat('Could not find any of %s in directory %s; set ' + 240 'environmant variable ZOSLIB_INCLUDES to the path ' + 241 'that contains zos-base.h', candidates.toString(), nodeRootDir) 242 } 243 } 244 if (zoslibIncPath !== undefined) { 245 zoslibIncDir = path.dirname(zoslibIncPath) 246 log.verbose(logprefix, "Found zoslib's zos-base.h in: %s", zoslibIncDir) 247 } else if (release.version.split('.')[0] >= 16) { 248 // zoslib is only shipped in Node v16 and above. 249 log.error(logprefix, msg) 250 return callback(new Error(msg)) 251 } 252 } 253 254 // this logic ported from the old `gyp_addon` python file 255 var gypScript = path.resolve(__dirname, '..', 'gyp', 'gyp_main.py') 256 var addonGypi = path.resolve(__dirname, '..', 'addon.gypi') 257 var commonGypi = path.resolve(nodeDir, 'include/node/common.gypi') 258 fs.stat(commonGypi, function (err) { 259 if (err) { 260 commonGypi = path.resolve(nodeDir, 'common.gypi') 261 } 262 263 var outputDir = 'build' 264 if (win) { 265 // Windows expects an absolute path 266 outputDir = buildDir 267 } 268 var nodeGypDir = path.resolve(__dirname, '..') 269 270 var nodeLibFile = path.join(nodeDir, 271 !gyp.opts.nodedir ? '<(target_arch)' : '$(Configuration)', 272 release.name + '.lib') 273 274 argv.push('-I', addonGypi) 275 argv.push('-I', commonGypi) 276 argv.push('-Dlibrary=shared_library') 277 argv.push('-Dvisibility=default') 278 argv.push('-Dnode_root_dir=' + nodeDir) 279 if (process.platform === 'aix' || process.platform === 'os390' || process.platform === 'os400') { 280 argv.push('-Dnode_exp_file=' + nodeExpFile) 281 if (process.platform === 'os390' && zoslibIncDir) { 282 argv.push('-Dzoslib_include_dir=' + zoslibIncDir) 283 } 284 } 285 argv.push('-Dnode_gyp_dir=' + nodeGypDir) 286 287 // Do this to keep Cygwin environments happy, else the unescaped '\' gets eaten up, 288 // resulting in bad paths, Ex c:parentFolderfolderanotherFolder instead of c:\parentFolder\folder\anotherFolder 289 if (win) { 290 nodeLibFile = nodeLibFile.replace(/\\/g, '\\\\') 291 } 292 argv.push('-Dnode_lib_file=' + nodeLibFile) 293 argv.push('-Dmodule_root_dir=' + process.cwd()) 294 argv.push('-Dnode_engine=' + 295 (gyp.opts.node_engine || process.jsEngine || 'v8')) 296 argv.push('--depth=.') 297 argv.push('--no-parallel') 298 299 // tell gyp to write the Makefile/Solution files into output_dir 300 argv.push('--generator-output', outputDir) 301 302 // tell make to write its output into the same dir 303 argv.push('-Goutput_dir=.') 304 305 // enforce use of the "binding.gyp" file 306 argv.unshift('binding.gyp') 307 308 // execute `gyp` from the current target nodedir 309 argv.unshift(gypScript) 310 311 // make sure python uses files that came with this particular node package 312 var pypath = [path.join(__dirname, '..', 'gyp', 'pylib')] 313 if (process.env.PYTHONPATH) { 314 pypath.push(process.env.PYTHONPATH) 315 } 316 process.env.PYTHONPATH = pypath.join(win ? ';' : ':') 317 318 var cp = gyp.spawn(python, argv) 319 cp.on('exit', onCpExit) 320 }) 321 } 322 323 function onCpExit (code) { 324 if (code !== 0) { 325 callback(new Error('`gyp` failed with exit code: ' + code)) 326 } else { 327 // we're done 328 callback() 329 } 330 } 331} 332 333/** 334 * Returns the first file or directory from an array of candidates that is 335 * readable by the current user, or undefined if none of the candidates are 336 * readable. 337 */ 338function findAccessibleSync (logprefix, dir, candidates) { 339 for (var next = 0; next < candidates.length; next++) { 340 var candidate = path.resolve(dir, candidates[next]) 341 try { 342 var fd = fs.openSync(candidate, 'r') 343 } catch (e) { 344 // this candidate was not found or not readable, do nothing 345 log.silly(logprefix, 'Could not open %s: %s', candidate, e.message) 346 continue 347 } 348 fs.closeSync(fd) 349 log.silly(logprefix, 'Found readable %s', candidate) 350 return candidate 351 } 352 353 return undefined 354} 355 356module.exports = configure 357module.exports.test = { 358 findAccessibleSync: findAccessibleSync 359} 360module.exports.usage = 'Generates ' + (win ? 'MSVC project files' : 'a Makefile') + ' for the current module' 361