configure.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. 'use strict'
  2. const { promises: fs, readFileSync } = require('graceful-fs')
  3. const path = require('path')
  4. const log = require('./log')
  5. const os = require('os')
  6. const processRelease = require('./process-release')
  7. const win = process.platform === 'win32'
  8. const findNodeDirectory = require('./find-node-directory')
  9. const { createConfigGypi } = require('./create-config-gypi')
  10. const { format: msgFormat } = require('util')
  11. const { findAccessibleSync } = require('./util')
  12. const { findPython } = require('./find-python')
  13. const { findVisualStudio } = win ? require('./find-visualstudio') : {}
  14. const majorRe = /^#define NODE_MAJOR_VERSION (\d+)/m
  15. const minorRe = /^#define NODE_MINOR_VERSION (\d+)/m
  16. const patchRe = /^#define NODE_PATCH_VERSION (\d+)/m
  17. async function configure (gyp, argv) {
  18. const buildDir = path.resolve('build')
  19. const configNames = ['config.gypi', 'common.gypi']
  20. const configs = []
  21. let nodeDir
  22. const release = processRelease(argv, gyp, process.version, process.release)
  23. const python = await findPython(gyp.opts.python)
  24. return getNodeDir()
  25. async function getNodeDir () {
  26. // 'python' should be set by now
  27. process.env.PYTHON = python
  28. if (!gyp.opts.nodedir &&
  29. process.config.variables.use_prefix_to_find_headers) {
  30. // check if the headers can be found using the prefix specified
  31. // at build time. Use them if they match the version expected
  32. const prefix = process.config.variables.node_prefix
  33. let availVersion
  34. try {
  35. const nodeVersionH = readFileSync(path.join(prefix,
  36. 'include', 'node', 'node_version.h'), { encoding: 'utf8' })
  37. const major = nodeVersionH.match(majorRe)[1]
  38. const minor = nodeVersionH.match(minorRe)[1]
  39. const patch = nodeVersionH.match(patchRe)[1]
  40. availVersion = major + '.' + minor + '.' + patch
  41. } catch {}
  42. if (availVersion === release.version) {
  43. // ok version matches, use the headers
  44. gyp.opts.nodedir = prefix
  45. log.verbose('using local node headers based on prefix',
  46. 'setting nodedir to ' + gyp.opts.nodedir)
  47. }
  48. }
  49. if (gyp.opts.nodedir) {
  50. // --nodedir was specified. use that for the dev files
  51. nodeDir = gyp.opts.nodedir.replace(/^~/, os.homedir())
  52. log.verbose('get node dir', 'compiling against specified --nodedir dev files: %s', nodeDir)
  53. } else {
  54. // if no --nodedir specified, ensure node dependencies are installed
  55. if ('v' + release.version !== process.version) {
  56. // if --target was given, then determine a target version to compile for
  57. log.verbose('get node dir', 'compiling against --target node version: %s', release.version)
  58. } else {
  59. // if no --target was specified then use the current host node version
  60. log.verbose('get node dir', 'no --target version specified, falling back to host node version: %s', release.version)
  61. }
  62. if (!release.semver) {
  63. // could not parse the version string with semver
  64. throw new Error('Invalid version number: ' + release.version)
  65. }
  66. // If the tarball option is set, always remove and reinstall the headers
  67. // into devdir. Otherwise only install if they're not already there.
  68. gyp.opts.ensure = !gyp.opts.tarball
  69. await gyp.commands.install([release.version])
  70. log.verbose('get node dir', 'target node version installed:', release.versionDir)
  71. nodeDir = path.resolve(gyp.devDir, release.versionDir)
  72. }
  73. return createBuildDir()
  74. }
  75. async function createBuildDir () {
  76. log.verbose('build dir', 'attempting to create "build" dir: %s', buildDir)
  77. const isNew = await fs.mkdir(buildDir, { recursive: true })
  78. log.verbose(
  79. 'build dir', '"build" dir needed to be created?', isNew ? 'Yes' : 'No'
  80. )
  81. if (win) {
  82. let usingMakeGenerator = false
  83. for (let i = argv.length - 1; i >= 0; --i) {
  84. const arg = argv[i]
  85. if (arg === '-f' || arg === '--format') {
  86. const format = argv[i + 1]
  87. if (typeof format === 'string' && format.startsWith('make')) {
  88. usingMakeGenerator = true
  89. break
  90. }
  91. } else if (arg.startsWith('--format=make')) {
  92. usingMakeGenerator = true
  93. break
  94. }
  95. }
  96. let vsInfo = {}
  97. if (!usingMakeGenerator) {
  98. vsInfo = await findVisualStudio(release.semver, gyp.opts['msvs-version'])
  99. }
  100. return createConfigFile(vsInfo)
  101. }
  102. return createConfigFile(null)
  103. }
  104. async function createConfigFile (vsInfo) {
  105. if (win) {
  106. process.env.GYP_MSVS_VERSION = Math.min(vsInfo.versionYear, 2015)
  107. process.env.GYP_MSVS_OVERRIDE_PATH = vsInfo.path
  108. }
  109. const configPath = await createConfigGypi({ gyp, buildDir, nodeDir, vsInfo, python })
  110. configs.push(configPath)
  111. return findConfigs()
  112. }
  113. async function findConfigs () {
  114. const name = configNames.shift()
  115. if (!name) {
  116. return runGyp()
  117. }
  118. const fullPath = path.resolve(name)
  119. log.verbose(name, 'checking for gypi file: %s', fullPath)
  120. try {
  121. await fs.stat(fullPath)
  122. log.verbose(name, 'found gypi file')
  123. configs.push(fullPath)
  124. } catch (err) {
  125. // ENOENT will check next gypi filename
  126. if (err.code !== 'ENOENT') {
  127. throw err
  128. }
  129. }
  130. return findConfigs()
  131. }
  132. async function runGyp () {
  133. if (!~argv.indexOf('-f') && !~argv.indexOf('--format')) {
  134. if (win) {
  135. log.verbose('gyp', 'gyp format was not specified; forcing "msvs"')
  136. // force the 'make' target for non-Windows
  137. argv.push('-f', 'msvs')
  138. } else {
  139. log.verbose('gyp', 'gyp format was not specified; forcing "make"')
  140. // force the 'make' target for non-Windows
  141. argv.push('-f', 'make')
  142. }
  143. }
  144. // include all the ".gypi" files that were found
  145. configs.forEach(function (config) {
  146. argv.push('-I', config)
  147. })
  148. // For AIX and z/OS we need to set up the path to the exports file
  149. // which contains the symbols needed for linking.
  150. let nodeExpFile
  151. let nodeRootDir
  152. let candidates
  153. let logprefix = 'find exports file'
  154. if (process.platform === 'aix' || process.platform === 'os390' || process.platform === 'os400') {
  155. const ext = process.platform === 'os390' ? 'x' : 'exp'
  156. nodeRootDir = findNodeDirectory()
  157. if (process.platform === 'aix' || process.platform === 'os400') {
  158. candidates = [
  159. 'include/node/node',
  160. 'out/Release/node',
  161. 'out/Debug/node',
  162. 'node'
  163. ].map(function (file) {
  164. return file + '.' + ext
  165. })
  166. } else {
  167. candidates = [
  168. 'out/Release/lib.target/libnode',
  169. 'out/Debug/lib.target/libnode',
  170. 'out/Release/obj.target/libnode',
  171. 'out/Debug/obj.target/libnode',
  172. 'lib/libnode'
  173. ].map(function (file) {
  174. return file + '.' + ext
  175. })
  176. }
  177. nodeExpFile = findAccessibleSync(logprefix, nodeRootDir, candidates)
  178. if (nodeExpFile !== undefined) {
  179. log.verbose(logprefix, 'Found exports file: %s', nodeExpFile)
  180. } else {
  181. const msg = msgFormat('Could not find node.%s file in %s', ext, nodeRootDir)
  182. log.error(logprefix, 'Could not find exports file')
  183. throw new Error(msg)
  184. }
  185. }
  186. // For z/OS we need to set up the path to zoslib include directory,
  187. // which contains headers included in v8config.h.
  188. let zoslibIncDir
  189. if (process.platform === 'os390') {
  190. logprefix = "find zoslib's zos-base.h:"
  191. let msg
  192. let zoslibIncPath = process.env.ZOSLIB_INCLUDES
  193. if (zoslibIncPath) {
  194. zoslibIncPath = findAccessibleSync(logprefix, zoslibIncPath, ['zos-base.h'])
  195. if (zoslibIncPath === undefined) {
  196. msg = msgFormat('Could not find zos-base.h file in the directory set ' +
  197. 'in ZOSLIB_INCLUDES environment variable: %s; set it ' +
  198. 'to the correct path, or unset it to search %s', process.env.ZOSLIB_INCLUDES, nodeRootDir)
  199. }
  200. } else {
  201. candidates = [
  202. 'include/node/zoslib/zos-base.h',
  203. 'include/zoslib/zos-base.h',
  204. 'zoslib/include/zos-base.h',
  205. 'install/include/node/zoslib/zos-base.h'
  206. ]
  207. zoslibIncPath = findAccessibleSync(logprefix, nodeRootDir, candidates)
  208. if (zoslibIncPath === undefined) {
  209. msg = msgFormat('Could not find any of %s in directory %s; set ' +
  210. 'environmant variable ZOSLIB_INCLUDES to the path ' +
  211. 'that contains zos-base.h', candidates.toString(), nodeRootDir)
  212. }
  213. }
  214. if (zoslibIncPath !== undefined) {
  215. zoslibIncDir = path.dirname(zoslibIncPath)
  216. log.verbose(logprefix, "Found zoslib's zos-base.h in: %s", zoslibIncDir)
  217. } else if (release.version.split('.')[0] >= 16) {
  218. // zoslib is only shipped in Node v16 and above.
  219. log.error(logprefix, msg)
  220. throw new Error(msg)
  221. }
  222. }
  223. // this logic ported from the old `gyp_addon` python file
  224. const gypScript = path.resolve(__dirname, '..', 'gyp', 'gyp_main.py')
  225. const addonGypi = path.resolve(__dirname, '..', 'addon.gypi')
  226. let commonGypi = path.resolve(nodeDir, 'include/node/common.gypi')
  227. try {
  228. await fs.stat(commonGypi)
  229. } catch (err) {
  230. commonGypi = path.resolve(nodeDir, 'common.gypi')
  231. }
  232. let outputDir = 'build'
  233. if (win) {
  234. // Windows expects an absolute path
  235. outputDir = buildDir
  236. }
  237. const nodeGypDir = path.resolve(__dirname, '..')
  238. let nodeLibFile = path.join(nodeDir,
  239. !gyp.opts.nodedir ? '<(target_arch)' : '$(Configuration)',
  240. release.name + '.lib')
  241. argv.push('-I', addonGypi)
  242. argv.push('-I', commonGypi)
  243. argv.push('-Dlibrary=shared_library')
  244. argv.push('-Dvisibility=default')
  245. argv.push('-Dnode_root_dir=' + nodeDir)
  246. if (process.platform === 'aix' || process.platform === 'os390' || process.platform === 'os400') {
  247. argv.push('-Dnode_exp_file=' + nodeExpFile)
  248. if (process.platform === 'os390' && zoslibIncDir) {
  249. argv.push('-Dzoslib_include_dir=' + zoslibIncDir)
  250. }
  251. }
  252. argv.push('-Dnode_gyp_dir=' + nodeGypDir)
  253. // Do this to keep Cygwin environments happy, else the unescaped '\' gets eaten up,
  254. // resulting in bad paths, Ex c:parentFolderfolderanotherFolder instead of c:\parentFolder\folder\anotherFolder
  255. if (win) {
  256. nodeLibFile = nodeLibFile.replace(/\\/g, '\\\\')
  257. }
  258. argv.push('-Dnode_lib_file=' + nodeLibFile)
  259. argv.push('-Dmodule_root_dir=' + process.cwd())
  260. argv.push('-Dnode_engine=' +
  261. (gyp.opts.node_engine || process.jsEngine || 'v8'))
  262. argv.push('--depth=.')
  263. argv.push('--no-parallel')
  264. // tell gyp to write the Makefile/Solution files into output_dir
  265. argv.push('--generator-output', outputDir)
  266. // tell make to write its output into the same dir
  267. argv.push('-Goutput_dir=.')
  268. // enforce use of the "binding.gyp" file
  269. argv.unshift('binding.gyp')
  270. // execute `gyp` from the current target nodedir
  271. argv.unshift(gypScript)
  272. // make sure python uses files that came with this particular node package
  273. const pypath = [path.join(__dirname, '..', 'gyp', 'pylib')]
  274. if (process.env.PYTHONPATH) {
  275. pypath.push(process.env.PYTHONPATH)
  276. }
  277. process.env.PYTHONPATH = pypath.join(win ? ';' : ':')
  278. await new Promise((resolve, reject) => {
  279. const cp = gyp.spawn(python, argv)
  280. cp.on('exit', (code) => {
  281. if (code !== 0) {
  282. reject(new Error('`gyp` failed with exit code: ' + code))
  283. } else {
  284. // we're done
  285. resolve()
  286. }
  287. })
  288. })
  289. }
  290. }
  291. module.exports = configure
  292. module.exports.usage = 'Generates ' + (win ? 'MSVC project files' : 'a Makefile') + ' for the current module'