• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const log = require('npmlog')
4const execFile = require('child_process').execFile
5const path = require('path').win32
6const logWithPrefix = require('./util').logWithPrefix
7const regSearchKeys = require('./util').regSearchKeys
8
9function findVisualStudio (nodeSemver, configMsvsVersion, callback) {
10  const finder = new VisualStudioFinder(nodeSemver, configMsvsVersion,
11    callback)
12  finder.findVisualStudio()
13}
14
15function VisualStudioFinder (nodeSemver, configMsvsVersion, callback) {
16  this.nodeSemver = nodeSemver
17  this.configMsvsVersion = configMsvsVersion
18  this.callback = callback
19  this.errorLog = []
20  this.validVersions = []
21}
22
23VisualStudioFinder.prototype = {
24  log: logWithPrefix(log, 'find VS'),
25
26  regSearchKeys: regSearchKeys,
27
28  // Logs a message at verbose level, but also saves it to be displayed later
29  // at error level if an error occurs. This should help diagnose the problem.
30  addLog: function addLog (message) {
31    this.log.verbose(message)
32    this.errorLog.push(message)
33  },
34
35  findVisualStudio: function findVisualStudio () {
36    this.configVersionYear = null
37    this.configPath = null
38    if (this.configMsvsVersion) {
39      this.addLog('msvs_version was set from command line or npm config')
40      if (this.configMsvsVersion.match(/^\d{4}$/)) {
41        this.configVersionYear = parseInt(this.configMsvsVersion, 10)
42        this.addLog(
43          `- looking for Visual Studio version ${this.configVersionYear}`)
44      } else {
45        this.configPath = path.resolve(this.configMsvsVersion)
46        this.addLog(
47          `- looking for Visual Studio installed in "${this.configPath}"`)
48      }
49    } else {
50      this.addLog('msvs_version not set from command line or npm config')
51    }
52
53    if (process.env.VCINSTALLDIR) {
54      this.envVcInstallDir =
55        path.resolve(process.env.VCINSTALLDIR, '..')
56      this.addLog('running in VS Command Prompt, installation path is:\n' +
57        `"${this.envVcInstallDir}"\n- will only use this version`)
58    } else {
59      this.addLog('VCINSTALLDIR not set, not running in VS Command Prompt')
60    }
61
62    this.findVisualStudio2017OrNewer((info) => {
63      if (info) {
64        return this.succeed(info)
65      }
66      this.findVisualStudio2015((info) => {
67        if (info) {
68          return this.succeed(info)
69        }
70        this.findVisualStudio2013((info) => {
71          if (info) {
72            return this.succeed(info)
73          }
74          this.fail()
75        })
76      })
77    })
78  },
79
80  succeed: function succeed (info) {
81    this.log.info(`using VS${info.versionYear} (${info.version}) found at:` +
82                  `\n"${info.path}"` +
83                  '\nrun with --verbose for detailed information')
84    process.nextTick(this.callback.bind(null, null, info))
85  },
86
87  fail: function fail () {
88    if (this.configMsvsVersion && this.envVcInstallDir) {
89      this.errorLog.push(
90        'msvs_version does not match this VS Command Prompt or the',
91        'installation cannot be used.')
92    } else if (this.configMsvsVersion) {
93      // If msvs_version was specified but finding VS failed, print what would
94      // have been accepted
95      this.errorLog.push('')
96      if (this.validVersions) {
97        this.errorLog.push('valid versions for msvs_version:')
98        this.validVersions.forEach((version) => {
99          this.errorLog.push(`- "${version}"`)
100        })
101      } else {
102        this.errorLog.push('no valid versions for msvs_version were found')
103      }
104    }
105
106    const errorLog = this.errorLog.join('\n')
107
108    // For Windows 80 col console, use up to the column before the one marked
109    // with X (total 79 chars including logger prefix, 62 chars usable here):
110    //                                                               X
111    const infoLog = [
112      '**************************************************************',
113      'You need to install the latest version of Visual Studio',
114      'including the "Desktop development with C++" workload.',
115      'For more information consult the documentation at:',
116      'https://github.com/nodejs/node-gyp#on-windows',
117      '**************************************************************'
118    ].join('\n')
119
120    this.log.error(`\n${errorLog}\n\n${infoLog}\n`)
121    process.nextTick(this.callback.bind(null, new Error(
122      'Could not find any Visual Studio installation to use')))
123  },
124
125  // Invoke the PowerShell script to get information about Visual Studio 2017
126  // or newer installations
127  findVisualStudio2017OrNewer: function findVisualStudio2017OrNewer (cb) {
128    var ps = path.join(process.env.SystemRoot, 'System32',
129      'WindowsPowerShell', 'v1.0', 'powershell.exe')
130    var csFile = path.join(__dirname, 'Find-VisualStudio.cs')
131    var psArgs = [
132      '-ExecutionPolicy',
133      'Unrestricted',
134      '-NoProfile',
135      '-Command',
136      '&{Add-Type -Path \'' + csFile + '\';' + '[VisualStudioConfiguration.Main]::PrintJson()}'
137    ]
138
139    this.log.silly('Running', ps, psArgs)
140    var child = execFile(ps, psArgs, { encoding: 'utf8' },
141      (err, stdout, stderr) => {
142        this.parseData(err, stdout, stderr, cb)
143      })
144    child.stdin.end()
145  },
146
147  // Parse the output of the PowerShell script and look for an installation
148  // of Visual Studio 2017 or newer to use
149  parseData: function parseData (err, stdout, stderr, cb) {
150    this.log.silly('PS stderr = %j', stderr)
151
152    const failPowershell = () => {
153      this.addLog(
154        'could not use PowerShell to find Visual Studio 2017 or newer')
155      cb(null)
156    }
157
158    if (err) {
159      this.log.silly('PS err = %j', err && (err.stack || err))
160      return failPowershell()
161    }
162
163    var vsInfo
164    try {
165      vsInfo = JSON.parse(stdout)
166    } catch (e) {
167      this.log.silly('PS stdout = %j', stdout)
168      this.log.silly(e)
169      return failPowershell()
170    }
171
172    if (!Array.isArray(vsInfo)) {
173      this.log.silly('PS stdout = %j', stdout)
174      return failPowershell()
175    }
176
177    vsInfo = vsInfo.map((info) => {
178      this.log.silly(`processing installation: "${info.path}"`)
179      info.path = path.resolve(info.path)
180      var ret = this.getVersionInfo(info)
181      ret.path = info.path
182      ret.msBuild = this.getMSBuild(info, ret.versionYear)
183      ret.toolset = this.getToolset(info, ret.versionYear)
184      ret.sdk = this.getSDK(info)
185      return ret
186    })
187    this.log.silly('vsInfo:', vsInfo)
188
189    // Remove future versions or errors parsing version number
190    vsInfo = vsInfo.filter((info) => {
191      if (info.versionYear) {
192        return true
193      }
194      this.addLog(`unknown version "${info.version}" found at "${info.path}"`)
195      return false
196    })
197
198    // Sort to place newer versions first
199    vsInfo.sort((a, b) => b.versionYear - a.versionYear)
200
201    for (var i = 0; i < vsInfo.length; ++i) {
202      const info = vsInfo[i]
203      this.addLog(`checking VS${info.versionYear} (${info.version}) found ` +
204                  `at:\n"${info.path}"`)
205
206      if (info.msBuild) {
207        this.addLog('- found "Visual Studio C++ core features"')
208      } else {
209        this.addLog('- "Visual Studio C++ core features" missing')
210        continue
211      }
212
213      if (info.toolset) {
214        this.addLog(`- found VC++ toolset: ${info.toolset}`)
215      } else {
216        this.addLog('- missing any VC++ toolset')
217        continue
218      }
219
220      if (info.sdk) {
221        this.addLog(`- found Windows SDK: ${info.sdk}`)
222      } else {
223        this.addLog('- missing any Windows SDK')
224        continue
225      }
226
227      if (!this.checkConfigVersion(info.versionYear, info.path)) {
228        continue
229      }
230
231      return cb(info)
232    }
233
234    this.addLog(
235      'could not find a version of Visual Studio 2017 or newer to use')
236    cb(null)
237  },
238
239  // Helper - process version information
240  getVersionInfo: function getVersionInfo (info) {
241    const match = /^(\d+)\.(\d+)\..*/.exec(info.version)
242    if (!match) {
243      this.log.silly('- failed to parse version:', info.version)
244      return {}
245    }
246    this.log.silly('- version match = %j', match)
247    var ret = {
248      version: info.version,
249      versionMajor: parseInt(match[1], 10),
250      versionMinor: parseInt(match[2], 10)
251    }
252    if (ret.versionMajor === 15) {
253      ret.versionYear = 2017
254      return ret
255    }
256    if (ret.versionMajor === 16) {
257      ret.versionYear = 2019
258      return ret
259    }
260    this.log.silly('- unsupported version:', ret.versionMajor)
261    return {}
262  },
263
264  // Helper - process MSBuild information
265  getMSBuild: function getMSBuild (info, versionYear) {
266    const pkg = 'Microsoft.VisualStudio.VC.MSBuild.Base'
267    if (info.packages.indexOf(pkg) !== -1) {
268      this.log.silly('- found VC.MSBuild.Base')
269      if (versionYear === 2017) {
270        return path.join(info.path, 'MSBuild', '15.0', 'Bin', 'MSBuild.exe')
271      }
272      if (versionYear === 2019) {
273        return path.join(info.path, 'MSBuild', 'Current', 'Bin', 'MSBuild.exe')
274      }
275    }
276    return null
277  },
278
279  // Helper - process toolset information
280  getToolset: function getToolset (info, versionYear) {
281    const pkg = 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64'
282    const express = 'Microsoft.VisualStudio.WDExpress'
283
284    if (info.packages.indexOf(pkg) !== -1) {
285      this.log.silly('- found VC.Tools.x86.x64')
286    } else if (info.packages.indexOf(express) !== -1) {
287      this.log.silly('- found Visual Studio Express (looking for toolset)')
288    } else {
289      return null
290    }
291
292    if (versionYear === 2017) {
293      return 'v141'
294    } else if (versionYear === 2019) {
295      return 'v142'
296    }
297    this.log.silly('- invalid versionYear:', versionYear)
298    return null
299  },
300
301  // Helper - process Windows SDK information
302  getSDK: function getSDK (info) {
303    const win8SDK = 'Microsoft.VisualStudio.Component.Windows81SDK'
304    const win10SDKPrefix = 'Microsoft.VisualStudio.Component.Windows10SDK.'
305
306    var Win10SDKVer = 0
307    info.packages.forEach((pkg) => {
308      if (!pkg.startsWith(win10SDKPrefix)) {
309        return
310      }
311      const parts = pkg.split('.')
312      if (parts.length > 5 && parts[5] !== 'Desktop') {
313        this.log.silly('- ignoring non-Desktop Win10SDK:', pkg)
314        return
315      }
316      const foundSdkVer = parseInt(parts[4], 10)
317      if (isNaN(foundSdkVer)) {
318        // Microsoft.VisualStudio.Component.Windows10SDK.IpOverUsb
319        this.log.silly('- failed to parse Win10SDK number:', pkg)
320        return
321      }
322      this.log.silly('- found Win10SDK:', foundSdkVer)
323      Win10SDKVer = Math.max(Win10SDKVer, foundSdkVer)
324    })
325
326    if (Win10SDKVer !== 0) {
327      return `10.0.${Win10SDKVer}.0`
328    } else if (info.packages.indexOf(win8SDK) !== -1) {
329      this.log.silly('- found Win8SDK')
330      return '8.1'
331    }
332    return null
333  },
334
335  // Find an installation of Visual Studio 2015 to use
336  findVisualStudio2015: function findVisualStudio2015 (cb) {
337    return this.findOldVS({
338      version: '14.0',
339      versionMajor: 14,
340      versionMinor: 0,
341      versionYear: 2015,
342      toolset: 'v140'
343    }, cb)
344  },
345
346  // Find an installation of Visual Studio 2013 to use
347  findVisualStudio2013: function findVisualStudio2013 (cb) {
348    if (this.nodeSemver.major >= 9) {
349      this.addLog(
350        'not looking for VS2013 as it is only supported up to Node.js 8')
351      return cb(null)
352    }
353    return this.findOldVS({
354      version: '12.0',
355      versionMajor: 12,
356      versionMinor: 0,
357      versionYear: 2013,
358      toolset: 'v120'
359    }, cb)
360  },
361
362  // Helper - common code for VS2013 and VS2015
363  findOldVS: function findOldVS (info, cb) {
364    const regVC7 = ['HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7',
365      'HKLM\\Software\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VC7']
366    const regMSBuild = 'HKLM\\Software\\Microsoft\\MSBuild\\ToolsVersions'
367
368    this.addLog(`looking for Visual Studio ${info.versionYear}`)
369    this.regSearchKeys(regVC7, info.version, [], (err, res) => {
370      if (err) {
371        this.addLog('- not found')
372        return cb(null)
373      }
374
375      const vsPath = path.resolve(res, '..')
376      this.addLog(`- found in "${vsPath}"`)
377
378      const msBuildRegOpts = process.arch === 'ia32' ? [] : ['/reg:32']
379      this.regSearchKeys([`${regMSBuild}\\${info.version}`],
380        'MSBuildToolsPath', msBuildRegOpts, (err, res) => {
381          if (err) {
382            this.addLog(
383              '- could not find MSBuild in registry for this version')
384            return cb(null)
385          }
386
387          const msBuild = path.join(res, 'MSBuild.exe')
388          this.addLog(`- MSBuild in "${msBuild}"`)
389
390          if (!this.checkConfigVersion(info.versionYear, vsPath)) {
391            return cb(null)
392          }
393
394          info.path = vsPath
395          info.msBuild = msBuild
396          info.sdk = null
397          cb(info)
398        })
399    })
400  },
401
402  // After finding a usable version of Visual Stuido:
403  // - add it to validVersions to be displayed at the end if a specific
404  //   version was requested and not found;
405  // - check if this is the version that was requested.
406  // - check if this matches the Visual Studio Command Prompt
407  checkConfigVersion: function checkConfigVersion (versionYear, vsPath) {
408    this.validVersions.push(versionYear)
409    this.validVersions.push(vsPath)
410
411    if (this.configVersionYear && this.configVersionYear !== versionYear) {
412      this.addLog('- msvs_version does not match this version')
413      return false
414    }
415    if (this.configPath &&
416        path.relative(this.configPath, vsPath) !== '') {
417      this.addLog('- msvs_version does not point to this installation')
418      return false
419    }
420    if (this.envVcInstallDir &&
421        path.relative(this.envVcInstallDir, vsPath) !== '') {
422      this.addLog('- does not match this Visual Studio Command Prompt')
423      return false
424    }
425
426    return true
427  }
428}
429
430module.exports = findVisualStudio
431module.exports.test = {
432  VisualStudioFinder: VisualStudioFinder,
433  findVisualStudio: findVisualStudio
434}
435