codekingpro/portable-devtools
115k
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 