diff --git a/lib/build.js b/lib/build.js index eeaf680c6b..783ab9c595 100644 --- a/lib/build.js +++ b/lib/build.js @@ -6,7 +6,6 @@ var fs = require('graceful-fs') , glob = require('glob') , log = require('npmlog') , which = require('which') - , exec = require('child_process').exec , win = process.platform === 'win32' exports.usage = 'Invokes `' + (win ? 'msbuild' : 'make') + '` and builds the module' @@ -96,7 +95,11 @@ function build (gyp, argv, callback) { function doWhich () { // On Windows use msbuild provided by node-gyp configure - if (win && config.variables.msbuild_path) { + if (win) { + if (!config.variables.msbuild_path) { + return callback(new Error( + 'MSBuild is not set, please run `node-gyp configure`.')) + } command = config.variables.msbuild_path log.verbose('using MSBuild:', command) doBuild() @@ -105,13 +108,8 @@ function build (gyp, argv, callback) { // First make sure we have the build command in the PATH which(command, function (err, execPath) { if (err) { - if (win && /not found/.test(err.message)) { - // On windows and no 'msbuild' found. Let's guess where it is - findMsbuild() - } else { - // Some other error or 'make' not found on Unix, report that to the user - callback(err) - } + // Some other error or 'make' not found on Unix, report that to the user + callback(err) return } log.verbose('`which` succeeded for `' + command + '`', execPath) @@ -119,66 +117,6 @@ function build (gyp, argv, callback) { }) } - /** - * Search for the location of "msbuild.exe" file on Windows. - */ - - function findMsbuild () { - log.verbose('could not find "msbuild.exe" in PATH - finding location in registry') - var notfoundErr = 'Can\'t find "msbuild.exe". Do you have Microsoft Visual Studio C++ 2008+ installed?' - var cmd = 'reg query "HKLM\\Software\\Microsoft\\MSBuild\\ToolsVersions" /s' - if (process.arch !== 'ia32') - cmd += ' /reg:32' - exec(cmd, function (err, stdout) { - if (err) { - return callback(new Error(err.message + '\n' + notfoundErr)) - } - var reVers = /ToolsVersions\\([^\\]+)$/i - , rePath = /\r\n[ \t]+MSBuildToolsPath[ \t]+REG_SZ[ \t]+([^\r]+)/i - , msbuilds = [] - , r - , msbuildPath - stdout.split('\r\n\r\n').forEach(function(l) { - if (!l) return - l = l.trim() - if (r = reVers.exec(l.substring(0, l.indexOf('\r\n')))) { - var ver = parseFloat(r[1], 10) - if (ver >= 3.5) { - if (r = rePath.exec(l)) { - msbuilds.push({ - version: ver, - path: r[1] - }) - } - } - } - }) - msbuilds.sort(function (x, y) { - return (x.version < y.version ? -1 : 1) - }) - ;(function verifyMsbuild () { - if (!msbuilds.length) return callback(new Error(notfoundErr)) - msbuildPath = path.resolve(msbuilds.pop().path, 'msbuild.exe') - fs.stat(msbuildPath, function (err) { - if (err) { - if (err.code == 'ENOENT') { - if (msbuilds.length) { - return verifyMsbuild() - } else { - callback(new Error(notfoundErr)) - } - } else { - callback(err) - } - return - } - command = msbuildPath - doBuild() - }) - })() - }) - } - /** * Actually spawn the process and compile the module. */ diff --git a/lib/configure.js b/lib/configure.js index 254824f824..6e49c23d91 100644 --- a/lib/configure.js +++ b/lib/configure.js @@ -83,22 +83,16 @@ function configure (gyp, argv, callback) { mkdirp(buildDir, function (err, isNew) { 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')) { - findVisualStudio(function (err, vsSetup) { - if (err) { - log.verbose('Not using VS2017:', err.message) - createConfigFile() - } else { - createConfigFile(null, vsSetup) - } - }) + if (win) { + findVisualStudio(release.semver, gyp.opts.msvs_version, + createConfigFile) } else { createConfigFile() } }) } - function createConfigFile (err, vsSetup) { + function createConfigFile (err, vsInfo) { if (err) return callback(err) var configFilename = 'config.gypi' @@ -145,17 +139,14 @@ function configure (gyp, argv, callback) { // disable -T "thin" static archives by default variables.standalone_static_library = gyp.opts.thin ? 0 : 1 - if (vsSetup) { - // GYP doesn't (yet) have support for VS2017, so we force it to VS2015 - // to avoid pulling a floating patch that has not landed upstream. - // Ref: https://chromium-review.googlesource.com/#/c/433540/ - gyp.opts.msvs_version = '2015' - process.env['GYP_MSVS_VERSION'] = 2015 - process.env['GYP_MSVS_OVERRIDE_PATH'] = vsSetup.path - defaults['msbuild_toolset'] = 'v141' - defaults['msvs_windows_target_platform_version'] = vsSetup.sdk - variables['msbuild_path'] = path.join(vsSetup.path, 'MSBuild', '15.0', - 'Bin', 'MSBuild.exe') + if (win) { + process.env['GYP_MSVS_VERSION'] = Math.min(vsInfo.versionYear, 2015) + process.env['GYP_MSVS_OVERRIDE_PATH'] = vsInfo.path + defaults['msbuild_toolset'] = vsInfo.toolset + if (vsInfo.sdk) { + defaults['msvs_windows_target_platform_version'] = vsInfo.sdk + } + variables['msbuild_path'] = vsInfo.msBuild } // loop through the rest of the opts and add the unknown ones as variables. @@ -221,20 +212,6 @@ function configure (gyp, argv, callback) { } } - function hasMsvsVersion () { - return argv.some(function (arg) { - return arg.indexOf('msvs_version') === 0 - }) - } - - if (win && !hasMsvsVersion()) { - if ('msvs_version' in gyp.opts) { - argv.push('-G', 'msvs_version=' + gyp.opts.msvs_version) - } else { - argv.push('-G', 'msvs_version=auto') - } - } - // include all the ".gypi" files that were found configs.forEach(function (config) { argv.push('-I', config) diff --git a/lib/find-visualstudio.js b/lib/find-visualstudio.js index 624567154f..94cc994d79 100644 --- a/lib/find-visualstudio.js +++ b/lib/find-visualstudio.js @@ -8,20 +8,27 @@ const log = require('npmlog') const execFile = require('child_process').execFile const path = require('path').win32 const logWithPrefix = require('./util').logWithPrefix +const regSearchKeys = require('./util').regSearchKeys -function findVisualStudio (callback) { - const finder = new VisualStudioFinder(callback) +function findVisualStudio (nodeSemver, configMsvsVersion, callback) { + const finder = new VisualStudioFinder(nodeSemver, configMsvsVersion, + callback) finder.findVisualStudio() } -function VisualStudioFinder (callback) { +function VisualStudioFinder (nodeSemver, configMsvsVersion, callback) { + this.nodeSemver = nodeSemver + this.configMsvsVersion = configMsvsVersion this.callback = callback this.errorLog = [] + this.validVersions = [] } VisualStudioFinder.prototype = { log: logWithPrefix(log, 'find VS'), + regSearchKeys: regSearchKeys, + // 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) { @@ -30,6 +37,85 @@ VisualStudioFinder.prototype = { }, findVisualStudio: function findVisualStudio () { + this.configVersionYear = null + this.configPath = null + if (this.configMsvsVersion) { + this.addLog('msvs_version was set from command line or npm config') + if (this.configMsvsVersion.match(/^\d{4}$/)) { + this.configVersionYear = parseInt(this.configMsvsVersion, 10) + this.addLog( + `- looking for Visual Studio version ${this.configVersionYear}`) + } else { + this.configPath = path.resolve(this.configMsvsVersion) + this.addLog( + `- looking for Visual Studio installed in "${this.configPath}"`) + } + } else { + this.addLog('msvs_version not set from command line or npm config') + } + + this.findVisualStudio2017OrNewer((info) => { + if (info) { + return this.succeed(info) + } + this.findVisualStudio2015((info) => { + if (info) { + return this.succeed(info) + } + this.findVisualStudio2013((info) => { + if (info) { + return this.succeed(info) + } + this.fail() + }) + }) + }) + }, + + succeed: function succeed (info) { + this.log.info(`using VS${info.versionYear} (${info.version}) found at:` + + `\n"${info.path}"` + + '\nrun with --verbose for detailed information') + process.nextTick(this.callback.bind(null, null, info)) + }, + + fail: function fail () { + // If msvs_version was specified but finding VS failed, print what would + // have been accepted + if (this.configMsvsVersion) { + this.errorLog.push('') + if (this.validVersions) { + this.errorLog.push('valid versions for msvs_version:') + this.validVersions.forEach((version) => { + this.errorLog.push(`- "${version}"`) + }) + } else { + this.errorLog.push('no valid versions for msvs_version were found') + } + } + + 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'))) + }, + + // Invoke the PowerShell script to get information about Visual Studio 2017 + // or newer installations + findVisualStudio2017OrNewer: function findVisualStudio2017OrNewer (cb) { var ps = path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe') var csFile = path.join(__dirname, 'Find-VisualStudio.cs') @@ -39,16 +125,26 @@ VisualStudioFinder.prototype = { this.log.silly('Running', ps, psArgs) var child = execFile(ps, psArgs, { encoding: 'utf8' }, - this.parseData.bind(this)) + (err, stdout, stderr) => { + this.parseData(err, stdout, stderr, cb) + }) child.stdin.end() }, - parseData: function parseData (err, stdout, stderr) { + // Parse the output of the PowerShell script and look for an installation + // of Visual Studio 2017 or newer to use + parseData: function parseData (err, stdout, stderr, cb) { this.log.silly('PS stderr = %j', stderr) + const failPowershell = () => { + this.addLog( + 'could not use PowerShell to find Visual Studio 2017 or newer') + cb(null) + } + if (err) { this.log.silly('PS err = %j', err && (err.stack || err)) - return this.failPowershell() + return failPowershell() } var vsInfo @@ -57,25 +153,22 @@ VisualStudioFinder.prototype = { } catch (e) { this.log.silly('PS stdout = %j', stdout) this.log.silly(e) - return this.failPowershell() + return failPowershell() } if (!Array.isArray(vsInfo)) { this.log.silly('PS stdout = %j', stdout) - return this.failPowershell() + return 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) - } + var ret = this.getVersionInfo(info) + ret.path = info.path + ret.msBuild = this.getMSBuild(info, ret.versionYear) + ret.toolset = this.getToolset(info, ret.versionYear) + ret.sdk = this.getSDK(info) + return ret }) this.log.silly('vsInfo:', vsInfo) @@ -92,9 +185,9 @@ VisualStudioFinder.prototype = { 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}"`) + `at:\n"${info.path}"`) - if (info.hasMSBuild) { + if (info.msBuild) { this.addLog('- found "Visual Studio C++ core features"') } else { this.addLog('- "Visual Studio C++ core features" missing') @@ -115,58 +208,52 @@ VisualStudioFinder.prototype = { 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') + if (!this.checkConfigVersion(info.versionYear, info.path)) { + continue + } - // 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') + return cb(info) + } - 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'))) + this.addLog( + 'could not find a version of Visual Studio 2017 or newer to use') + cb(null) }, - getVersionYear: function getVersionYear (info) { - const version = parseInt(info.version, 10) - if (version === 15) { - return 2017 + // Helper - process version information + getVersionInfo: function getVersionInfo (info) { + const match = /^(\d+)\.(\d+)\..*/.exec(info.version) + if (!match) { + this.log.silly('- failed to parse version:', info.version) + return {} } - this.log.silly('- failed to parse version:', info.version) - return null + this.log.silly('- version match = %j', match) + var ret = { + version: info.version, + versionMajor: parseInt(match[1], 10), + versionMinor: parseInt(match[2], 10) + } + if (ret.versionMajor === 15) { + ret.versionYear = 2017 + return ret + } + this.log.silly('- unsupported version:', ret.versionMajor) + return {} }, - getHasMSBuild: function getHasMSBuild (info) { + // Helper - process MSBuild information + getMSBuild: function getMSBuild (info, versionYear) { const pkg = 'Microsoft.VisualStudio.VC.MSBuild.Base' - return info.packages.indexOf(pkg) !== -1 + if (info.packages.indexOf(pkg) !== -1) { + this.log.silly('- found VC.MSBuild.Base') + if (versionYear === 2017) { + return path.join(info.path, 'MSBuild', '15.0', 'Bin', 'MSBuild.exe') + } + } + return null }, + // Helper - process toolset information getToolset: function getToolset (info, versionYear) { const pkg = 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64' if (info.packages.indexOf(pkg) !== -1) { @@ -178,6 +265,7 @@ VisualStudioFinder.prototype = { return null }, + // Helper - process Windows SDK information getSDK: function getSDK (info) { const win8SDK = 'Microsoft.VisualStudio.Component.Windows81SDK' const win10SDKPrefix = 'Microsoft.VisualStudio.Component.Windows10SDK.' @@ -209,5 +297,93 @@ VisualStudioFinder.prototype = { return '8.1' } return null + }, + + // Find an installation of Visual Studio 2015 to use + findVisualStudio2015: function findVisualStudio2015 (cb) { + return this.findOldVS({ + version: '14.0', + versionMajor: 14, + versionMinor: 0, + versionYear: 2015, + toolset: 'v140' + }, cb) + }, + + // Find an installation of Visual Studio 2013 to use + findVisualStudio2013: function findVisualStudio2013 (cb) { + if (this.nodeSemver.major >= 9) { + this.addLog( + 'not looking for VS2013 as it is only supported up to Node.js 8') + return cb(null) + } + return this.findOldVS({ + version: '12.0', + versionMajor: 12, + versionMinor: 0, + versionYear: 2013, + toolset: 'v120' + }, cb) + }, + + // Helper - common code for VS2013 and VS2015 + findOldVS: function findOldVS (info, cb) { + const regVC7 = ['HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7', + 'HKLM\\Software\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VC7'] + const regMSBuild = 'HKLM\\Software\\Microsoft\\MSBuild\\ToolsVersions' + + this.addLog(`looking for Visual Studio ${info.versionYear}`) + this.regSearchKeys(regVC7, info.version, [], (err, res) => { + if (err) { + this.addLog('- not found') + return cb(null) + } + + const vsPath = path.resolve(res, '..') + this.addLog(`- found in "${vsPath}"`) + + const msBuildRegOpts = process.arch === 'ia32' ? [] : ['/reg:32'] + this.regSearchKeys([`${regMSBuild}\\${info.version}`], + 'MSBuildToolsPath', msBuildRegOpts, (err, res) => { + if (err) { + this.addLog( + '- could not find MSBuild in registry for this version') + return cb(null) + } + + const msBuild = path.join(res, 'MSBuild.exe') + this.addLog(`- MSBuild in "${msBuild}"`) + + if (!this.checkConfigVersion(info.versionYear, vsPath)) { + return cb(null) + } + + info.path = vsPath + info.msBuild = msBuild + info.sdk = null + cb(info) + }) + }) + }, + + // After finding a usable version of Visual Stuido: + // - add it to validVersions to be displayed at the end if a specific + // version was requested and not found; + // - check if this is the version that was requested. + checkConfigVersion: function checkConfigVersion (versionYear, vsPath) { + this.validVersions.push(versionYear) + this.validVersions.push(vsPath) + + if (this.configVersionYear && + this.configVersionYear !== versionYear) { + this.addLog('- not looking for this version') + return false + } + if (this.configPath && this.configPath !== vsPath) { + this.addLog('- not looking for this installation') + return false + } + + return true } } diff --git a/lib/util.js b/lib/util.js index cb12b936ef..ac6a875354 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,4 +1,14 @@ -module.exports.logWithPrefix = function logWithPrefix (log, prefix) { +module.exports = { + logWithPrefix: logWithPrefix, + regGetValue: regGetValue, + regSearchKeys: regSearchKeys +} + +const log = require('npmlog') +const execFile = require('child_process').execFile +const path = require('path') + +function logWithPrefix (log, prefix) { function setPrefix(logFunction) { return (...args) => logFunction.apply(null, [prefix, ...args]) } @@ -10,3 +20,43 @@ module.exports.logWithPrefix = function logWithPrefix (log, prefix) { error: setPrefix(log.error), } } + +function regGetValue (key, value, addOpts, cb) { + const outReValue = value.replace(/\W/g, '.') + const outRe = new RegExp(`^\\s+${outReValue}\\s+REG_\\w+\\s+(\\S.*)$`, 'im') + const reg = path.join(process.env.SystemRoot, 'System32', 'reg.exe') + const regArgs = ['query', key, '/v', value].concat(addOpts) + + log.silly('reg', 'running', reg, regArgs) + const child = execFile(reg, regArgs, { encoding: 'utf8' }, + function (err, stdout, stderr) { + log.silly('reg', 'reg.exe stdout = %j', stdout) + if (err || stderr.trim() !== '') { + log.silly('reg', 'reg.exe err = %j', err && (err.stack || err)) + log.silly('reg', 'reg.exe stderr = %j', stderr) + return cb(err, stderr) + } + + const result = outRe.exec(stdout) + if (!result) { + log.silly('reg', 'error parsing stdout') + return cb(new Error('Could not parse output of reg.exe')) + } + log.silly('reg', 'found: %j', result[1]) + cb(null, result[1]) + }) + child.stdin.end() +} + +function regSearchKeys (keys, value, addOpts, cb) { + var i = 0 + const search = () => { + log.silly('reg-search', 'looking for %j in %j', value, keys[i]) + regGetValue(keys[i], value, addOpts, (err, res) => { + ++i + if (err && i < keys.length) { return search() } + cb(err, res) + }) + } + search() +} diff --git a/test/test-find-visualstudio.js b/test/test-find-visualstudio.js index d7bb1d85a7..507db96792 100644 --- a/test/test-find-visualstudio.js +++ b/test/test-find-visualstudio.js @@ -6,57 +6,206 @@ const path = require('path') const findVisualStudio = require('../lib/find-visualstudio') const VisualStudioFinder = findVisualStudio.test.VisualStudioFinder -test('empty output', function (t) { - t.plan(2) +const semverV1 = { major: 1, minor: 0, patch: 0 } - const finder = new VisualStudioFinder(function (err, info) { - t.ok(/se PowerShell/i.test(err), 'expect error') - t.false(info, 'no data') +function poison (object, property) { + function fail () { + console.error(Error(`Property ${property} should not have been accessed.`)) + process.abort() + } + var descriptor = { + configurable: false, + enumerable: false, + get: fail, + set: fail + } + Object.defineProperty(object, property, descriptor) +} + +function TestVisualStudioFinder () { VisualStudioFinder.apply(this, arguments) } +TestVisualStudioFinder.prototype = Object.create(VisualStudioFinder.prototype) +// Silence npmlog - remove for debugging +TestVisualStudioFinder.prototype.log = { + silly: () => {}, + verbose: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} +} + +test('VS2013', function (t) { + t.plan(4) + + const finder = new TestVisualStudioFinder(semverV1, null, (err, info) => { + t.strictEqual(err, null) + t.deepEqual(info, { + msBuild: 'C:\\MSBuild12\\MSBuild.exe', + path: 'C:\\VS2013', + sdk: null, + toolset: 'v120', + version: '12.0', + versionMajor: 12, + versionMinor: 0, + versionYear: 2013 + }) }) - finder.parseData(null, '', '') + finder.findVisualStudio2017OrNewer = (cb) => { + finder.parseData(new Error(), '', '', cb) + } + finder.regSearchKeys = (keys, value, addOpts, cb) => { + for (var i = 0; i < keys.length; ++i) { + const fullName = `${keys[i]}\\${value}` + switch (fullName) { + case 'HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7\\14.0': + case 'HKLM\\Software\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VC7\\14.0': + continue + case 'HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7\\12.0': + t.pass(`expected search for registry value ${fullName}`) + return cb(null, 'C:\\VS2013\\VC\\') + case 'HKLM\\Software\\Microsoft\\MSBuild\\ToolsVersions\\12.0\\MSBuildToolsPath': + t.pass(`expected search for registry value ${fullName}`) + return cb(null, 'C:\\MSBuild12\\') + default: + t.fail(`unexpected search for registry value ${fullName}`) + } + } + return cb(new Error()) + } + finder.findVisualStudio() }) -test('output not JSON', function (t) { +test('VS2013 should not be found on new node versions', function (t) { t.plan(2) - const finder = new VisualStudioFinder(function (err, info) { - t.ok(/use PowerShell/i.test(err), 'expect error') + const finder = new TestVisualStudioFinder({ + major: 10, + minor: 0, + patch: 0 + }, null, (err, info) => { + t.ok(/find .* Visual Studio/i.test(err), 'expect error') t.false(info, 'no data') }) - finder.parseData(null, 'AAAABBBB', '') + finder.findVisualStudio2017OrNewer = (cb) => { + const file = path.join(__dirname, 'fixtures', 'VS_2017_Unusable.txt') + const data = fs.readFileSync(file) + finder.parseData(null, data, '', cb) + } + finder.regSearchKeys = (keys, value, addOpts, cb) => { + for (var i = 0; i < keys.length; ++i) { + const fullName = `${keys[i]}\\${value}` + switch (fullName) { + case 'HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7\\14.0': + case 'HKLM\\Software\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VC7\\14.0': + continue + default: + t.fail(`unexpected search for registry value ${fullName}`) + } + } + return cb(new Error()) + } + finder.findVisualStudio() }) -test('wrong JSON', function (t) { +test('VS2015', function (t) { + t.plan(4) + + const finder = new TestVisualStudioFinder(semverV1, null, (err, info) => { + t.strictEqual(err, null) + t.deepEqual(info, { + msBuild: 'C:\\MSBuild14\\MSBuild.exe', + path: 'C:\\VS2015', + sdk: null, + toolset: 'v140', + version: '14.0', + versionMajor: 14, + versionMinor: 0, + versionYear: 2015 + }) + }) + + finder.findVisualStudio2017OrNewer = (cb) => { + finder.parseData(new Error(), '', '', cb) + } + finder.regSearchKeys = (keys, value, addOpts, cb) => { + for (var i = 0; i < keys.length; ++i) { + const fullName = `${keys[i]}\\${value}` + switch (fullName) { + case 'HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7\\14.0': + t.pass(`expected search for registry value ${fullName}`) + return cb(null, 'C:\\VS2015\\VC\\') + case 'HKLM\\Software\\Microsoft\\MSBuild\\ToolsVersions\\14.0\\MSBuildToolsPath': + t.pass(`expected search for registry value ${fullName}`) + return cb(null, 'C:\\MSBuild14\\') + default: + t.fail(`unexpected search for registry value ${fullName}`) + } + } + return cb(new Error()) + } + finder.findVisualStudio() +}) + +test('error from PowerShell', function (t) { t.plan(2) - const finder = new VisualStudioFinder(function (err, info) { - t.ok(/use PowerShell/i.test(err), 'expect error') + const finder = new TestVisualStudioFinder(semverV1, null, null) + + finder.parseData(new Error(), '', '', (info) => { + t.ok(/use PowerShell/i.test(finder.errorLog[0]), 'expect error') t.false(info, 'no data') }) +}) - finder.parseData(null, '{}', '') +test('empty output from PowerShell', function (t) { + t.plan(2) + + const finder = new TestVisualStudioFinder(semverV1, null, null) + + finder.parseData(null, '', '', (info) => { + t.ok(/use PowerShell/i.test(finder.errorLog[0]), 'expect error') + t.false(info, 'no data') + }) }) -test('empty JSON', function (t) { +test('output from PowerShell not JSON', function (t) { t.plan(2) - const finder = new VisualStudioFinder(function (err, info) { - t.ok(/find any Visual Studio/i.test(err), 'expect error') + const finder = new TestVisualStudioFinder(semverV1, null, null) + + finder.parseData(null, 'AAAABBBB', '', (info) => { + t.ok(/use PowerShell/i.test(finder.errorLog[0]), 'expect error') t.false(info, 'no data') }) +}) + +test('wrong JSON from PowerShell', function (t) { + t.plan(2) + + const finder = new TestVisualStudioFinder(semverV1, null, null) - finder.parseData(null, '[]', '') + finder.parseData(null, '{}', '', (info) => { + t.ok(/use PowerShell/i.test(finder.errorLog[0]), 'expect error') + t.false(info, 'no data') + }) }) -test('future version', function (t) { +test('empty JSON from PowerShell', function (t) { t.plan(2) - const finder = new VisualStudioFinder(function (err, info) { - t.ok(/find any Visual Studio/i.test(err), 'expect error') + const finder = new TestVisualStudioFinder(semverV1, null, null) + + finder.parseData(null, '[]', '', (info) => { + t.ok(/find .* Visual Studio/i.test(finder.errorLog[0]), 'expect error') t.false(info, 'no data') }) +}) + +test('future version', function (t) { + t.plan(3) + + const finder = new TestVisualStudioFinder(semverV1, null, null) finder.parseData(null, JSON.stringify([{ packages: [ @@ -66,62 +215,202 @@ test('future version', function (t) { ], path: 'C:\\VS', version: '9999.9999.9999.9999' - }]), '') + }]), '', (info) => { + t.ok(/unknown version/i.test(finder.errorLog[0]), 'expect error') + t.ok(/find .* Visual Studio/i.test(finder.errorLog[1]), 'expect error') + t.false(info, 'no data') + }) }) test('single unusable VS2017', function (t) { - t.plan(2) + t.plan(3) - const finder = new VisualStudioFinder(function (err, info) { - t.ok(/find any Visual Studio/i.test(err), 'expect error') - t.false(info, 'no data') - }) + const finder = new TestVisualStudioFinder(semverV1, null, null) const file = path.join(__dirname, 'fixtures', 'VS_2017_Unusable.txt') const data = fs.readFileSync(file) - finder.parseData(null, data, '') + finder.parseData(null, data, '', (info) => { + t.ok(/checking/i.test(finder.errorLog[0]), 'expect error') + t.ok(/find .* Visual Studio/i.test(finder.errorLog[2]), 'expect error') + t.false(info, 'no data') + }) }) test('minimal VS2017 Build Tools', function (t) { t.plan(2) - const finder = new VisualStudioFinder(function (err, info) { + const finder = new TestVisualStudioFinder(semverV1, null, (err, info) => { t.strictEqual(err, null) t.deepEqual(info, { - hasMSBuild: true, + msBuild: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\' + + 'BuildTools\\MSBuild\\15.0\\Bin\\MSBuild.exe', path: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\BuildTools', sdk: '10.0.17134.0', toolset: 'v141', version: '15.9.28307.665', + versionMajor: 15, + versionMinor: 9, versionYear: 2017 }) }) - const file = path.join(__dirname, 'fixtures', - 'VS_2017_BuildTools_minimal.txt') - const data = fs.readFileSync(file) - finder.parseData(null, data, '') + poison(finder, 'regSearchKeys') + finder.findVisualStudio2017OrNewer = (cb) => { + const file = path.join(__dirname, 'fixtures', + 'VS_2017_BuildTools_minimal.txt') + const data = fs.readFileSync(file) + finder.parseData(null, data, '', cb) + } + finder.findVisualStudio() }) test('VS2017 Community with C++ workload', function (t) { t.plan(2) - const finder = new VisualStudioFinder(function (err, info) { + const finder = new TestVisualStudioFinder(semverV1, null, (err, info) => { t.strictEqual(err, null) t.deepEqual(info, { - hasMSBuild: true, + msBuild: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\' + + 'Community\\MSBuild\\15.0\\Bin\\MSBuild.exe', path: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community', sdk: '10.0.17763.0', toolset: 'v141', version: '15.9.28307.665', + versionMajor: 15, + versionMinor: 9, versionYear: 2017 }) }) - const file = path.join(__dirname, 'fixtures', - 'VS_2017_Community_workload.txt') - const data = fs.readFileSync(file) - finder.parseData(null, data, '') + poison(finder, 'regSearchKeys') + finder.findVisualStudio2017OrNewer = (cb) => { + const file = path.join(__dirname, 'fixtures', + 'VS_2017_Community_workload.txt') + const data = fs.readFileSync(file) + finder.parseData(null, data, '', cb) + } + finder.findVisualStudio() +}) + +function allVsVersions (t, finder) { + finder.findVisualStudio2017OrNewer = (cb) => { + const file1 = path.join(__dirname, 'fixtures', 'VS_2017_BuildTools_minimal.txt') + const data1 = JSON.parse(fs.readFileSync(file1)) + const file2 = path.join(__dirname, 'fixtures', 'VS_2017_Community_workload.txt') + const data2 = JSON.parse(fs.readFileSync(file2)) + const data = JSON.stringify(data1.concat(data2)) + finder.parseData(null, data, '', cb) + } + finder.regSearchKeys = (keys, value, addOpts, cb) => { + for (var i = 0; i < keys.length; ++i) { + const fullName = `${keys[i]}\\${value}` + switch (fullName) { + case 'HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7\\14.0': + case 'HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7\\12.0': + continue + case 'HKLM\\Software\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VC7\\12.0': + return cb(null, 'C:\\VS2013\\VC\\') + case 'HKLM\\Software\\Microsoft\\MSBuild\\ToolsVersions\\12.0\\MSBuildToolsPath': + return cb(null, 'C:\\MSBuild12\\') + case 'HKLM\\Software\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VC7\\14.0': + return cb(null, 'C:\\VS2015\\VC\\') + case 'HKLM\\Software\\Microsoft\\MSBuild\\ToolsVersions\\14.0\\MSBuildToolsPath': + return cb(null, 'C:\\MSBuild14\\') + default: + t.fail(`unexpected search for registry value ${fullName}`) + } + } + return cb(new Error()) + } +} + +test('fail when looking for invalid path', function (t) { + t.plan(2) + + const finder = new TestVisualStudioFinder(semverV1, 'AABB', (err, info) => { + t.ok(/find .* Visual Studio/i.test(err), 'expect error') + t.false(info, 'no data') + }) + + allVsVersions(t, finder) + finder.findVisualStudio() +}) + +test('look for VS2013 by version number', function (t) { + t.plan(2) + + const finder = new TestVisualStudioFinder(semverV1, '2013', (err, info) => { + t.strictEqual(err, null) + t.deepEqual(info.versionYear, 2013) + }) + + allVsVersions(t, finder) + finder.findVisualStudio() +}) + +test('look for VS2013 by installation path', function (t) { + t.plan(2) + + const finder = new TestVisualStudioFinder(semverV1, 'C:\\VS2013', + (err, info) => { + t.strictEqual(err, null) + t.deepEqual(info.path, 'C:\\VS2013') + }) + + allVsVersions(t, finder) + finder.findVisualStudio() +}) + +test('look for VS2015 by version number', function (t) { + t.plan(2) + + const finder = new TestVisualStudioFinder(semverV1, '2015', (err, info) => { + t.strictEqual(err, null) + t.deepEqual(info.versionYear, 2015) + }) + + allVsVersions(t, finder) + finder.findVisualStudio() +}) + +test('look for VS2015 by installation path', function (t) { + t.plan(2) + + const finder = new TestVisualStudioFinder(semverV1, 'C:\\VS2015', + (err, info) => { + t.strictEqual(err, null) + t.deepEqual(info.path, 'C:\\VS2015') + }) + + allVsVersions(t, finder) + finder.findVisualStudio() +}) + +test('look for VS2017 by version number', function (t) { + t.plan(2) + + const finder = new TestVisualStudioFinder(semverV1, '2017', (err, info) => { + t.strictEqual(err, null) + t.deepEqual(info.versionYear, 2017) + }) + + allVsVersions(t, finder) + finder.findVisualStudio() +}) + +test('look for VS2017 by installation path', function (t) { + t.plan(2) + + const finder = new TestVisualStudioFinder(semverV1, + 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community', + (err, info) => { + t.strictEqual(err, null) + t.deepEqual(info.path, + 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community') + }) + + allVsVersions(t, finder) + finder.findVisualStudio() })