Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
find-visualstudio.js607 linesDownload Raw Back to lib
1'use strict'
2
3const log = require('./log')
4const { existsSync } = require('fs')
5const { win32: path } = require('path')
6const { regSearchKeys, execFile } = require('./util')
7
8class VisualStudioFinder {
9  static findVisualStudio = (...args) => new VisualStudioFinder(...args).findVisualStudio()
10
11  log = log.withPrefix('find VS')
12
13  regSearchKeys = regSearchKeys
14
15  constructor (nodeSemver, configMsvsVersion) {
16    this.nodeSemver = nodeSemver
17    this.configMsvsVersion = configMsvsVersion
18    this.errorLog = []
19    this.validVersions = []
20  }
21
22  // Logs a message at verbose level, but also saves it to be displayed later
23  // at error level if an error occurs. This should help diagnose the problem.
24  addLog (message) {
25    this.log.verbose(message)
26    this.errorLog.push(message)
27  }
28
29  async findVisualStudio () {
30    this.configVersionYear = null
31    this.configPath = null
32    if (this.configMsvsVersion) {
33      this.addLog(`--msvs_version=${this.configMsvsVersion} was set on the command line`)
34      if (this.configMsvsVersion.match(/^\d{4}$/)) {
35        this.configVersionYear = parseInt(this.configMsvsVersion, 10)
36        this.addLog(
37          `- looking for Visual Studio version ${this.configVersionYear}`)
38      } else {
39        this.configPath = path.resolve(this.configMsvsVersion)
40        this.addLog(
41          `- looking for Visual Studio installed in "${this.configPath}"`)
42      }
43    } else {
44      this.addLog('--msvs_version was not set on the command line')
45    }
46
47    if (process.env.VCINSTALLDIR) {
48      this.envVcInstallDir =
49        path.resolve(process.env.VCINSTALLDIR, '..')
50      this.addLog('running in VS Command Prompt, installation path is:\n' +
51        `"${this.envVcInstallDir}"\n- will only use this version`)
52    } else {
53      this.addLog('VCINSTALLDIR not set, not running in VS Command Prompt')
54    }
55
56    const checks = [
57      () => this.findVisualStudio2019OrNewerFromSpecifiedLocation(),
58      () => this.findVisualStudio2019OrNewerUsingSetupModule(),
59      () => this.findVisualStudio2019OrNewer(),
60      () => this.findVisualStudio2017FromSpecifiedLocation(),
61      () => this.findVisualStudio2017UsingSetupModule(),
62      () => this.findVisualStudio2017(),
63      () => this.findVisualStudio2015(),
64      () => this.findVisualStudio2013()
65    ]
66
67    for (const check of checks) {
68      const info = await check()
69      if (info) {
70        return this.succeed(info)
71      }
72    }
73
74    return this.fail()
75  }
76
77  succeed (info) {
78    this.log.info(`using VS${info.versionYear} (${info.version}) found at:` +
79                  `\n"${info.path}"` +
80                  '\nrun with --verbose for detailed information')
81    return info
82  }
83
84  fail () {
85    if (this.configMsvsVersion && this.envVcInstallDir) {
86      this.errorLog.push(
87        'msvs_version does not match this VS Command Prompt or the',
88        'installation cannot be used.')
89    } else if (this.configMsvsVersion) {
90      // If msvs_version was specified but finding VS failed, print what would
91      // have been accepted
92      this.errorLog.push('')
93      if (this.validVersions) {
94        this.errorLog.push('valid versions for msvs_version:')
95        this.validVersions.forEach((version) => {
96          this.errorLog.push(`- "${version}"`)
97        })
98      } else {
99        this.errorLog.push('no valid versions for msvs_version were found')
100      }
101    }
102
103    const errorLog = this.errorLog.join('\n')
104
105    // For Windows 80 col console, use up to the column before the one marked
106    // with X (total 79 chars including logger prefix, 62 chars usable here):
107    //                                                               X
108    const infoLog = [
109      '**************************************************************',
110      'You need to install the latest version of Visual Studio',
111      'including the "Desktop development with C++" workload.',
112      'For more information consult the documentation at:',
113      'https://github.com/nodejs/node-gyp#on-windows',
114      '**************************************************************'
115    ].join('\n')
116
117    this.log.error(`\n${errorLog}\n\n${infoLog}\n`)
118    throw new Error('Could not find any Visual Studio installation to use')
119  }
120
121  async findVisualStudio2019OrNewerFromSpecifiedLocation () {
122    return this.findVSFromSpecifiedLocation([2019, 2022, 2026])
123  }
124
125  async findVisualStudio2017FromSpecifiedLocation () {
126    if (this.nodeSemver.major >= 22) {
127      this.addLog(
128        'not looking for VS2017 as it is only supported up to Node.js 21')
129      return null
130    }
131    return this.findVSFromSpecifiedLocation([2017])
132  }
133
134  async findVSFromSpecifiedLocation (supportedYears) {
135    if (!this.envVcInstallDir) {
136      return null
137    }
138    const info = {
139      path: path.resolve(this.envVcInstallDir),
140      // Assume the version specified by the user is correct.
141      // Since Visual Studio 2015, the Developer Command Prompt sets the
142      // VSCMD_VER environment variable which contains the version information
143      // for Visual Studio.
144      // https://learn.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell?view=vs-2022
145      version: process.env.VSCMD_VER,
146      packages: [
147        'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
148        'Microsoft.VisualStudio.Component.VC.Tools.ARM64',
149        // Assume MSBuild exists. It will be checked in processing.
150        'Microsoft.VisualStudio.VC.MSBuild.Base'
151      ]
152    }
153
154    // Is there a better way to get SDK information?
155    const envWindowsSDKVersion = process.env.WindowsSDKVersion
156    const sdkVersionMatched = envWindowsSDKVersion?.match(/^(\d+)\.(\d+)\.(\d+)\..*/)
157    if (sdkVersionMatched) {
158      info.packages.push(`Microsoft.VisualStudio.Component.Windows10SDK.${sdkVersionMatched[3]}.Desktop`)
159    }
160    // pass for further processing
161    return this.processData([info], supportedYears)
162  }
163
164  async findVisualStudio2019OrNewerUsingSetupModule () {
165    return this.findNewVSUsingSetupModule([2019, 2022, 2026])
166  }
167
168  async findVisualStudio2017UsingSetupModule () {
169    if (this.nodeSemver.major >= 22) {
170      this.addLog(
171        'not looking for VS2017 as it is only supported up to Node.js 21')
172      return null
173    }
174    return this.findNewVSUsingSetupModule([2017])
175  }
176
177  async findNewVSUsingSetupModule (supportedYears) {
178    const ps = path.join(process.env.SystemRoot, 'System32',
179      'WindowsPowerShell', 'v1.0', 'powershell.exe')
180    const vcInstallDir = this.envVcInstallDir
181
182    const checkModuleArgs = [
183      '-NoProfile',
184      '-Command',
185      '&{@(Get-Module -ListAvailable -Name VSSetup).Version.ToString()}'
186    ]
187    this.log.silly('Running', ps, checkModuleArgs)
188    const [cErr] = await this.execFile(ps, checkModuleArgs)
189    if (cErr) {
190      this.addLog('VSSetup module doesn\'t seem to exist. You can install it via: "Install-Module VSSetup -Scope CurrentUser"')
191      this.log.silly('VSSetup error = %j', cErr && (cErr.stack || cErr))
192      return null
193    }
194    const filterArg = vcInstallDir !== undefined ? `| where {$_.InstallationPath -eq '${vcInstallDir}' }` : ''
195    const psArgs = [
196      '-NoProfile',
197      '-Command',
198      `&{Get-VSSetupInstance ${filterArg} | ConvertTo-Json -Depth 3}`
199    ]
200
201    this.log.silly('Running', ps, psArgs)
202    const [err, stdout, stderr] = await this.execFile(ps, psArgs)
203    let parsedData = this.parseData(err, stdout, stderr)
204    if (parsedData === null) {
205      return null
206    }
207    this.log.silly('Parsed data', parsedData)
208    if (!Array.isArray(parsedData)) {
209      // if there are only 1 result, then Powershell will output non-array
210      parsedData = [parsedData]
211    }
212    // normalize output
213    parsedData = parsedData.map((info) => {
214      info.path = info.InstallationPath
215      info.version = `${info.InstallationVersion.Major}.${info.InstallationVersion.Minor}.${info.InstallationVersion.Build}.${info.InstallationVersion.Revision}`
216      info.packages = info.Packages.map((p) => p.Id)
217      return info
218    })
219    // pass for further processing
220    return this.processData(parsedData, supportedYears)
221  }
222
223  // Invoke the PowerShell script to get information about Visual Studio 2019
224  // or newer installations
225  async findVisualStudio2019OrNewer () {
226    return this.findNewVS([2019, 2022, 2026])
227  }
228
229  // Invoke the PowerShell script to get information about Visual Studio 2017
230  async findVisualStudio2017 () {
231    if (this.nodeSemver.major >= 22) {
232      this.addLog(
233        'not looking for VS2017 as it is only supported up to Node.js 21')
234      return null
235    }
236    return this.findNewVS([2017])
237  }
238
239  // Invoke the PowerShell script to get information about Visual Studio 2017
240  // or newer installations
241  async findNewVS (supportedYears) {
242    const ps = path.join(process.env.SystemRoot, 'System32',
243      'WindowsPowerShell', 'v1.0', 'powershell.exe')
244    const csFile = path.join(__dirname, 'Find-VisualStudio.cs')
245    const psArgs = [
246      '-ExecutionPolicy',
247      'Unrestricted',
248      '-NoProfile',
249      '-Command',
250      '&{Add-Type -Path \'' + csFile + '\';' + '[VisualStudioConfiguration.Main]::PrintJson()}'
251    ]
252
253    this.log.silly('Running', ps, psArgs)
254    const [err, stdout, stderr] = await this.execFile(ps, psArgs)
255    const parsedData = this.parseData(err, stdout, stderr, { checkIsArray: true })
256    if (parsedData === null) {
257      return null
258    }
259    return this.processData(parsedData, supportedYears)
260  }
261
262  // Parse the output of the PowerShell script, make sanity checks
263  parseData (err, stdout, stderr, sanityCheckOptions) {
264    const defaultOptions = {
265      checkIsArray: false
266    }
267
268    // Merging provided options with the default options
269    const sanityOptions = { ...defaultOptions, ...sanityCheckOptions }
270
271    this.log.silly('PS stderr = %j', stderr)
272
273    const failPowershell = (failureDetails) => {
274      this.addLog(
275        `could not use PowerShell to find Visual Studio 2017 or newer, try re-running with '--loglevel silly' for more details. \n
276        Failure details: ${failureDetails}`)
277      return null
278    }
279
280    if (err) {
281      this.log.silly('PS err = %j', err && (err.stack || err))
282      return failPowershell(`${err}`.substring(0, 40))
283    }
284
285    let vsInfo
286    try {
287      vsInfo = JSON.parse(stdout)
288    } catch (e) {
289      this.log.silly('PS stdout = %j', stdout)
290      this.log.silly(e)
291      return failPowershell()
292    }
293
294    if (sanityOptions.checkIsArray && !Array.isArray(vsInfo)) {
295      this.log.silly('PS stdout = %j', stdout)
296      return failPowershell('Expected array as output of the PS script')
297    }
298    return vsInfo
299  }
300
301  // Process parsed data containing information about VS installations
302  // Look for the required parts, extract and output them back
303  processData (vsInfo, supportedYears) {
304    vsInfo = vsInfo.map((info) => {
305      this.log.silly(`processing installation: "${info.path}"`)
306      info.path = path.resolve(info.path)
307      const ret = this.getVersionInfo(info)
308      ret.path = info.path
309      ret.msBuild = this.getMSBuild(info, ret.versionYear)
310      ret.toolset = this.getToolset(info, ret.versionYear)
311      ret.sdk = this.getSDK(info)
312      return ret
313    })
314    this.log.silly('vsInfo:', vsInfo)
315
316    // Remove future versions or errors parsing version number
317    // Also remove any unsupported versions
318    vsInfo = vsInfo.filter((info) => {
319      if (info.versionYear && supportedYears.indexOf(info.versionYear) !== -1) {
320        return true
321      }
322      this.addLog(`${info.versionYear ? 'unsupported' : 'unknown'} version "${info.version}" found at "${info.path}"`)
323      return false
324    })
325
326    // Sort to place newer versions first
327    vsInfo.sort((a, b) => b.versionYear - a.versionYear)
328
329    for (let i = 0; i < vsInfo.length; ++i) {
330      const info = vsInfo[i]
331      this.addLog(`checking VS${info.versionYear} (${info.version}) found ` +
332                  `at:\n"${info.path}"`)
333
334      if (info.msBuild) {
335        this.addLog('- found "Visual Studio C++ core features"')
336      } else {
337        this.addLog('- "Visual Studio C++ core features" missing')
338        continue
339      }
340
341      if (info.toolset) {
342        this.addLog(`- found VC++ toolset: ${info.toolset}`)
343      } else {
344        this.addLog('- missing any VC++ toolset')
345        continue
346      }
347
348      if (info.sdk) {
349        this.addLog(`- found Windows SDK: ${info.sdk}`)
350      } else {
351        this.addLog('- missing any Windows SDK')
352        continue
353      }
354
355      if (!this.checkConfigVersion(info.versionYear, info.path)) {
356        continue
357      }
358
359      return info
360    }
361
362    this.addLog(
363      'could not find a version of Visual Studio 2017 or newer to use')
364    return null
365  }
366
367  // Helper - process version information
368  getVersionInfo (info) {
369    const match = /^(\d+)\.(\d+)(?:\..*)?/.exec(info.version)
370    if (!match) {
371      this.log.silly('- failed to parse version:', info.version)
372      return {}
373    }
374    this.log.silly('- version match = %j', match)
375    const ret = {
376      version: info.version,
377      versionMajor: parseInt(match[1], 10),
378      versionMinor: parseInt(match[2], 10)
379    }
380    if (ret.versionMajor === 15) {
381      ret.versionYear = 2017
382      return ret
383    }
384    if (ret.versionMajor === 16) {
385      ret.versionYear = 2019
386      return ret
387    }
388    if (ret.versionMajor === 17) {
389      ret.versionYear = 2022
390      return ret
391    }
392    if (ret.versionMajor === 18) {
393      ret.versionYear = 2026
394      return ret
395    }
396    this.log.silly('- unsupported version:', ret.versionMajor)
397    return {}
398  }
399
400  msBuildPathExists (path) {
401    return existsSync(path)
402  }
403
404  // Helper - process MSBuild information
405  getMSBuild (info, versionYear) {
406    const pkg = 'Microsoft.VisualStudio.VC.MSBuild.Base'
407    const msbuildPath = path.join(info.path, 'MSBuild', 'Current', 'Bin', 'MSBuild.exe')
408    const msbuildPathArm64 = path.join(info.path, 'MSBuild', 'Current', 'Bin', 'arm64', 'MSBuild.exe')
409    if (info.packages.indexOf(pkg) !== -1) {
410      this.log.silly('- found VC.MSBuild.Base')
411      if (versionYear === 2017) {
412        return path.join(info.path, 'MSBuild', '15.0', 'Bin', 'MSBuild.exe')
413      }
414      if (versionYear === 2019) {
415        if (process.arch === 'arm64' && this.msBuildPathExists(msbuildPathArm64)) {
416          return msbuildPathArm64
417        } else {
418          return msbuildPath
419        }
420      }
421    }
422    /**
423     * Visual Studio 2022 doesn't have the MSBuild package.
424     * Support for compiling _on_ ARM64 was added in MSVC 14.32.31326,
425     * so let's leverage it if the user has an ARM64 device.
426     */
427    if (process.arch === 'arm64' && this.msBuildPathExists(msbuildPathArm64)) {
428      return msbuildPathArm64
429    } else if (this.msBuildPathExists(msbuildPath)) {
430      return msbuildPath
431    }
432    return null
433  }
434
435  // Helper - process toolset information
436  getToolset (info, versionYear) {
437    const vcToolsArm64 = 'VC.Tools.ARM64'
438    const pkgArm64 = `Microsoft.VisualStudio.Component.${vcToolsArm64}`
439    const vcToolsX64 = 'VC.Tools.x86.x64'
440    const pkgX64 = `Microsoft.VisualStudio.Component.${vcToolsX64}`
441    const express = 'Microsoft.VisualStudio.WDExpress'
442
443    if (process.arch === 'arm64' && info.packages.includes(pkgArm64)) {
444      this.log.silly(`- found ${vcToolsArm64}`)
445    } else if (info.packages.includes(pkgX64)) {
446      if (process.arch === 'arm64') {
447        this.addLog(`- found ${vcToolsX64} on ARM64 platform. Expect less performance and/or link failure with ARM64 binary.`)
448      } else {
449        this.log.silly(`- found ${vcToolsX64}`)
450      }
451    } else if (info.packages.includes(express)) {
452      this.log.silly('- found Visual Studio Express (looking for toolset)')
453    } else {
454      return null
455    }
456
457    if (versionYear === 2017) {
458      return 'v141'
459    } else if (versionYear === 2019) {
460      return 'v142'
461    } else if (versionYear === 2022) {
462      return 'v143'
463    } else if (versionYear === 2026) {
464      return 'v145'
465    }
466    this.log.silly('- invalid versionYear:', versionYear)
467    return null
468  }
469
470  // Helper - process Windows SDK information
471  getSDK (info) {
472    const win8SDK = 'Microsoft.VisualStudio.Component.Windows81SDK'
473    const win10SDKPrefix = 'Microsoft.VisualStudio.Component.Windows10SDK.'
474    const win11SDKPrefix = 'Microsoft.VisualStudio.Component.Windows11SDK.'
475
476    let Win10or11SDKVer = 0
477    info.packages.forEach((pkg) => {
478      if (!pkg.startsWith(win10SDKPrefix) && !pkg.startsWith(win11SDKPrefix)) {
479        return
480      }
481      const parts = pkg.split('.')
482      if (parts.length > 5 && parts[5] !== 'Desktop') {
483        this.log.silly('- ignoring non-Desktop Win10/11SDK:', pkg)
484        return
485      }
486      const foundSdkVer = parseInt(parts[4], 10)
487      if (isNaN(foundSdkVer)) {
488        // Microsoft.VisualStudio.Component.Windows10SDK.IpOverUsb
489        this.log.silly('- failed to parse Win10/11SDK number:', pkg)
490        return
491      }
492      this.log.silly('- found Win10/11SDK:', foundSdkVer)
493      Win10or11SDKVer = Math.max(Win10or11SDKVer, foundSdkVer)
494    })
495
496    if (Win10or11SDKVer !== 0) {
497      return `10.0.${Win10or11SDKVer}.0`
498    } else if (info.packages.indexOf(win8SDK) !== -1) {
499      this.log.silly('- found Win8SDK')
500      return '8.1'
501    }
502    return null
503  }
504
505  // Find an installation of Visual Studio 2015 to use
506  async findVisualStudio2015 () {
507    if (this.nodeSemver.major >= 19) {
508      this.addLog(
509        'not looking for VS2015 as it is only supported up to Node.js 18')
510      return null
511    }
512    return this.findOldVS({
513      version: '14.0',
514      versionMajor: 14,
515      versionMinor: 0,
516      versionYear: 2015,
517      toolset: 'v140'
518    })
519  }
520
521  // Find an installation of Visual Studio 2013 to use
522  async findVisualStudio2013 () {
523    if (this.nodeSemver.major >= 9) {
524      this.addLog(
525        'not looking for VS2013 as it is only supported up to Node.js 8')
526      return null
527    }
528    return this.findOldVS({
529      version: '12.0',
530      versionMajor: 12,
531      versionMinor: 0,
532      versionYear: 2013,
533      toolset: 'v120'
534    })
535  }
536
537  // Helper - common code for VS2013 and VS2015
538  async findOldVS (info) {
539    const regVC7 = ['HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7',
540      'HKLM\\Software\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VC7']
541    const regMSBuild = 'HKLM\\Software\\Microsoft\\MSBuild\\ToolsVersions'
542
543    this.addLog(`looking for Visual Studio ${info.versionYear}`)
544    try {
545      let res = await this.regSearchKeys(regVC7, info.version, [])
546      const vsPath = path.resolve(res, '..')
547      this.addLog(`- found in "${vsPath}"`)
548      const msBuildRegOpts = process.arch === 'ia32' ? [] : ['/reg:32']
549
550      try {
551        res = await this.regSearchKeys([`${regMSBuild}\\${info.version}`], 'MSBuildToolsPath', msBuildRegOpts)
552      } catch (err) {
553        this.addLog('- could not find MSBuild in registry for this version')
554        return null
555      }
556
557      const msBuild = path.join(res, 'MSBuild.exe')
558      this.addLog(`- MSBuild in "${msBuild}"`)
559
560      if (!this.checkConfigVersion(info.versionYear, vsPath)) {
561        return null
562      }
563
564      info.path = vsPath
565      info.msBuild = msBuild
566      info.sdk = null
567      return info
568    } catch (err) {
569      this.addLog('- not found')
570      return null
571    }
572  }
573
574  // After finding a usable version of Visual Studio:
575  // - add it to validVersions to be displayed at the end if a specific
576  //   version was requested and not found;
577  // - check if this is the version that was requested.
578  // - check if this matches the Visual Studio Command Prompt
579  checkConfigVersion (versionYear, vsPath) {
580    this.validVersions.push(versionYear)
581    this.validVersions.push(vsPath)
582
583    if (this.configVersionYear && this.configVersionYear !== versionYear) {
584      this.addLog('- msvs_version does not match this version')
585      return false
586    }
587    if (this.configPath &&
588        path.relative(this.configPath, vsPath) !== '') {
589      this.addLog('- msvs_version does not point to this installation')
590      return false
591    }
592    if (this.envVcInstallDir &&
593        path.relative(this.envVcInstallDir, vsPath) !== '') {
594      this.addLog('- does not match this Visual Studio Command Prompt')
595      return false
596    }
597
598    return true
599  }
600
601  async execFile (exec, args) {
602    return await execFile(exec, args, { encoding: 'utf8' })
603  }
604}
605
606module.exports = VisualStudioFinder
607 
codekingpro/portable-devtools · Team Ai