• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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