Initial commit
[yaffs-website] / node_modules / node-gyp / lib / configure.js
1 module.exports = exports = configure
2 module.exports.test = {
3   PythonFinder: PythonFinder,
4   findAccessibleSync: findAccessibleSync,
5   findPython: findPython,
6 }
7
8 /**
9  * Module dependencies.
10  */
11
12 var fs = require('graceful-fs')
13   , path = require('path')
14   , log = require('npmlog')
15   , osenv = require('osenv')
16   , which = require('which')
17   , semver = require('semver')
18   , mkdirp = require('mkdirp')
19   , cp = require('child_process')
20   , extend = require('util')._extend
21   , processRelease = require('./process-release')
22   , win = process.platform == 'win32'
23   , findNodeDirectory = require('./find-node-directory')
24   , msgFormat = require('util').format
25
26 exports.usage = 'Generates ' + (win ? 'MSVC project files' : 'a Makefile') + ' for the current module'
27
28 function configure (gyp, argv, callback) {
29
30   var python = gyp.opts.python || process.env.PYTHON || 'python2'
31     , buildDir = path.resolve('build')
32     , configNames = [ 'config.gypi', 'common.gypi' ]
33     , configs = []
34     , nodeDir
35     , release = processRelease(argv, gyp, process.version, process.release)
36
37   findPython(python, function (err, found) {
38     if (err) {
39       callback(err)
40     } else {
41       python = found
42       getNodeDir()
43     }
44   })
45
46   function getNodeDir () {
47
48     // 'python' should be set by now
49     process.env.PYTHON = python
50
51     if (gyp.opts.nodedir) {
52       // --nodedir was specified. use that for the dev files
53       nodeDir = gyp.opts.nodedir.replace(/^~/, osenv.home())
54
55       log.verbose('get node dir', 'compiling against specified --nodedir dev files: %s', nodeDir)
56       createBuildDir()
57
58     } else {
59       // if no --nodedir specified, ensure node dependencies are installed
60       if ('v' + release.version !== process.version) {
61         // if --target was given, then determine a target version to compile for
62         log.verbose('get node dir', 'compiling against --target node version: %s', release.version)
63       } else {
64         // if no --target was specified then use the current host node version
65         log.verbose('get node dir', 'no --target version specified, falling back to host node version: %s', release.version)
66       }
67
68       if (!release.semver) {
69         // could not parse the version string with semver
70         return callback(new Error('Invalid version number: ' + release.version))
71       }
72
73       // ensure that the target node version's dev files are installed
74       gyp.opts.ensure = true
75       gyp.commands.install([ release.version ], function (err, version) {
76         if (err) return callback(err)
77         log.verbose('get node dir', 'target node version installed:', release.versionDir)
78         nodeDir = path.resolve(gyp.devDir, release.versionDir)
79         createBuildDir()
80       })
81     }
82   }
83
84   function createBuildDir () {
85     log.verbose('build dir', 'attempting to create "build" dir: %s', buildDir)
86     mkdirp(buildDir, function (err, isNew) {
87       if (err) return callback(err)
88       log.verbose('build dir', '"build" dir needed to be created?', isNew)
89       createConfigFile()
90     })
91   }
92
93   function createConfigFile (err) {
94     if (err) return callback(err)
95
96     var configFilename = 'config.gypi'
97     var configPath = path.resolve(buildDir, configFilename)
98
99     log.verbose('build/' + configFilename, 'creating config file')
100
101     var config = process.config || {}
102       , defaults = config.target_defaults
103       , variables = config.variables
104
105     // default "config.variables"
106     if (!variables) variables = config.variables = {}
107
108     // default "config.defaults"
109     if (!defaults) defaults = config.target_defaults = {}
110
111     // don't inherit the "defaults" from node's `process.config` object.
112     // doing so could cause problems in cases where the `node` executable was
113     // compiled on a different machine (with different lib/include paths) than
114     // the machine where the addon is being built to
115     defaults.cflags = []
116     defaults.defines = []
117     defaults.include_dirs = []
118     defaults.libraries = []
119
120     // set the default_configuration prop
121     if ('debug' in gyp.opts) {
122       defaults.default_configuration = gyp.opts.debug ? 'Debug' : 'Release'
123     }
124     if (!defaults.default_configuration) {
125       defaults.default_configuration = 'Release'
126     }
127
128     // set the target_arch variable
129     variables.target_arch = gyp.opts.arch || process.arch || 'ia32'
130
131     // set the node development directory
132     variables.nodedir = nodeDir
133
134     // don't copy dev libraries with nodedir option
135     variables.copy_dev_lib = !gyp.opts.nodedir
136
137     // disable -T "thin" static archives by default
138     variables.standalone_static_library = gyp.opts.thin ? 0 : 1
139
140     // loop through the rest of the opts and add the unknown ones as variables.
141     // this allows for module-specific configure flags like:
142     //
143     //   $ node-gyp configure --shared-libxml2
144     Object.keys(gyp.opts).forEach(function (opt) {
145       if (opt === 'argv') return
146       if (opt in gyp.configDefs) return
147       variables[opt.replace(/-/g, '_')] = gyp.opts[opt]
148     })
149
150     // ensures that any boolean values from `process.config` get stringified
151     function boolsToString (k, v) {
152       if (typeof v === 'boolean')
153         return String(v)
154       return v
155     }
156
157     log.silly('build/' + configFilename, config)
158
159     // now write out the config.gypi file to the build/ dir
160     var prefix = '# Do not edit. File was generated by node-gyp\'s "configure" step'
161       , json = JSON.stringify(config, boolsToString, 2)
162     log.verbose('build/' + configFilename, 'writing out config file: %s', configPath)
163     configs.push(configPath)
164     fs.writeFile(configPath, [prefix, json, ''].join('\n'), findConfigs)
165   }
166
167   function findConfigs (err) {
168     if (err) return callback(err)
169     var name = configNames.shift()
170     if (!name) return runGyp()
171     var fullPath = path.resolve(name)
172     log.verbose(name, 'checking for gypi file: %s', fullPath)
173     fs.stat(fullPath, function (err, stat) {
174       if (err) {
175         if (err.code == 'ENOENT') {
176           findConfigs() // check next gypi filename
177         } else {
178           callback(err)
179         }
180       } else {
181         log.verbose(name, 'found gypi file')
182         configs.push(fullPath)
183         findConfigs()
184       }
185     })
186   }
187
188   function runGyp (err) {
189     if (err) return callback(err)
190
191     if (!~argv.indexOf('-f') && !~argv.indexOf('--format')) {
192       if (win) {
193         log.verbose('gyp', 'gyp format was not specified; forcing "msvs"')
194         // force the 'make' target for non-Windows
195         argv.push('-f', 'msvs')
196       } else {
197         log.verbose('gyp', 'gyp format was not specified; forcing "make"')
198         // force the 'make' target for non-Windows
199         argv.push('-f', 'make')
200       }
201     }
202
203     function hasMsvsVersion () {
204       return argv.some(function (arg) {
205         return arg.indexOf('msvs_version') === 0
206       })
207     }
208
209     if (win && !hasMsvsVersion()) {
210       if ('msvs_version' in gyp.opts) {
211         argv.push('-G', 'msvs_version=' + gyp.opts.msvs_version)
212       } else {
213         argv.push('-G', 'msvs_version=auto')
214       }
215     }
216
217     // include all the ".gypi" files that were found
218     configs.forEach(function (config) {
219       argv.push('-I', config)
220     })
221
222     // for AIX we need to set up the path to the exp file
223     // which contains the symbols needed for linking.
224     // The file will either be in one of the following
225     // depending on whether it is an installed or
226     // development environment:
227     //  - the include/node directory
228     //  - the out/Release directory
229     //  - the out/Debug directory
230     //  - the root directory
231     var node_exp_file = undefined
232     if (process.platform === 'aix') {
233       var node_root_dir = findNodeDirectory()
234       var candidates = ['include/node/node.exp',
235                         'out/Release/node.exp',
236                         'out/Debug/node.exp',
237                         'node.exp']
238       var logprefix = 'find exports file'
239       node_exp_file = findAccessibleSync(logprefix, node_root_dir, candidates)
240       if (node_exp_file !== undefined) {
241         log.verbose(logprefix, 'Found exports file: %s', node_exp_file)
242       } else {
243         var msg = msgFormat('Could not find node.exp file in %s', node_root_dir)
244         log.error(logprefix, 'Could not find exports file')
245         return callback(new Error(msg))
246       }
247     }
248
249     // this logic ported from the old `gyp_addon` python file
250     var gyp_script = path.resolve(__dirname, '..', 'gyp', 'gyp_main.py')
251     var addon_gypi = path.resolve(__dirname, '..', 'addon.gypi')
252     var common_gypi = path.resolve(nodeDir, 'include/node/common.gypi')
253     fs.stat(common_gypi, function (err, stat) {
254       if (err)
255         common_gypi = path.resolve(nodeDir, 'common.gypi')
256
257       var output_dir = 'build'
258       if (win) {
259         // Windows expects an absolute path
260         output_dir = buildDir
261       }
262       var nodeGypDir = path.resolve(__dirname, '..')
263
264       argv.push('-I', addon_gypi)
265       argv.push('-I', common_gypi)
266       argv.push('-Dlibrary=shared_library')
267       argv.push('-Dvisibility=default')
268       argv.push('-Dnode_root_dir=' + nodeDir)
269       if (process.platform === 'aix') {
270         argv.push('-Dnode_exp_file=' + node_exp_file)
271       }
272       argv.push('-Dnode_gyp_dir=' + nodeGypDir)
273       argv.push('-Dnode_lib_file=' + release.name + '.lib')
274       argv.push('-Dmodule_root_dir=' + process.cwd())
275       argv.push('--depth=.')
276       argv.push('--no-parallel')
277
278       // tell gyp to write the Makefile/Solution files into output_dir
279       argv.push('--generator-output', output_dir)
280
281       // tell make to write its output into the same dir
282       argv.push('-Goutput_dir=.')
283
284       // enforce use of the "binding.gyp" file
285       argv.unshift('binding.gyp')
286
287       // execute `gyp` from the current target nodedir
288       argv.unshift(gyp_script)
289
290       // make sure python uses files that came with this particular node package
291       var pypath = [path.join(__dirname, '..', 'gyp', 'pylib')]
292       if (process.env.PYTHONPATH) {
293         pypath.push(process.env.PYTHONPATH)
294       }
295       process.env.PYTHONPATH = pypath.join(win ? ';' : ':')
296
297       var cp = gyp.spawn(python, argv)
298       cp.on('exit', onCpExit)
299     })
300   }
301
302   /**
303    * Called when the `gyp` child process exits.
304    */
305
306   function onCpExit (code, signal) {
307     if (code !== 0) {
308       callback(new Error('`gyp` failed with exit code: ' + code))
309     } else {
310       // we're done
311       callback()
312     }
313   }
314
315 }
316
317 /**
318  * Returns the first file or directory from an array of candidates that is 
319  * readable by the current user, or undefined if none of the candidates are
320  * readable. 
321  */
322 function findAccessibleSync (logprefix, dir, candidates) {
323   for (var next = 0; next < candidates.length; next++) {
324      var candidate = path.resolve(dir, candidates[next])
325      try {
326        var fd = fs.openSync(candidate, 'r')
327      } catch (e) {
328        // this candidate was not found or not readable, do nothing
329        log.silly(logprefix, 'Could not open %s: %s', candidate, e.message)
330        continue
331      }
332      fs.closeSync(fd)
333      log.silly(logprefix, 'Found readable %s', candidate)
334      return candidate
335   }
336
337   return undefined
338 }
339
340 function PythonFinder(python, callback) {
341   this.callback = callback
342   this.python = python
343 }
344
345 PythonFinder.prototype = {
346   checkPythonLauncherDepth: 0,
347   env: process.env,
348   execFile: cp.execFile,
349   log: log,
350   stat: fs.stat,
351   which: which,
352   win: win,
353
354   checkPython: function checkPython () {
355     this.log.verbose('check python',
356                      'checking for Python executable "%s" in the PATH',
357                      this.python)
358     this.which(this.python, function (err, execPath) {
359       if (err) {
360         this.log.verbose('`which` failed', this.python, err)
361         if (this.python === 'python2') {
362           this.python = 'python'
363           return this.checkPython()
364         }
365         if (this.win) {
366           this.checkPythonLauncher()
367         } else {
368           this.failNoPython()
369         }
370       } else {
371         this.log.verbose('`which` succeeded', this.python, execPath)
372         // Found the `python` executable, and from now on we use it explicitly.
373         // This solves #667 and #750 (`execFile` won't run batch files
374         // (*.cmd, and *.bat))
375         this.python = execPath
376         this.checkPythonVersion()
377       }
378     }.bind(this))
379   },
380
381   // Distributions of Python on Windows by default install with the "py.exe"
382   // Python launcher which is more likely to exist than the Python executable
383   // being in the $PATH.
384   // Because the Python launcher supports all versions of Python, we have to
385   // explicitly request a Python 2 version. This is done by supplying "-2" as
386   // the first command line argument. Since "py.exe -2" would be an invalid
387   // executable for "execFile", we have to use the launcher to figure out
388   // where the actual "python.exe" executable is located.
389   checkPythonLauncher: function checkPythonLauncher () {
390     this.checkPythonLauncherDepth += 1
391
392     this.log.verbose(
393         'could not find "' + this.python + '". checking python launcher')
394     var env = extend({}, this.env)
395     env.TERM = 'dumb'
396
397     var launcherArgs = ['-2', '-c', 'import sys; print sys.executable']
398     this.execFile('py.exe', launcherArgs, { env: env }, function (err, stdout) {
399       if (err) {
400         this.guessPython()
401       } else {
402         this.python = stdout.trim()
403         this.log.verbose('check python launcher',
404                          'python executable found: %j',
405                          this.python)
406         this.checkPythonVersion()
407       }
408       this.checkPythonLauncherDepth -= 1
409     }.bind(this))
410   },
411
412   checkPythonVersion: function checkPythonVersion () {
413     var args = ['-c', 'import platform; print(platform.python_version());']
414     var env = extend({}, this.env)
415     env.TERM = 'dumb'
416
417     this.execFile(this.python, args, { env: env }, function (err, stdout) {
418       if (err) {
419         return this.callback(err)
420       }
421       this.log.verbose('check python version',
422                        '`%s -c "' + args[1] + '"` returned: %j',
423                        this.python, stdout)
424       var version = stdout.trim()
425       if (~version.indexOf('+')) {
426         this.log.silly('stripping "+" sign(s) from version')
427         version = version.replace(/\+/g, '')
428       }
429       if (~version.indexOf('rc')) {
430         this.log.silly('stripping "rc" identifier from version')
431         version = version.replace(/rc(.*)$/ig, '')
432       }
433       var range = semver.Range('>=2.5.0 <3.0.0')
434       var valid = false
435       try {
436         valid = range.test(version)
437       } catch (e) {
438         this.log.silly('range.test() error', e)
439       }
440       if (valid) {
441         this.callback(null, this.python)
442       } else if (this.win && this.checkPythonLauncherDepth === 0) {
443         this.checkPythonLauncher()
444       } else {
445         this.failPythonVersion(version)
446       }
447     }.bind(this))
448   },
449
450   failNoPython: function failNoPython () {
451     var errmsg =
452         'Can\'t find Python executable "' + this.python +
453         '", you can set the PYTHON env variable.'
454     this.callback(new Error(errmsg))
455   },
456
457   failPythonVersion: function failPythonVersion (badVersion) {
458     var errmsg =
459         'Python executable "' + this.python +
460         '" is v' + badVersion + ', which is not supported by gyp.\n' +
461         'You can pass the --python switch to point to ' +
462         'Python >= v2.5.0 & < 3.0.0.'
463     this.callback(new Error(errmsg))
464   },
465
466   // Called on Windows when "python" isn't available in the current $PATH.
467   // We are going to check if "%SystemDrive%\python27\python.exe" exists.
468   guessPython: function guessPython () {
469     this.log.verbose('could not find "' + this.python + '". guessing location')
470     var rootDir = this.env.SystemDrive || 'C:\\'
471     if (rootDir[rootDir.length - 1] !== '\\') {
472       rootDir += '\\'
473     }
474     var resolve = path.win32 && path.win32.resolve || path.resolve
475     var pythonPath = resolve(rootDir, 'Python27', 'python.exe')
476     this.log.verbose('ensuring that file exists:', pythonPath)
477     this.stat(pythonPath, function (err, stat) {
478       if (err) {
479         if (err.code == 'ENOENT') {
480           this.failNoPython()
481         } else {
482           this.callback(err)
483         }
484         return
485       }
486       this.python = pythonPath
487       this.checkPythonVersion()
488     }.bind(this))
489   },
490 }
491
492 function findPython (python, callback) {
493   var finder = new PythonFinder(python, callback)
494   finder.checkPython()
495 }