gulp-msbuild
Version:
msbuild plugin for gulp. Inspired by grunt-msbuild.
355 lines (322 loc) • 11.3 kB
JavaScript
;
const path = require("path");
var constants = require("./constants");
const fs = require("fs");
const PluginError = require("plugin-error");
const child = require("child_process");
var constants = require("./constants");
const childProcess = require("child_process");
const os = require("os");
const lsCache = {};
function msBuildFromWhere(pathRoot, findAll) {
findAll = findAll || false;
const vsWherePath = path.join(pathRoot, "Microsoft Visual Studio", "Installer", "vswhere.exe");
if (!fs.existsSync(vsWherePath)) {
// no vswhere -- fall back on manual methods
return [];
}
let args = findAll ? [] : [ "-latest" ];
args = args.concat([ "-products", "*", "-requires", "Microsoft.Component.MSBuild" ]);
const whereProcess = child.spawnSync(
vsWherePath,
args,
{
cwd: process.cwd(),
env: process.env,
stdio: "pipe",
encoding: "utf-8"
}
);
if (whereProcess.output === null) {
return [];
}
let cmdOutput = "";
if (whereProcess.output.length > 0) {
for (let index = 0; index < whereProcess.output.length; index++) {
cmdOutput = whereProcess.output[index] || "";
if (cmdOutput.length > 0) {
break;
}
}
}
const installKeyword = "installationPath";
const installationVersionKeyword = "installationVersion";
const all = [];
if (cmdOutput.length > 0) {
let installationPath, installationVersion;
const results = cmdOutput.split(/\r?\n/);
for (let cmdLineIndex = 0; cmdLineIndex < results.length; cmdLineIndex++) {
const cmdLine = results[cmdLineIndex];
if (cmdLine.startsWith(installKeyword)) {
installationPath = cmdLine.replace(installKeyword + ": ", "");
}
if (cmdLine.startsWith(installationVersionKeyword)) {
let versionParts = cmdLine.replace(installationVersionKeyword + ": ", "").split(".");
if (versionParts.length > 0) {
installationVersion = parseFloat(versionParts[0]);
}
}
if (installationPath && installationVersion) {
all.push({ installationPath, installationVersion });
installationPath = undefined;
installationVersion = undefined;
}
}
}
all.sort((a, b) => a.installationVersion - b.installationVersion);
if (findAll) {
return all.map(o => [ o.installationPath, o.installationVersion ]);
} else {
const highest = all[all.length - 1];
if (highest === undefined) {
return [];
}
return [ highest.installationPath, highest.installationVersion ];
}
}
module.exports.msBuildFromWhere = msBuildFromWhere;
function detectMsBuild15Dir(pathRoot) {
const vs2017Path = path.join(pathRoot, "Microsoft Visual Studio", "2017");
const possibleFolders = [ "BuildTools", "Enterprise", "Professional", "Community" ];
for (let index = 0; index < possibleFolders.length; index++) {
try {
const folderPath = path.join(vs2017Path, possibleFolders[index]);
fs.statSync(folderPath);
return folderPath;
} catch (e) {
}
}
}
// Use MSBuild over XBuild where possible
const detectMsBuildOverXBuild = function () {
try {
const output = child.spawnSync("which", [ "msbuild" ], { encoding: "utf8" });
if (output.stderr && output.stderr !== 0) {
return "xbuild";
}
return "msbuild";
} catch (e) {
}
};
function lsR(folder) {
if (lsCache[folder]) {
return lsCache[folder];
}
return lsCache[folder] = fs.readdirSync(folder)
.reduce((acc, cur) => {
const fullPath = path.join(folder, cur);
const st = fs.statSync(fullPath);
if (st.isFile()) {
acc.push(fullPath);
return acc;
}
return acc.concat(lsR(fullPath));
}, []);
}
function findMSBuildExeUnder(folder) {
return lsR(folder).filter(fpath => {
const fileName = path.basename(fpath);
return fileName.toLowerCase() === "msbuild.exe";
});
}
function addDetectedMsBuildVersionsToConstantsLookup(executables) {
return executables.map(exe => {
try {
const proc = childProcess.spawnSync(exe, [ "/version" ], { encoding: "utf8" });
if (proc.stdout === null) {
// happens if the process couldn't actually be started, eg ARM64 binary on AMD64 system
return undefined;
}
const lines = proc.stdout.split(os.EOL);
const thisVersion = lines[lines.length - 1];
const verParts = thisVersion.split(".");
const major = verParts[0];
const shortVer = `${major}.0`; // not technically correct: I see msbuild 16.1 on my machine, but keeps in line with prior versioning
const ver = parseFloat(shortVer);
if (!constants.MSBUILD_VERSIONS[shortVer]) {
constants.MSBUILD_VERSIONS[ver] = shortVer;
return ver;
}
} catch (e) {
console.warn(`Unable to query version of ${exe}: ${e}`);
}
})
.filter(ver => !!ver)
.reduce((acc, cur) => {
if (acc.indexOf(cur) === -1) {
acc.push(cur);
}
return acc;
}, [])
.sort()
.reverse();
}
function autoDetectVersion(pathRoot, matchVersion) {
// Try to detect using fromWhere
const findAll = matchVersion !== undefined;
const wherePath = msBuildFromWhere(pathRoot, findAll);
if (wherePath.length > 0) {
if (findAll) {
for (const pair of wherePath) {
const [ _, version ] = pair;
if (version === matchVersion) {
return pair;
}
}
} else {
return wherePath;
}
}
if (matchVersion >= 15.0) {
// Try to detect MSBuild 15.0.
const msbuild15OrLaterDir = detectMsBuild15Dir(pathRoot);
if (msbuild15OrLaterDir) {
const msbuildHome = path.join(msbuild15OrLaterDir, "MSBuild");
const msbuildExecutables = findMSBuildExeUnder(msbuildHome);
const detected = addDetectedMsBuildVersionsToConstantsLookup(msbuildExecutables);
return [ msbuild15OrLaterDir, detected[0] || 15.0 ];
}
}
// Detect MSBuild lower than 15.0.
// ported from https://github.com/stevewillcock/grunt-msbuild/blob/master/tasks/msbuild.js#L167-L181
const msbuildDir = path.join(pathRoot, "MSBuild");
let msbuildDirExists;
try {
fs.statSync(msbuildDir);
msbuildDirExists = true;
} catch (e) {
msbuildDirExists = false;
}
if (msbuildDirExists) {
const msbuildVersions = fs.readdirSync(msbuildDir)
.filter(function (entryName) {
let binDirExists = true;
const binDirPath = path.join(msbuildDir, entryName, "Bin");
try {
fs.statSync(binDirPath);
} catch (e) {
binDirExists = false;
}
return entryName.indexOf("1") === 0 && binDirExists;
});
if (msbuildVersions.length > 0) {
// Return latest installed msbuild version
return [ pathRoot, parseFloat(msbuildVersions.pop()) ];
}
}
return matchVersion === undefined
? [ pathRoot, 4.0 ]
: [];
}
module.exports.find = function (options) {
if (options.platform.match(/linux|darwin/)) {
const msbuildPath = detectMsBuildOverXBuild();
if (msbuildPath) {
return msbuildPath;
}
return "xbuild";
} else if (!options.platform.match(/^win/)) {
return "xbuild";
}
const buildIs64Bit = options.architecture === "x64";
const hostIs64Bit = constants.DEFAULTS.architecture === "x64";
// On 64-bit systems msbuild is always under the x86 directory. If this
// doesn"t exist we are on a 32-bit system. See also:
// https://blogs.msdn.microsoft.com/visualstudio/2013/07/24/msbuild-is-now-part-of-visual-studio/
let pathRoot = process.env["ProgramFiles"] || "C:/Program Files";
if (hostIs64Bit) {
pathRoot = process.env["ProgramFiles(x86)"] || "C:/Program Files (x86)";
}
let msbuildRoot = pathRoot;
let toolsVersion = parseFloat(options.toolsVersion);
if (isNaN(toolsVersion)) {
// if the toolsVersion is 'auto' or any other non-parsable string
// we'll auto delect the version
const auto = autoDetectVersion(pathRoot);
msbuildRoot = auto[0];
toolsVersion = parseFloat(auto[1]);
options.toolsVersion = toolsVersion;
} else {
if (toolsVersion < 1) {
throw new Error(`Invalid MSBuild version was supplied: ${options.toolsVersion}`);
}
const matched = autoDetectVersion(pathRoot, toolsVersion);
if (matched.length) {
msbuildRoot = matched[0];
toolsVersion = parseFloat(matched[1]);
options.toolsVersion = toolsVersion;
} else if (toolsVersion <= 4.0) {
// try find in windir
return findWindirMsBuildFor(buildIs64Bit, toolsVersion);
} else {
// fall back on msbuild 15
const msbuildDir = detectMsBuild15Dir(pathRoot);
if (msbuildDir && toolsVersion >= 15.0) {
msbuildRoot = msbuildDir;
}
}
}
const
configuredVersion = constants.MSBUILD_VERSIONS[toolsVersion],
version = configuredVersion === undefined ? toolsVersion.toString() : configuredVersion;
if (!version) {
throw new PluginError(constants.PLUGIN_NAME, "No or invalid MSBuild version was supplied! Please set or check the value of the 'toolsVersion' property.");
}
let major = parseInt(version.split(".")[0]);
if (major >= 16) {
if (buildIs64Bit) {
return path.join(msbuildRoot, "MSBuild", "Current", "Bin", "amd64", "MSBuild.exe");
} else {
return path.join(msbuildRoot, "MSBuild", "Current", "Bin", "MSBuild.exe");
}
} else if (major > 15 && major < 16) {
let x64_dir = buildIs64Bit ? "amd64" : "";
const msbuildHome = path.join(msbuildRoot, "MSBuild");
const msbuildExe = findMSBuildExeUnder(msbuildHome)
.filter(exe => {
const pathParts = exe.split(path.sep);
return buildIs64Bit
? pathParts.indexOf(x64_dir) > -1
: pathParts.indexOf(x64_dir) === -1;
})[0];
if (!msbuildExe) {
throw new PluginError(
constants.PLUGIN_NAME,
`Unable to find msbuild.exe under ${msbuildHome}`);
}
return msbuildExe;
} else if (major >= 12 && major <= 15) {
let x64_dir = buildIs64Bit ? "amd64" : "";
return path.join(msbuildRoot, "MSBuild", version, "Bin", x64_dir, "MSBuild.exe");
}
const framework = buildIs64Bit ? "Framework64" : "Framework";
return path.join(options.windir, "Microsoft.Net", framework, version, "MSBuild.exe");
};
function findWindirMsBuildFor(is64Bit, toolsVersion, windir) {
windir = windir || process.env.WINDIR;
if (!windir) {
return []; // can't look when we don't know where
}
const framework = is64Bit ? "Framework64" : "Framework";
const baseDir = path.join(windir, "Microsoft.NET", framework);
const frameworkVersions = fs.readdirSync(baseDir)
.filter(p => isDir(path.join(baseDir, p)))
.map(p => p.replace(/^v/, "")) // strip leading 'v'
.map(p => parseFloat(p))
.filter(version => !isNaN(version));
const match = frameworkVersions.find(
ver => ver.toFixed(1) === toolsVersion.toFixed(1)
);
if (!match) {
throw new Error(`No or invalid MSBuild version was supplied! (Can't find msbuild for toolsVersion under ${baseDir} (perhaps override windir?))`);
}
return path.join(baseDir, `v${match}`, "MSBuild.exe");
}
function isDir(at) {
try {
const st = fs.statSync(at);
return st && st.isDirectory();
} catch (e) {
return false;
}
}