win: generic Visual Studio 2017 detection

PR-URL: https://github.com/nodejs/node-gyp/pull/1762
Reviewed-By: Rod Vagg <rod@vagg.org>
Reviewed-By: Refael Ackermann <refack@gmail.com>
This commit is contained in:
João Reis 2019-05-24 12:26:31 +01:00
parent 721eb691cf
commit 7fe4095974
9 changed files with 388 additions and 119 deletions

View file

@ -3,10 +3,13 @@
// See accompanying file LICENSE at https://github.com/node4good/windows-autoconf
// Usage:
// powershell -ExecutionPolicy Unrestricted -Version "2.0" -Command "&{Add-Type -Path Find-VS2017.cs; [VisualStudioConfiguration.Main]::Query()}"
// powershell -ExecutionPolicy Unrestricted -Command "Add-Type -Path Find-VisualStudio.cs; [VisualStudioConfiguration.Main]::PrintJson()"
// This script needs to be compatible with PowerShell v2 to run on Windows 2008R2 and Windows 7.
using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Collections.Generic;
namespace VisualStudioConfiguration
{
@ -184,7 +187,7 @@ namespace VisualStudioConfiguration
public static class Main
{
public static void Query()
public static void PrintJson()
{
ISetupConfiguration query = new SetupConfiguration();
ISetupConfiguration2 query2 = (ISetupConfiguration2)query;
@ -192,82 +195,49 @@ namespace VisualStudioConfiguration
int pceltFetched;
ISetupInstance2[] rgelt = new ISetupInstance2[1];
StringBuilder log = new StringBuilder();
List<string> instances = new List<string>();
while (true)
{
e.Next(1, rgelt, out pceltFetched);
if (pceltFetched <= 0)
{
Console.WriteLine(String.Format("{{\"log\":\"{0}\"}}", log.ToString()));
Console.WriteLine(String.Format("[{0}]", string.Join(",", instances.ToArray())));
return;
}
if (CheckInstance(rgelt[0], ref log))
return;
instances.Add(InstanceJson(rgelt[0]));
}
}
private static bool CheckInstance(ISetupInstance2 setupInstance2, ref StringBuilder log)
private static string JsonString(string s)
{
// Visual Studio Community 2017 component directory:
// https://www.visualstudio.com/en-us/productinfo/vs2017-install-product-Community.workloads
return "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
}
string path = setupInstance2.GetInstallationPath().Replace("\\", "\\\\");
log.Append(String.Format("Found installation at: {0}\\n", path));
private static string InstanceJson(ISetupInstance2 setupInstance2)
{
// Visual Studio component directory:
// https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids
bool hasMSBuild = false;
bool hasVCTools = false;
uint Win10SDKVer = 0;
bool hasWin8SDK = false;
StringBuilder json = new StringBuilder();
json.Append("{");
string path = JsonString(setupInstance2.GetInstallationPath());
json.Append(String.Format("\"path\":{0},", path));
string version = JsonString(setupInstance2.GetInstallationVersion());
json.Append(String.Format("\"version\":{0},", version));
List<string> packages = new List<string>();
foreach (ISetupPackageReference package in setupInstance2.GetPackages())
{
const string Win10SDKPrefix = "Microsoft.VisualStudio.Component.Windows10SDK.";
string id = package.GetId();
if (id == "Microsoft.VisualStudio.VC.MSBuild.Base")
hasMSBuild = true;
else if (id == "Microsoft.VisualStudio.Component.VC.Tools.x86.x64")
hasVCTools = true;
else if (id.StartsWith(Win10SDKPrefix)) {
string[] parts = id.Substring(Win10SDKPrefix.Length).Split('.');
if (parts.Length > 1 && parts[1] != "Desktop")
continue;
uint foundSdkVer;
if (UInt32.TryParse(parts[0], out foundSdkVer))
Win10SDKVer = Math.Max(Win10SDKVer, foundSdkVer);
} else if (id == "Microsoft.VisualStudio.Component.Windows81SDK")
hasWin8SDK = true;
else
continue;
log.Append(String.Format(" - Found {0}\\n", id));
string id = JsonString(package.GetId());
packages.Add(id);
}
json.Append(String.Format("\"packages\":[{0}]", string.Join(",", packages.ToArray())));
if (!hasMSBuild)
log.Append(" - Missing Visual Studio C++ core features (Microsoft.VisualStudio.VC.MSBuild.Base)\\n");
if (!hasVCTools)
log.Append(" - Missing VC++ 2017 v141 toolset (x86,x64) (Microsoft.VisualStudio.Component.VC.Tools.x86.x64)\\n");
if ((Win10SDKVer == 0) && (!hasWin8SDK))
log.Append(" - Missing a Windows SDK (Microsoft.VisualStudio.Component.Windows10SDK.* or Microsoft.VisualStudio.Component.Windows81SDK)\\n");
if (hasMSBuild && hasVCTools)
{
if (Win10SDKVer > 0)
{
log.Append(" - Using this installation with Windows 10 SDK"/*\\n*/);
Console.WriteLine(String.Format("{{\"log\":\"{0}\",\"path\":\"{1}\",\"sdk\":\"10.0.{2}.0\"}}", log.ToString(), path, Win10SDKVer));
return true;
}
else if (hasWin8SDK)
{
log.Append(" - Using this installation with Windows 8.1 SDK"/*\\n*/);
Console.WriteLine(String.Format("{{\"log\":\"{0}\",\"path\":\"{1}\",\"sdk\":\"8.1\"}}", log.ToString(), path));
return true;
}
}
log.Append(" - Some required components are missing, not using this installation\\n");
return false;
json.Append("}");
return json.ToString();
}
}
}

View file

@ -17,8 +17,9 @@ var fs = require('graceful-fs')
, win = process.platform === 'win32'
, findNodeDirectory = require('./find-node-directory')
, msgFormat = require('util').format
, logWithPrefix = require('./util').logWithPrefix
if (win)
var findVS2017 = require('./find-vs2017')
var findVisualStudio = require('./find-visualstudio')
exports.usage = 'Generates ' + (win ? 'MSVC project files' : 'a Makefile') + ' for the current module'
@ -83,7 +84,7 @@ function configure (gyp, argv, callback) {
if (err) return callback(err)
log.verbose('build dir', '"build" dir needed to be created?', isNew)
if (win && (!gyp.opts.msvs_version || gyp.opts.msvs_version === '2017')) {
findVS2017(function (err, vsSetup) {
findVisualStudio(function (err, vsSetup) {
if (err) {
log.verbose('Not using VS2017:', err.message)
createConfigFile()
@ -644,16 +645,3 @@ function findPython (configPython, callback) {
var finder = new PythonFinder(configPython, callback)
finder.findPython()
}
function logWithPrefix (log, prefix) {
function setPrefix(logFunction) {
return (...args) => logFunction.apply(null, [prefix, ...args])
}
return {
silly: setPrefix(log.silly),
verbose: setPrefix(log.verbose),
info: setPrefix(log.info),
warn: setPrefix(log.warn),
error: setPrefix(log.error),
}
}

213
lib/find-visualstudio.js Normal file
View file

@ -0,0 +1,213 @@
module.exports = exports = findVisualStudio
module.exports.test = {
VisualStudioFinder: VisualStudioFinder,
findVisualStudio: findVisualStudio
}
const log = require('npmlog')
const execFile = require('child_process').execFile
const path = require('path').win32
const logWithPrefix = require('./util').logWithPrefix
function findVisualStudio (callback) {
const finder = new VisualStudioFinder(callback)
finder.findVisualStudio()
}
function VisualStudioFinder (callback) {
this.callback = callback
this.errorLog = []
}
VisualStudioFinder.prototype = {
log: logWithPrefix(log, 'find VS'),
// Logs a message at verbose level, but also saves it to be displayed later
// at error level if an error occurs. This should help diagnose the problem.
addLog: function addLog (message) {
this.log.verbose(message)
this.errorLog.push(message)
},
findVisualStudio: function findVisualStudio () {
var ps = path.join(process.env.SystemRoot, 'System32',
'WindowsPowerShell', 'v1.0', 'powershell.exe')
var csFile = path.join(__dirname, 'Find-VisualStudio.cs')
var psArgs = ['-ExecutionPolicy', 'Unrestricted', '-NoProfile',
'-Command', '&{Add-Type -Path \'' + csFile + '\';' +
'[VisualStudioConfiguration.Main]::PrintJson()}']
this.log.silly('Running', ps, psArgs)
var child = execFile(ps, psArgs, { encoding: 'utf8' },
this.parseData.bind(this))
child.stdin.end()
},
parseData: function parseData (err, stdout, stderr) {
this.log.silly('PS stderr = %j', stderr)
if (err) {
this.log.silly('PS err = %j', err && (err.stack || err))
return this.failPowershell()
}
var vsInfo
try {
vsInfo = JSON.parse(stdout)
} catch (e) {
this.log.silly('PS stdout = %j', stdout)
this.log.silly(e)
return this.failPowershell()
}
if (!Array.isArray(vsInfo)) {
this.log.silly('PS stdout = %j', stdout)
return this.failPowershell()
}
vsInfo = vsInfo.map((info) => {
this.log.silly(`processing installation: "${info.path}"`)
const versionYear = this.getVersionYear(info)
return {
path: info.path,
version: info.version,
versionYear: versionYear,
hasMSBuild: this.getHasMSBuild(info),
toolset: this.getToolset(info, versionYear),
sdk: this.getSDK(info)
}
})
this.log.silly('vsInfo:', vsInfo)
// Remove future versions or errors parsing version number
vsInfo = vsInfo.filter((info) => {
if (info.versionYear) { return true }
this.addLog(`unknown version "${info.version}" found at "${info.path}"`)
return false
})
// Sort to place newer versions first
vsInfo.sort((a, b) => b.versionYear - a.versionYear)
for (var i = 0; i < vsInfo.length; ++i) {
const info = vsInfo[i]
this.addLog(`checking VS${info.versionYear} (${info.version}) found ` +
`at\n"${info.path}"`)
if (info.hasMSBuild) {
this.addLog('- found "Visual Studio C++ core features"')
} else {
this.addLog('- "Visual Studio C++ core features" missing')
continue
}
if (info.toolset) {
this.addLog(`- found VC++ toolset: ${info.toolset}`)
} else {
this.addLog('- missing any VC++ toolset')
continue
}
if (info.sdk) {
this.addLog(`- found Windows SDK: ${info.sdk}`)
} else {
this.addLog('- missing any Windows SDK')
continue
}
this.succeed(info)
return
}
this.fail()
},
succeed: function succeed (info) {
this.log.info(`using VS${info.versionYear} (${info.version}) found ` +
`at\n"${info.path}"`)
process.nextTick(this.callback.bind(null, null, info))
},
failPowershell: function failPowershell () {
process.nextTick(this.callback.bind(null, new Error(
'Could not use PowerShell to find Visual Studio')))
},
fail: function fail () {
const errorLog = this.errorLog.join('\n')
// For Windows 80 col console, use up to the column before the one marked
// with X (total 79 chars including logger prefix, 62 chars usable here):
// X
const infoLog = [
'**************************************************************',
'You need to install the latest version of Visual Studio',
'including the "Desktop development with C++" workload.',
'For more information consult the documentation at:',
'https://github.com/nodejs/node-gyp#on-windows',
'**************************************************************'
].join('\n')
this.log.error(`\n${errorLog}\n\n${infoLog}\n`)
process.nextTick(this.callback.bind(null, new Error(
'Could not find any Visual Studio installation to use')))
},
getVersionYear: function getVersionYear (info) {
const version = parseInt(info.version, 10)
if (version === 15) {
return 2017
}
this.log.silly('- failed to parse version:', info.version)
return null
},
getHasMSBuild: function getHasMSBuild (info) {
const pkg = 'Microsoft.VisualStudio.VC.MSBuild.Base'
return info.packages.indexOf(pkg) !== -1
},
getToolset: function getToolset (info, versionYear) {
const pkg = 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64'
if (info.packages.indexOf(pkg) !== -1) {
this.log.silly('- found VC.Tools.x86.x64')
if (versionYear === 2017) {
return 'v141'
}
}
return null
},
getSDK: function getSDK (info) {
const win8SDK = 'Microsoft.VisualStudio.Component.Windows81SDK'
const win10SDKPrefix = 'Microsoft.VisualStudio.Component.Windows10SDK.'
var Win10SDKVer = 0
info.packages.forEach((pkg) => {
if (!pkg.startsWith(win10SDKPrefix)) {
return
}
const parts = pkg.split('.')
if (parts.length > 5 && parts[5] !== 'Desktop') {
this.log.silly('- ignoring non-Desktop Win10SDK:', pkg)
return
}
const foundSdkVer = parseInt(parts[4], 10)
if (isNaN(foundSdkVer)) {
// Microsoft.VisualStudio.Component.Windows10SDK.IpOverUsb
this.log.silly('- failed to parse Win10SDK number:', pkg)
return
}
this.log.silly('- found Win10SDK:', foundSdkVer)
Win10SDKVer = Math.max(Win10SDKVer, foundSdkVer)
})
if (Win10SDKVer !== 0) {
return `10.0.${Win10SDKVer}.0`
} else if (info.packages.indexOf(win8SDK) !== -1) {
this.log.silly('- found Win8SDK')
return '8.1'
}
return null
}
}

View file

@ -1,44 +0,0 @@
var log = require('npmlog')
, execFile = require('child_process').execFile
, path = require('path')
module.exports = function findVS2017(callback) {
var ps = path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell',
'v1.0', 'powershell.exe')
var csFile = path.join(__dirname, 'Find-VS2017.cs')
var psArgs = ['-ExecutionPolicy', 'Unrestricted', '-NoProfile',
'-Command', '&{Add-Type -Path \'' + csFile + '\';' +
'[VisualStudioConfiguration.Main]::Query()}']
log.silly('find vs2017', 'Running', ps, psArgs)
var child = execFile(ps, psArgs, { encoding: 'utf8' },
function (err, stdout, stderr) {
log.silly('find vs2017', 'PS err:', err)
log.silly('find vs2017', 'PS stdout:', stdout)
log.silly('find vs2017', 'PS stderr:', stderr)
if (err)
return callback(new Error('Could not use PowerShell to find VS2017'))
var vsSetup
try {
vsSetup = JSON.parse(stdout)
} catch (e) {
log.silly('find vs2017', e)
return callback(new Error('Could not use PowerShell to find VS2017'))
}
log.silly('find vs2017', 'vsSetup:', vsSetup)
if (vsSetup && vsSetup.log)
log.verbose('find vs2017', vsSetup.log.trimRight())
if (!vsSetup || !vsSetup.path || !vsSetup.sdk) {
return callback(new Error('No usable installation of VS2017 found'))
}
log.verbose('find vs2017', 'using installation:', vsSetup.path)
callback(null, { "path": vsSetup.path, "sdk": vsSetup.sdk })
})
child.stdin.end()
}

12
lib/util.js Normal file
View file

@ -0,0 +1,12 @@
module.exports.logWithPrefix = function logWithPrefix (log, prefix) {
function setPrefix(logFunction) {
return (...args) => logFunction.apply(null, [prefix, ...args])
}
return {
silly: setPrefix(log.silly),
verbose: setPrefix(log.verbose),
info: setPrefix(log.info),
warn: setPrefix(log.warn),
error: setPrefix(log.error),
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
test/fixtures/VS_2017_Unusable.txt vendored Normal file
View file

@ -0,0 +1 @@
[{"path":"C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\BuildTools","version":"15.9.28307.665","packages":["Microsoft.VisualStudio.Product.BuildTools","Microsoft.VisualStudio.Component.Windows10SDK.17134","Win10SDK_10.0.17134","Microsoft.VisualStudio.Component.VC.Tools.x86.x64","Microsoft.VisualCpp.CodeAnalysis.Extensions","Microsoft.VisualCpp.CodeAnalysis.Extensions.X86","Microsoft.VisualCpp.CodeAnalysis.ConcurrencyCheck.X86","Microsoft.VisualCpp.CodeAnalysis.ConcurrencyCheck.X86.Resources","Microsoft.VisualCpp.CodeAnalysis.Extensions.X64","Microsoft.VisualCpp.CodeAnalysis.ConcurrencyCheck.X64","Microsoft.VisualCpp.CodeAnalysis.ConcurrencyCheck.X64.Resources","Microsoft.VisualStudio.Component.Static.Analysis.Tools","Microsoft.VisualStudio.StaticAnalysis","Microsoft.VisualStudio.StaticAnalysis.Resources","Microsoft.VisualCpp.Tools.HostX64.TargetX86","Microsoft.VisualCpp.VCTip.HostX64.TargetX86","Microsoft.VisualCpp.Tools.HostX64.TargetX86.Resources","Microsoft.VisualCpp.Tools.HostX64.TargetX64","Microsoft.VisualCpp.VCTip.HostX64.TargetX64","Microsoft.VisualCpp.Tools.HostX64.TargetX64.Resources","Microsoft.VisualCpp.Premium.Tools.HostX86.TargetX64","Microsoft.VisualCpp.Premium.Tools.Hostx86.Targetx64.Resources","Microsoft.VisualCpp.Premium.Tools.HostX86.TargetX86","Microsoft.VisualCpp.Premium.Tools.HostX86.TargetX86.Resources","Microsoft.VisualCpp.Premium.Tools.HostX64.TargetX86","Microsoft.VisualCpp.Premium.Tools.HostX64.TargetX86.Resources","Microsoft.VisualCpp.Premium.Tools.HostX64.TargetX64","Microsoft.VisualCpp.Premium.Tools.HostX64.TargetX64.Resources","Microsoft.VisualCpp.PGO.X86","Microsoft.VisualCpp.PGO.X64","Microsoft.VisualCpp.PGO.Headers","Microsoft.VisualCpp.CRT.x86.Store","Microsoft.VisualCpp.CRT.x86.OneCore.Desktop","Microsoft.VisualCpp.CRT.x64.Store","Microsoft.VisualCpp.CRT.x64.OneCore.Desktop","Microsoft.VisualCpp.CRT.Redist.x86.OneCore.Desktop","Microsoft.VisualCpp.CRT.Redist.x64.OneCore.Desktop","Microsoft.VisualCpp.CRT.ClickOnce.Msi","Microsoft.VisualStudio.PackageGroup.VC.Tools.x86","Microsoft.VisualCpp.Tools.HostX86.TargetX64","Microsoft.VisualCpp.VCTip.hostX86.targetX64","Microsoft.VisualCpp.Tools.Hostx86.Targetx64.Resources","Microsoft.VisualCpp.Tools.HostX86.TargetX86","Microsoft.VisualCpp.VCTip.hostX86.targetX86","Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources","Microsoft.VisualCpp.Tools.Core.Resources","Microsoft.VisualCpp.Tools.Core.x86","Microsoft.VisualCpp.Tools.Common.Utils","Microsoft.VisualCpp.Tools.Common.Utils.Resources","Microsoft.VisualCpp.DIA.SDK","Microsoft.VisualCpp.CRT.x86.Desktop","Microsoft.VisualCpp.CRT.x64.Desktop","Microsoft.VisualCpp.CRT.Source","Microsoft.VisualCpp.CRT.Redist.X86","Microsoft.VisualCpp.CRT.Redist.X64","Microsoft.VisualCpp.CRT.Redist.Resources","Microsoft.VisualCpp.RuntimeDebug.14","Microsoft.VisualCpp.RuntimeDebug.14","Microsoft.VisualCpp.Redist.14","Microsoft.VisualCpp.Redist.14","Microsoft.VisualCpp.CRT.Headers","Microsoft.VisualStudio.Workload.MSBuildTools","Microsoft.VisualStudio.Component.CoreBuildTools","Microsoft.VisualStudio.Setup.Configuration","Microsoft.VisualStudio.PackageGroup.VsDevCmd","Microsoft.VisualStudio.VsDevCmd.Ext.NetFxSdk","Microsoft.VisualStudio.VsDevCmd.Core.WinSdk","Microsoft.VisualStudio.VsDevCmd.Core.DotNet","Microsoft.VisualStudio.VC.DevCmd","Microsoft.VisualStudio.VC.DevCmd.Resources","Microsoft.VisualStudio.BuildTools.Resources","Microsoft.VisualStudio.Net.Eula.Resources","Microsoft.Build.Dependencies","Microsoft.Build.FileTracker.Msi","Microsoft.Component.MSBuild","Microsoft.PythonTools.BuildCore.Vsix","Microsoft.NuGet.Build.Tasks","Microsoft.VisualStudio.Component.Roslyn.Compiler","Microsoft.CodeAnalysis.Compilers.Resources","Microsoft.CodeAnalysis.Compilers","Microsoft.Net.PackageGroup.4.6.1.Redist","Microsoft.Net.4.6.1.FullRedist.NonThreshold","Microsoft.Windows.UniversalCRT.Msu.81","Microsoft.VisualStudio.NativeImageSupport","Microsoft.Build"]}]

View file

@ -0,0 +1,127 @@
'use strict'
const test = require('tape')
const fs = require('fs')
const path = require('path')
const findVisualStudio = require('../lib/find-visualstudio')
const VisualStudioFinder = findVisualStudio.test.VisualStudioFinder
test('empty output', function (t) {
t.plan(2)
const finder = new VisualStudioFinder(function (err, info) {
t.ok(/se PowerShell/i.test(err), 'expect error')
t.false(info, 'no data')
})
finder.parseData(null, '', '')
})
test('output not JSON', function (t) {
t.plan(2)
const finder = new VisualStudioFinder(function (err, info) {
t.ok(/use PowerShell/i.test(err), 'expect error')
t.false(info, 'no data')
})
finder.parseData(null, 'AAAABBBB', '')
})
test('wrong JSON', function (t) {
t.plan(2)
const finder = new VisualStudioFinder(function (err, info) {
t.ok(/use PowerShell/i.test(err), 'expect error')
t.false(info, 'no data')
})
finder.parseData(null, '{}', '')
})
test('empty JSON', function (t) {
t.plan(2)
const finder = new VisualStudioFinder(function (err, info) {
t.ok(/find any Visual Studio/i.test(err), 'expect error')
t.false(info, 'no data')
})
finder.parseData(null, '[]', '')
})
test('future version', function (t) {
t.plan(2)
const finder = new VisualStudioFinder(function (err, info) {
t.ok(/find any Visual Studio/i.test(err), 'expect error')
t.false(info, 'no data')
})
finder.parseData(null, JSON.stringify([{
packages: [
'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
'Microsoft.VisualStudio.Component.Windows10SDK.17763',
'Microsoft.VisualStudio.VC.MSBuild.Base'
],
path: 'C:\\VS',
version: '9999.9999.9999.9999'
}]), '')
})
test('single unusable VS2017', function (t) {
t.plan(2)
const finder = new VisualStudioFinder(function (err, info) {
t.ok(/find any Visual Studio/i.test(err), 'expect error')
t.false(info, 'no data')
})
const file = path.join(__dirname, 'fixtures', 'VS_2017_Unusable.txt')
const data = fs.readFileSync(file)
finder.parseData(null, data, '')
})
test('minimal VS2017 Build Tools', function (t) {
t.plan(2)
const finder = new VisualStudioFinder(function (err, info) {
t.strictEqual(err, null)
t.deepEqual(info, {
hasMSBuild: true,
path:
'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\BuildTools',
sdk: '10.0.17134.0',
toolset: 'v141',
version: '15.9.28307.665',
versionYear: 2017
})
})
const file = path.join(__dirname, 'fixtures',
'VS_2017_BuildTools_minimal.txt')
const data = fs.readFileSync(file)
finder.parseData(null, data, '')
})
test('VS2017 Community with C++ workload', function (t) {
t.plan(2)
const finder = new VisualStudioFinder(function (err, info) {
t.strictEqual(err, null)
t.deepEqual(info, {
hasMSBuild: true,
path:
'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community',
sdk: '10.0.17763.0',
toolset: 'v141',
version: '15.9.28307.665',
versionYear: 2017
})
})
const file = path.join(__dirname, 'fixtures',
'VS_2017_Community_workload.txt')
const data = fs.readFileSync(file)
finder.parseData(null, data, '')
})