1 module.exports = exports = configure
7 var fs = require('graceful-fs')
8 , path = require('path')
9 , glob = require('glob')
10 , log = require('npmlog')
11 , osenv = require('osenv')
12 , which = require('which')
13 , semver = require('semver')
14 , mkdirp = require('mkdirp')
15 , cp = require('child_process')
16 , PathArray = require('path-array')
17 , extend = require('util')._extend
19 , execFile = cp.execFile
20 , win = process.platform == 'win32'
22 exports.usage = 'Generates ' + (win ? 'MSVC project files' : 'a Makefile') + ' for the current module'
24 function configure (gyp, argv, callback) {
26 var python = gyp.opts.python || process.env.PYTHON || 'python2'
27 , buildDir = path.resolve('build')
28 , configNames = [ 'config.gypi', 'common.gypi' ]
34 // Check if Python is in the $PATH
35 function checkPython () {
36 log.verbose('check python', 'checking for Python executable "%s" in the PATH', python)
37 which(python, function (err, execPath) {
39 log.verbose('`which` failed', python, err)
40 if (python === 'python2') {
50 log.verbose('`which` succeeded', python, execPath)
56 // Called on Windows when "python" isn't available in the current $PATH.
57 // We're gonna check if "%SystemDrive%\python27\python.exe" exists.
58 function guessPython () {
59 log.verbose('could not find "' + python + '". guessing location')
60 var rootDir = process.env.SystemDrive || 'C:\\'
61 if (rootDir[rootDir.length - 1] !== '\\') {
64 var pythonPath = path.resolve(rootDir, 'Python27', 'python.exe')
65 log.verbose('ensuring that file exists:', pythonPath)
66 fs.stat(pythonPath, function (err, stat) {
68 if (err.code == 'ENOENT') {
80 function checkPythonVersion () {
81 var env = extend({}, process.env)
84 execFile(python, ['-c', 'import platform; print(platform.python_version());'], { env: env }, function (err, stdout) {
88 log.verbose('check python version', '`%s -c "import platform; print(platform.python_version());"` returned: %j', python, stdout)
89 var version = stdout.trim()
90 if (~version.indexOf('+')) {
91 log.silly('stripping "+" sign(s) from version')
92 version = version.replace(/\+/g, '')
94 if (~version.indexOf('rc')) {
95 log.silly('stripping "rc" identifier from version')
96 version = version.replace(/rc(.*)$/ig, '')
98 var range = semver.Range('>=2.5.0 <3.0.0')
101 valid = range.test(version)
103 log.silly('range.test() error', e)
108 failPythonVersion(version)
113 function failNoPython () {
114 callback(new Error('Can\'t find Python executable "' + python +
115 '", you can set the PYTHON env variable.'))
118 function failPythonVersion (badVersion) {
119 callback(new Error('Python executable "' + python +
120 '" is v' + badVersion + ', which is not supported by gyp.\n' +
121 'You can pass the --python switch to point to Python >= v2.5.0 & < 3.0.0.'))
124 function getNodeDir () {
126 // 'python' should be set by now
127 process.env.PYTHON = python
129 if (gyp.opts.nodedir) {
130 // --nodedir was specified. use that for the dev files
131 nodeDir = gyp.opts.nodedir.replace(/^~/, osenv.home())
133 log.verbose('get node dir', 'compiling against specified --nodedir dev files: %s', nodeDir)
137 // if no --nodedir specified, ensure node dependencies are installed
141 if (gyp.opts.target) {
142 // if --target was given, then determine a target version to compile for
143 versionStr = gyp.opts.target
144 log.verbose('get node dir', 'compiling against --target node version: %s', versionStr)
146 // if no --target was specified then use the current host node version
147 versionStr = process.version
148 log.verbose('get node dir', 'no --target version specified, falling back to host node version: %s', versionStr)
151 // make sure we have a valid version
153 version = semver.parse(versionStr)
158 return callback(new Error('Invalid version number: ' + versionStr))
161 // ensure that the target node version's dev files are installed
162 gyp.opts.ensure = true
163 gyp.commands.install([ versionStr ], function (err, version) {
164 if (err) return callback(err)
165 log.verbose('get node dir', 'target node version installed:', version)
166 nodeDir = path.resolve(gyp.devDir, version)
172 function createBuildDir () {
173 log.verbose('build dir', 'attempting to create "build" dir: %s', buildDir)
174 mkdirp(buildDir, function (err, isNew) {
175 if (err) return callback(err)
176 log.verbose('build dir', '"build" dir needed to be created?', isNew)
181 function createConfigFile (err) {
182 if (err) return callback(err)
184 var configFilename = 'config.gypi'
185 var configPath = path.resolve(buildDir, configFilename)
187 log.verbose('build/' + configFilename, 'creating config file')
189 var config = process.config || {}
190 , defaults = config.target_defaults
191 , variables = config.variables
193 // default "config.variables"
194 if (!variables) variables = config.variables = {}
196 // default "config.defaults"
197 if (!defaults) defaults = config.target_defaults = {}
199 // don't inherit the "defaults" from node's `process.config` object.
200 // doing so could cause problems in cases where the `node` executable was
201 // compiled on a different machine (with different lib/include paths) than
202 // the machine where the addon is being built to
204 defaults.defines = []
205 defaults.include_dirs = []
206 defaults.libraries = []
208 // set the default_configuration prop
209 if ('debug' in gyp.opts) {
210 defaults.default_configuration = gyp.opts.debug ? 'Debug' : 'Release'
212 if (!defaults.default_configuration) {
213 defaults.default_configuration = 'Release'
216 // set the target_arch variable
217 variables.target_arch = gyp.opts.arch || process.arch || 'ia32'
219 // set the node development directory
220 variables.nodedir = nodeDir
222 // don't copy dev libraries with nodedir option
223 variables.copy_dev_lib = !gyp.opts.nodedir
225 // disable -T "thin" static archives by default
226 variables.standalone_static_library = gyp.opts.thin ? 0 : 1
228 // loop through the rest of the opts and add the unknown ones as variables.
229 // this allows for module-specific configure flags like:
231 // $ node-gyp configure --shared-libxml2
232 Object.keys(gyp.opts).forEach(function (opt) {
233 if (opt === 'argv') return
234 if (opt in gyp.configDefs) return
235 variables[opt.replace(/-/g, '_')] = gyp.opts[opt]
238 // ensures that any boolean values from `process.config` get stringified
239 function boolsToString (k, v) {
240 if (typeof v === 'boolean')
245 log.silly('build/' + configFilename, config)
247 // now write out the config.gypi file to the build/ dir
248 var prefix = '# Do not edit. File was generated by node-gyp\'s "configure" step'
249 , json = JSON.stringify(config, boolsToString, 2)
250 log.verbose('build/' + configFilename, 'writing out config file: %s', configPath)
251 configs.push(configPath)
252 fs.writeFile(configPath, [prefix, json, ''].join('\n'), findConfigs)
255 function findConfigs (err) {
256 if (err) return callback(err)
257 var name = configNames.shift()
258 if (!name) return runGyp()
259 var fullPath = path.resolve(name)
260 log.verbose(name, 'checking for gypi file: %s', fullPath)
261 fs.stat(fullPath, function (err, stat) {
263 if (err.code == 'ENOENT') {
264 findConfigs() // check next gypi filename
269 log.verbose(name, 'found gypi file')
270 configs.push(fullPath)
276 function runGyp (err) {
277 if (err) return callback(err)
279 if (!~argv.indexOf('-f') && !~argv.indexOf('--format')) {
281 log.verbose('gyp', 'gyp format was not specified; forcing "msvs"')
282 // force the 'make' target for non-Windows
283 argv.push('-f', 'msvs')
285 log.verbose('gyp', 'gyp format was not specified; forcing "make"')
286 // force the 'make' target for non-Windows
287 argv.push('-f', 'make')
291 function hasMsvsVersion () {
292 return argv.some(function (arg) {
293 return arg.indexOf('msvs_version') === 0
297 if (win && !hasMsvsVersion()) {
298 if ('msvs_version' in gyp.opts) {
299 argv.push('-G', 'msvs_version=' + gyp.opts.msvs_version)
301 argv.push('-G', 'msvs_version=auto')
305 // include all the ".gypi" files that were found
306 configs.forEach(function (config) {
307 argv.push('-I', config)
310 // this logic ported from the old `gyp_addon` python file
311 var gyp_script = path.resolve(__dirname, '..', 'gyp', 'gyp_main.py')
312 var addon_gypi = path.resolve(__dirname, '..', 'addon.gypi')
313 var common_gypi = path.resolve(nodeDir, 'include/node/common.gypi')
314 //TODO: ideally async
315 if (!fs.existsSync(common_gypi))
316 common_gypi = path.resolve(nodeDir, 'common.gypi')
318 var output_dir = 'build'
320 // Windows expects an absolute path
321 output_dir = buildDir
323 var nodeGypDir = path.resolve(__dirname, '..')
325 argv.push('-I', addon_gypi)
326 argv.push('-I', common_gypi)
327 argv.push('-Dlibrary=shared_library')
328 argv.push('-Dvisibility=default')
329 argv.push('-Dnode_root_dir=' + nodeDir)
330 argv.push('-Dnode_gyp_dir=' + nodeGypDir)
331 argv.push('-Dmodule_root_dir=' + process.cwd())
332 argv.push('--depth=.')
333 argv.push('--no-parallel')
335 // tell gyp to write the Makefile/Solution files into output_dir
336 argv.push('--generator-output', output_dir)
338 // tell make to write its output into the same dir
339 argv.push('-Goutput_dir=.')
341 // enforce use of the "binding.gyp" file
342 argv.unshift('binding.gyp')
344 // execute `gyp` from the current target nodedir
345 argv.unshift(gyp_script)
347 // make sure python uses files that came with this particular node package
348 var pypath = new PathArray(process.env, 'PYTHONPATH')
349 pypath.unshift(path.join(__dirname, '..', 'gyp', 'pylib'))
351 var cp = gyp.spawn(python, argv)
352 cp.on('exit', onCpExit)
356 * Called when the `gyp` child process exits.
359 function onCpExit (code, signal) {
361 callback(new Error('`gyp` failed with exit code: ' + code))