Skip to content

Commit 7fe4095

Browse files
committed
win: generic Visual Studio 2017 detection
PR-URL: #1762 Reviewed-By: Rod Vagg <rod@vagg.org> Reviewed-By: Refael Ackermann <refack@gmail.com>
1 parent 721eb69 commit 7fe4095

9 files changed

+387
-118
lines changed

lib/Find-VS2017.cs renamed to lib/Find-VisualStudio.cs

Lines changed: 29 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
// See accompanying file LICENSE at https://github.com/node4good/windows-autoconf
44

55
// Usage:
6-
// powershell -ExecutionPolicy Unrestricted -Version "2.0" -Command "&{Add-Type -Path Find-VS2017.cs; [VisualStudioConfiguration.Main]::Query()}"
6+
// powershell -ExecutionPolicy Unrestricted -Command "Add-Type -Path Find-VisualStudio.cs; [VisualStudioConfiguration.Main]::PrintJson()"
7+
// This script needs to be compatible with PowerShell v2 to run on Windows 2008R2 and Windows 7.
8+
79
using System;
810
using System.Text;
911
using System.Runtime.InteropServices;
12+
using System.Collections.Generic;
1013

1114
namespace VisualStudioConfiguration
1215
{
@@ -184,90 +187,57 @@ public class SetupConfigurationClass
184187

185188
public static class Main
186189
{
187-
public static void Query()
190+
public static void PrintJson()
188191
{
189192
ISetupConfiguration query = new SetupConfiguration();
190193
ISetupConfiguration2 query2 = (ISetupConfiguration2)query;
191194
IEnumSetupInstances e = query2.EnumAllInstances();
192195

193196
int pceltFetched;
194197
ISetupInstance2[] rgelt = new ISetupInstance2[1];
195-
StringBuilder log = new StringBuilder();
198+
List<string> instances = new List<string>();
196199
while (true)
197200
{
198201
e.Next(1, rgelt, out pceltFetched);
199202
if (pceltFetched <= 0)
200203
{
201-
Console.WriteLine(String.Format("{{\"log\":\"{0}\"}}", log.ToString()));
204+
Console.WriteLine(String.Format("[{0}]", string.Join(",", instances.ToArray())));
202205
return;
203206
}
204-
if (CheckInstance(rgelt[0], ref log))
205-
return;
207+
208+
instances.Add(InstanceJson(rgelt[0]));
206209
}
207210
}
208211

209-
private static bool CheckInstance(ISetupInstance2 setupInstance2, ref StringBuilder log)
212+
private static string JsonString(string s)
210213
{
211-
// Visual Studio Community 2017 component directory:
212-
// https://www.visualstudio.com/en-us/productinfo/vs2017-install-product-Community.workloads
214+
return "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
215+
}
213216

214-
string path = setupInstance2.GetInstallationPath().Replace("\\", "\\\\");
215-
log.Append(String.Format("Found installation at: {0}\\n", path));
217+
private static string InstanceJson(ISetupInstance2 setupInstance2)
218+
{
219+
// Visual Studio component directory:
220+
// https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids
216221

217-
bool hasMSBuild = false;
218-
bool hasVCTools = false;
219-
uint Win10SDKVer = 0;
220-
bool hasWin8SDK = false;
222+
StringBuilder json = new StringBuilder();
223+
json.Append("{");
221224

222-
foreach (ISetupPackageReference package in setupInstance2.GetPackages())
223-
{
224-
const string Win10SDKPrefix = "Microsoft.VisualStudio.Component.Windows10SDK.";
225-
226-
string id = package.GetId();
227-
if (id == "Microsoft.VisualStudio.VC.MSBuild.Base")
228-
hasMSBuild = true;
229-
else if (id == "Microsoft.VisualStudio.Component.VC.Tools.x86.x64")
230-
hasVCTools = true;
231-
else if (id.StartsWith(Win10SDKPrefix)) {
232-
string[] parts = id.Substring(Win10SDKPrefix.Length).Split('.');
233-
if (parts.Length > 1 && parts[1] != "Desktop")
234-
continue;
235-
uint foundSdkVer;
236-
if (UInt32.TryParse(parts[0], out foundSdkVer))
237-
Win10SDKVer = Math.Max(Win10SDKVer, foundSdkVer);
238-
} else if (id == "Microsoft.VisualStudio.Component.Windows81SDK")
239-
hasWin8SDK = true;
240-
else
241-
continue;
242-
243-
log.Append(String.Format(" - Found {0}\\n", id));
244-
}
225+
string path = JsonString(setupInstance2.GetInstallationPath());
226+
json.Append(String.Format("\"path\":{0},", path));
245227

246-
if (!hasMSBuild)
247-
log.Append(" - Missing Visual Studio C++ core features (Microsoft.VisualStudio.VC.MSBuild.Base)\\n");
248-
if (!hasVCTools)
249-
log.Append(" - Missing VC++ 2017 v141 toolset (x86,x64) (Microsoft.VisualStudio.Component.VC.Tools.x86.x64)\\n");
250-
if ((Win10SDKVer == 0) && (!hasWin8SDK))
251-
log.Append(" - Missing a Windows SDK (Microsoft.VisualStudio.Component.Windows10SDK.* or Microsoft.VisualStudio.Component.Windows81SDK)\\n");
228+
string version = JsonString(setupInstance2.GetInstallationVersion());
229+
json.Append(String.Format("\"version\":{0},", version));
252230

253-
if (hasMSBuild && hasVCTools)
231+
List<string> packages = new List<string>();
232+
foreach (ISetupPackageReference package in setupInstance2.GetPackages())
254233
{
255-
if (Win10SDKVer > 0)
256-
{
257-
log.Append(" - Using this installation with Windows 10 SDK"/*\\n*/);
258-
Console.WriteLine(String.Format("{{\"log\":\"{0}\",\"path\":\"{1}\",\"sdk\":\"10.0.{2}.0\"}}", log.ToString(), path, Win10SDKVer));
259-
return true;
260-
}
261-
else if (hasWin8SDK)
262-
{
263-
log.Append(" - Using this installation with Windows 8.1 SDK"/*\\n*/);
264-
Console.WriteLine(String.Format("{{\"log\":\"{0}\",\"path\":\"{1}\",\"sdk\":\"8.1\"}}", log.ToString(), path));
265-
return true;
266-
}
234+
string id = JsonString(package.GetId());
235+
packages.Add(id);
267236
}
237+
json.Append(String.Format("\"packages\":[{0}]", string.Join(",", packages.ToArray())));
268238

269-
log.Append(" - Some required components are missing, not using this installation\\n");
270-
return false;
239+
json.Append("}");
240+
return json.ToString();
271241
}
272242
}
273243
}

lib/configure.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ var fs = require('graceful-fs')
1717
, win = process.platform === 'win32'
1818
, findNodeDirectory = require('./find-node-directory')
1919
, msgFormat = require('util').format
20+
, logWithPrefix = require('./util').logWithPrefix
2021
if (win)
21-
var findVS2017 = require('./find-vs2017')
22+
var findVisualStudio = require('./find-visualstudio')
2223

2324
exports.usage = 'Generates ' + (win ? 'MSVC project files' : 'a Makefile') + ' for the current module'
2425

@@ -83,7 +84,7 @@ function configure (gyp, argv, callback) {
8384
if (err) return callback(err)
8485
log.verbose('build dir', '"build" dir needed to be created?', isNew)
8586
if (win && (!gyp.opts.msvs_version || gyp.opts.msvs_version === '2017')) {
86-
findVS2017(function (err, vsSetup) {
87+
findVisualStudio(function (err, vsSetup) {
8788
if (err) {
8889
log.verbose('Not using VS2017:', err.message)
8990
createConfigFile()
@@ -644,16 +645,3 @@ function findPython (configPython, callback) {
644645
var finder = new PythonFinder(configPython, callback)
645646
finder.findPython()
646647
}
647-
648-
function logWithPrefix (log, prefix) {
649-
function setPrefix(logFunction) {
650-
return (...args) => logFunction.apply(null, [prefix, ...args])
651-
}
652-
return {
653-
silly: setPrefix(log.silly),
654-
verbose: setPrefix(log.verbose),
655-
info: setPrefix(log.info),
656-
warn: setPrefix(log.warn),
657-
error: setPrefix(log.error),
658-
}
659-
}

lib/find-visualstudio.js

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
module.exports = exports = findVisualStudio
2+
module.exports.test = {
3+
VisualStudioFinder: VisualStudioFinder,
4+
findVisualStudio: findVisualStudio
5+
}
6+
7+
const log = require('npmlog')
8+
const execFile = require('child_process').execFile
9+
const path = require('path').win32
10+
const logWithPrefix = require('./util').logWithPrefix
11+
12+
function findVisualStudio (callback) {
13+
const finder = new VisualStudioFinder(callback)
14+
finder.findVisualStudio()
15+
}
16+
17+
function VisualStudioFinder (callback) {
18+
this.callback = callback
19+
this.errorLog = []
20+
}
21+
22+
VisualStudioFinder.prototype = {
23+
log: logWithPrefix(log, 'find VS'),
24+
25+
// Logs a message at verbose level, but also saves it to be displayed later
26+
// at error level if an error occurs. This should help diagnose the problem.
27+
addLog: function addLog (message) {
28+
this.log.verbose(message)
29+
this.errorLog.push(message)
30+
},
31+
32+
findVisualStudio: function findVisualStudio () {
33+
var ps = path.join(process.env.SystemRoot, 'System32',
34+
'WindowsPowerShell', 'v1.0', 'powershell.exe')
35+
var csFile = path.join(__dirname, 'Find-VisualStudio.cs')
36+
var psArgs = ['-ExecutionPolicy', 'Unrestricted', '-NoProfile',
37+
'-Command', '&{Add-Type -Path \'' + csFile + '\';' +
38+
'[VisualStudioConfiguration.Main]::PrintJson()}']
39+
40+
this.log.silly('Running', ps, psArgs)
41+
var child = execFile(ps, psArgs, { encoding: 'utf8' },
42+
this.parseData.bind(this))
43+
child.stdin.end()
44+
},
45+
46+
parseData: function parseData (err, stdout, stderr) {
47+
this.log.silly('PS stderr = %j', stderr)
48+
49+
if (err) {
50+
this.log.silly('PS err = %j', err && (err.stack || err))
51+
return this.failPowershell()
52+
}
53+
54+
var vsInfo
55+
try {
56+
vsInfo = JSON.parse(stdout)
57+
} catch (e) {
58+
this.log.silly('PS stdout = %j', stdout)
59+
this.log.silly(e)
60+
return this.failPowershell()
61+
}
62+
63+
if (!Array.isArray(vsInfo)) {
64+
this.log.silly('PS stdout = %j', stdout)
65+
return this.failPowershell()
66+
}
67+
68+
vsInfo = vsInfo.map((info) => {
69+
this.log.silly(`processing installation: "${info.path}"`)
70+
const versionYear = this.getVersionYear(info)
71+
return {
72+
path: info.path,
73+
version: info.version,
74+
versionYear: versionYear,
75+
hasMSBuild: this.getHasMSBuild(info),
76+
toolset: this.getToolset(info, versionYear),
77+
sdk: this.getSDK(info)
78+
}
79+
})
80+
this.log.silly('vsInfo:', vsInfo)
81+
82+
// Remove future versions or errors parsing version number
83+
vsInfo = vsInfo.filter((info) => {
84+
if (info.versionYear) { return true }
85+
this.addLog(`unknown version "${info.version}" found at "${info.path}"`)
86+
return false
87+
})
88+
89+
// Sort to place newer versions first
90+
vsInfo.sort((a, b) => b.versionYear - a.versionYear)
91+
92+
for (var i = 0; i < vsInfo.length; ++i) {
93+
const info = vsInfo[i]
94+
this.addLog(`checking VS${info.versionYear} (${info.version}) found ` +
95+
`at\n"${info.path}"`)
96+
97+
if (info.hasMSBuild) {
98+
this.addLog('- found "Visual Studio C++ core features"')
99+
} else {
100+
this.addLog('- "Visual Studio C++ core features" missing')
101+
continue
102+
}
103+
104+
if (info.toolset) {
105+
this.addLog(`- found VC++ toolset: ${info.toolset}`)
106+
} else {
107+
this.addLog('- missing any VC++ toolset')
108+
continue
109+
}
110+
111+
if (info.sdk) {
112+
this.addLog(`- found Windows SDK: ${info.sdk}`)
113+
} else {
114+
this.addLog('- missing any Windows SDK')
115+
continue
116+
}
117+
118+
this.succeed(info)
119+
return
120+
}
121+
122+
this.fail()
123+
},
124+
125+
succeed: function succeed (info) {
126+
this.log.info(`using VS${info.versionYear} (${info.version}) found ` +
127+
`at\n"${info.path}"`)
128+
process.nextTick(this.callback.bind(null, null, info))
129+
},
130+
131+
failPowershell: function failPowershell () {
132+
process.nextTick(this.callback.bind(null, new Error(
133+
'Could not use PowerShell to find Visual Studio')))
134+
},
135+
136+
fail: function fail () {
137+
const errorLog = this.errorLog.join('\n')
138+
139+
// For Windows 80 col console, use up to the column before the one marked
140+
// with X (total 79 chars including logger prefix, 62 chars usable here):
141+
// X
142+
const infoLog = [
143+
'**************************************************************',
144+
'You need to install the latest version of Visual Studio',
145+
'including the "Desktop development with C++" workload.',
146+
'For more information consult the documentation at:',
147+
'https://github.com/nodejs/node-gyp#on-windows',
148+
'**************************************************************'
149+
].join('\n')
150+
151+
this.log.error(`\n${errorLog}\n\n${infoLog}\n`)
152+
process.nextTick(this.callback.bind(null, new Error(
153+
'Could not find any Visual Studio installation to use')))
154+
},
155+
156+
getVersionYear: function getVersionYear (info) {
157+
const version = parseInt(info.version, 10)
158+
if (version === 15) {
159+
return 2017
160+
}
161+
this.log.silly('- failed to parse version:', info.version)
162+
return null
163+
},
164+
165+
getHasMSBuild: function getHasMSBuild (info) {
166+
const pkg = 'Microsoft.VisualStudio.VC.MSBuild.Base'
167+
return info.packages.indexOf(pkg) !== -1
168+
},
169+
170+
getToolset: function getToolset (info, versionYear) {
171+
const pkg = 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64'
172+
if (info.packages.indexOf(pkg) !== -1) {
173+
this.log.silly('- found VC.Tools.x86.x64')
174+
if (versionYear === 2017) {
175+
return 'v141'
176+
}
177+
}
178+
return null
179+
},
180+
181+
getSDK: function getSDK (info) {
182+
const win8SDK = 'Microsoft.VisualStudio.Component.Windows81SDK'
183+
const win10SDKPrefix = 'Microsoft.VisualStudio.Component.Windows10SDK.'
184+
185+
var Win10SDKVer = 0
186+
info.packages.forEach((pkg) => {
187+
if (!pkg.startsWith(win10SDKPrefix)) {
188+
return
189+
}
190+
const parts = pkg.split('.')
191+
if (parts.length > 5 && parts[5] !== 'Desktop') {
192+
this.log.silly('- ignoring non-Desktop Win10SDK:', pkg)
193+
return
194+
}
195+
const foundSdkVer = parseInt(parts[4], 10)
196+
if (isNaN(foundSdkVer)) {
197+
// Microsoft.VisualStudio.Component.Windows10SDK.IpOverUsb
198+
this.log.silly('- failed to parse Win10SDK number:', pkg)
199+
return
200+
}
201+
this.log.silly('- found Win10SDK:', foundSdkVer)
202+
Win10SDKVer = Math.max(Win10SDKVer, foundSdkVer)
203+
})
204+
205+
if (Win10SDKVer !== 0) {
206+
return `10.0.${Win10SDKVer}.0`
207+
} else if (info.packages.indexOf(win8SDK) !== -1) {
208+
this.log.silly('- found Win8SDK')
209+
return '8.1'
210+
}
211+
return null
212+
}
213+
}

0 commit comments

Comments
 (0)