Skip to content

Commit

Permalink
win: generic Visual Studio 2017 detection
Browse files Browse the repository at this point in the history
PR-URL: #1762
Reviewed-By: Rod Vagg <rod@vagg.org>
Reviewed-By: Refael Ackermann <refack@gmail.com>
  • Loading branch information
joaocgreis committed Jun 4, 2019
1 parent 721eb69 commit 7fe4095
Show file tree
Hide file tree
Showing 9 changed files with 387 additions and 118 deletions.
88 changes: 29 additions & 59 deletions lib/Find-VS2017.cs → lib/Find-VisualStudio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -184,90 +187,57 @@ public class SetupConfigurationClass

public static class Main
{
public static void Query()
public static void PrintJson()
{
ISetupConfiguration query = new SetupConfiguration();
ISetupConfiguration2 query2 = (ISetupConfiguration2)query;
IEnumSetupInstances e = query2.EnumAllInstances();

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("{");

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 path = JsonString(setupInstance2.GetInstallationPath());
json.Append(String.Format("\"path\":{0},", path));

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");
string version = JsonString(setupInstance2.GetInstallationVersion());
json.Append(String.Format("\"version\":{0},", version));

if (hasMSBuild && hasVCTools)
List<string> packages = new List<string>();
foreach (ISetupPackageReference package in setupInstance2.GetPackages())
{
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;
}
string id = JsonString(package.GetId());
packages.Add(id);
}
json.Append(String.Format("\"packages\":[{0}]", string.Join(",", packages.ToArray())));

log.Append(" - Some required components are missing, not using this installation\\n");
return false;
json.Append("}");
return json.ToString();
}
}
}
18 changes: 3 additions & 15 deletions lib/configure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 changes: 213 additions & 0 deletions lib/find-visualstudio.js
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 7fe4095

Please sign in to comment.