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