1'use strict'
2
3const fs = require('graceful-fs').promises
4const path = require('path')
5const { glob } = require('glob')
6const log = require('./log')
7const which = require('which')
8const win = process.platform === 'win32'
9
10async function build (gyp, argv) {
11  let platformMake = 'make'
12  if (process.platform === 'aix') {
13    platformMake = 'gmake'
14  } else if (process.platform === 'os400') {
15    platformMake = 'gmake'
16  } else if (process.platform.indexOf('bsd') !== -1) {
17    platformMake = 'gmake'
18  } else if (win && argv.length > 0) {
19    argv = argv.map(function (target) {
20      return '/t:' + target
21    })
22  }
23
24  const makeCommand = gyp.opts.make || process.env.MAKE || platformMake
25  let command = win ? 'msbuild' : makeCommand
26  const jobs = gyp.opts.jobs || process.env.JOBS
27  let buildType
28  let config
29  let arch
30  let nodeDir
31  let guessedSolution
32  let python
33  let buildBinsDir
34
35  await loadConfigGypi()
36
37  /**
38   * Load the "config.gypi" file that was generated during "configure".
39   */
40
41  async function loadConfigGypi () {
42    let data
43    try {
44      const configPath = path.resolve('build', 'config.gypi')
45      data = await fs.readFile(configPath, 'utf8')
46    } catch (err) {
47      if (err.code === 'ENOENT') {
48        throw new Error('You must run `node-gyp configure` first!')
49      } else {
50        throw err
51      }
52    }
53
54    config = JSON.parse(data.replace(/#.+\n/, ''))
55
56    // get the 'arch', 'buildType', and 'nodeDir' vars from the config
57    buildType = config.target_defaults.default_configuration
58    arch = config.variables.target_arch
59    nodeDir = config.variables.nodedir
60    python = config.variables.python
61
62    if ('debug' in gyp.opts) {
63      buildType = gyp.opts.debug ? 'Debug' : 'Release'
64    }
65    if (!buildType) {
66      buildType = 'Release'
67    }
68
69    log.verbose('build type', buildType)
70    log.verbose('architecture', arch)
71    log.verbose('node dev dir', nodeDir)
72    log.verbose('python', python)
73
74    if (win) {
75      await findSolutionFile()
76    } else {
77      await doWhich()
78    }
79  }
80
81  /**
82   * On Windows, find the first build/*.sln file.
83   */
84
85  async function findSolutionFile () {
86    const files = await glob('build/*.sln')
87    if (files.length === 0) {
88      throw new Error('Could not find *.sln file. Did you run "configure"?')
89    }
90    guessedSolution = files[0]
91    log.verbose('found first Solution file', guessedSolution)
92    await doWhich()
93  }
94
95  /**
96   * Uses node-which to locate the msbuild / make executable.
97   */
98
99  async function doWhich () {
100    // On Windows use msbuild provided by node-gyp configure
101    if (win) {
102      if (!config.variables.msbuild_path) {
103        throw new Error('MSBuild is not set, please run `node-gyp configure`.')
104      }
105      command = config.variables.msbuild_path
106      log.verbose('using MSBuild:', command)
107      await doBuild()
108      return
109    }
110
111    // First make sure we have the build command in the PATH
112    const execPath = await which(command)
113    log.verbose('`which` succeeded for `' + command + '`', execPath)
114    await doBuild()
115  }
116
117  /**
118   * Actually spawn the process and compile the module.
119   */
120
121  async function doBuild () {
122    // Enable Verbose build
123    const verbose = log.logger.isVisible('verbose')
124    let j
125
126    if (!win && verbose) {
127      argv.push('V=1')
128    }
129
130    if (win && !verbose) {
131      argv.push('/clp:Verbosity=minimal')
132    }
133
134    if (win) {
135      // Turn off the Microsoft logo on Windows
136      argv.push('/nologo')
137    }
138
139    // Specify the build type, Release by default
140    if (win) {
141      // Convert .gypi config target_arch to MSBuild /Platform
142      // Since there are many ways to state '32-bit Intel', default to it.
143      // N.B. msbuild's Condition string equality tests are case-insensitive.
144      const archLower = arch.toLowerCase()
145      const p = archLower === 'x64'
146        ? 'x64'
147        : (archLower === 'arm'
148            ? 'ARM'
149            : (archLower === 'arm64' ? 'ARM64' : 'Win32'))
150      argv.push('/p:Configuration=' + buildType + ';Platform=' + p)
151      if (jobs) {
152        j = parseInt(jobs, 10)
153        if (!isNaN(j) && j > 0) {
154          argv.push('/m:' + j)
155        } else if (jobs.toUpperCase() === 'MAX') {
156          argv.push('/m:' + require('os').cpus().length)
157        }
158      }
159    } else {
160      argv.push('BUILDTYPE=' + buildType)
161      // Invoke the Makefile in the 'build' dir.
162      argv.push('-C')
163      argv.push('build')
164      if (jobs) {
165        j = parseInt(jobs, 10)
166        if (!isNaN(j) && j > 0) {
167          argv.push('--jobs')
168          argv.push(j)
169        } else if (jobs.toUpperCase() === 'MAX') {
170          argv.push('--jobs')
171          argv.push(require('os').cpus().length)
172        }
173      }
174    }
175
176    if (win) {
177      // did the user specify their own .sln file?
178      const hasSln = argv.some(function (arg) {
179        return path.extname(arg) === '.sln'
180      })
181      if (!hasSln) {
182        argv.unshift(gyp.opts.solution || guessedSolution)
183      }
184    }
185
186    if (!win) {
187      // Add build-time dependency symlinks (such as Python) to PATH
188      buildBinsDir = path.resolve('build', 'node_gyp_bins')
189      process.env.PATH = `${buildBinsDir}:${process.env.PATH}`
190      await fs.mkdir(buildBinsDir, { recursive: true })
191      const symlinkDestination = path.join(buildBinsDir, 'python3')
192      try {
193        await fs.unlink(symlinkDestination)
194      } catch (err) {
195        if (err.code !== 'ENOENT') throw err
196      }
197      await fs.symlink(python, symlinkDestination)
198      log.verbose('bin symlinks', `created symlink to "${python}" in "${buildBinsDir}" and added to PATH`)
199    }
200
201    const proc = gyp.spawn(command, argv)
202    await new Promise((resolve, reject) => proc.on('exit', async (code, signal) => {
203      if (buildBinsDir) {
204        // Clean up the build-time dependency symlinks:
205        await fs.rm(buildBinsDir, { recursive: true })
206      }
207
208      if (code !== 0) {
209        return reject(new Error('`' + command + '` failed with exit code: ' + code))
210      }
211      if (signal) {
212        return reject(new Error('`' + command + '` got signal: ' + signal))
213      }
214      resolve()
215    }))
216  }
217}
218
219module.exports = build
220module.exports.usage = 'Invokes `' + (win ? 'msbuild' : 'make') + '` and builds the module'
221