diff --git a/src/WixBuildTools.MsgGen/WixBuildTools.MsgGen.csproj b/src/WixBuildTools.MsgGen/WixBuildTools.MsgGen.csproj index 80d6b0d..ccbb92c 100644 --- a/src/WixBuildTools.MsgGen/WixBuildTools.MsgGen.csproj +++ b/src/WixBuildTools.MsgGen/WixBuildTools.MsgGen.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/WixBuildTools.TestSupport/ExternalExecutable.cs b/src/WixBuildTools.TestSupport/ExternalExecutable.cs new file mode 100644 index 0000000..eb07aa1 --- /dev/null +++ b/src/WixBuildTools.TestSupport/ExternalExecutable.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixBuildTools.TestSupport +{ + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Text; + + public abstract class ExternalExecutable + { + private readonly string exePath; + + protected ExternalExecutable(string exePath) + { + this.exePath = exePath; + } + + protected ExternalExecutableResult Run(string args, bool mergeErrorIntoOutput = false, string workingDirectory = null) + { + var startInfo = new ProcessStartInfo(this.exePath, args) + { + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + WorkingDirectory = workingDirectory ?? Path.GetDirectoryName(this.exePath), + }; + + using (var process = Process.Start(startInfo)) + { + // This implementation of merging the streams does not guarantee that lines are retrieved in the same order that they were written. + // If the process is simultaneously writing to both streams, this is impossible to do anyway. + var standardOutput = new ConcurrentQueue(); + var standardError = mergeErrorIntoOutput ? standardOutput : new ConcurrentQueue(); + + process.ErrorDataReceived += (s, e) => { if (e.Data != null) { standardError.Enqueue(e.Data); } }; + process.OutputDataReceived += (s, e) => { if (e.Data != null) { standardOutput.Enqueue(e.Data); } }; + + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + process.WaitForExit(); + + return new ExternalExecutableResult + { + ExitCode = process.ExitCode, + StandardError = mergeErrorIntoOutput ? null : standardError.ToArray(), + StandardOutput = standardOutput.ToArray(), + StartInfo = startInfo, + }; + } + } + + // This is internal because it assumes backslashes aren't used as escape characters and there aren't any double quotes. + internal static string CombineArguments(IEnumerable arguments) + { + if (arguments == null) + { + return null; + } + + var sb = new StringBuilder(); + + foreach (var arg in arguments) + { + if (sb.Length > 0) + { + sb.Append(' '); + } + + if (arg.IndexOf(' ') > -1) + { + sb.Append("\""); + sb.Append(arg); + sb.Append("\""); + } + else + { + sb.Append(arg); + } + } + + return sb.ToString(); + } + } +} diff --git a/src/WixBuildTools.TestSupport/ExternalExecutableResult.cs b/src/WixBuildTools.TestSupport/ExternalExecutableResult.cs new file mode 100644 index 0000000..19b5183 --- /dev/null +++ b/src/WixBuildTools.TestSupport/ExternalExecutableResult.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixBuildTools.TestSupport +{ + using System.Diagnostics; + + public class ExternalExecutableResult + { + public int ExitCode { get; set; } + + public string[] StandardError { get; set; } + + public string[] StandardOutput { get; set; } + + public ProcessStartInfo StartInfo { get; set; } + } +} diff --git a/src/WixBuildTools.TestSupport/MsbuildRunner.cs b/src/WixBuildTools.TestSupport/MsbuildRunner.cs index b38387a..35e53de 100644 --- a/src/WixBuildTools.TestSupport/MsbuildRunner.cs +++ b/src/WixBuildTools.TestSupport/MsbuildRunner.cs @@ -4,96 +4,148 @@ namespace WixBuildTools.TestSupport { using System; using System.Collections.Generic; - using System.Diagnostics; using System.IO; - using System.Text; - public static class MsbuildRunner + public class MsbuildRunner : ExternalExecutable { - private static readonly string VswhereRelativePath = @"Microsoft Visual Studio\Installer\vswhere.exe"; - private static readonly string[] VswhereFindArguments = new[] { "-property", "installationPath" }; + private static readonly string VswhereFindArguments = "-property installationPath"; private static readonly string Msbuild15RelativePath = @"MSBuild\15.0\Bin\MSBuild.exe"; - private static readonly string Msbuild16RelativePath = @"MSBuild\Current\Bin\MSBuild.exe"; + private static readonly string Msbuild15RelativePath64 = @"MSBuild\15.0\Bin\amd64\MSBuild.exe"; + private static readonly string MsbuildCurrentRelativePath = @"MSBuild\Current\Bin\MSBuild.exe"; + private static readonly string MsbuildCurrentRelativePath64 = @"MSBuild\Current\Bin\amd64\MSBuild.exe"; private static readonly object InitLock = new object(); - private static string Msbuild15Path; - private static string Msbuild16Path; + private static bool Initialized; + private static MsbuildRunner Msbuild15Runner; + private static MsbuildRunner Msbuild15Runner64; + private static MsbuildRunner MsbuildCurrentRunner; + private static MsbuildRunner MsbuildCurrentRunner64; - public static MsbuildRunnerResult Execute(string projectPath, string[] arguments = null) => InitAndExecute(String.Empty, projectPath, arguments); + public static MsbuildRunnerResult Execute(string projectPath, string[] arguments = null, bool x64 = false) => + InitAndExecute(String.Empty, projectPath, arguments, x64); - public static MsbuildRunnerResult ExecuteWithMsbuild15(string projectPath, string[] arguments = null) => InitAndExecute("15", projectPath, arguments); + public static MsbuildRunnerResult ExecuteWithMsbuild15(string projectPath, string[] arguments = null, bool x64 = false) => + InitAndExecute("15", projectPath, arguments, x64); - public static MsbuildRunnerResult ExecuteWithMsbuild16(string projectPath, string[] arguments = null) => InitAndExecute("16", projectPath, arguments); + public static MsbuildRunnerResult ExecuteWithMsbuildCurrent(string projectPath, string[] arguments = null, bool x64 = false) => + InitAndExecute("Current", projectPath, arguments, x64); - private static MsbuildRunnerResult InitAndExecute(string msbuildVersion, string projectPath, string[] arguments) + private static MsbuildRunnerResult InitAndExecute(string msbuildVersion, string projectPath, string[] arguments, bool x64) { lock (InitLock) { - if (Msbuild15Path == null && Msbuild16Path == null) + if (!Initialized) { - var vswherePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), VswhereRelativePath); - if (!File.Exists(vswherePath)) + Initialized = true; + var vswhereResult = VswhereRunner.Execute(VswhereFindArguments, true); + if (vswhereResult.ExitCode != 0) { - throw new InvalidOperationException($"Failed to find vswhere at: {vswherePath}"); + throw new InvalidOperationException($"Failed to execute vswhere.exe, exit code: {vswhereResult.ExitCode}. Output:\r\n{String.Join("\r\n", vswhereResult.StandardOutput)}"); } - var result = RunProcessCaptureOutput(vswherePath, VswhereFindArguments); - if (result.ExitCode != 0) - { - throw new InvalidOperationException($"Failed to execute vswhere.exe, exit code: {result.ExitCode}"); - } + string msbuild15Path = null; + string msbuild15Path64 = null; + string msbuildCurrentPath = null; + string msbuildCurrentPath64 = null; - Msbuild15Path = String.Empty; - Msbuild16Path = String.Empty; - - foreach (var installPath in result.Output) + foreach (var installPath in vswhereResult.StandardOutput) { - if (String.IsNullOrEmpty(Msbuild16Path)) + if (msbuildCurrentPath == null) { - var path = Path.Combine(installPath, Msbuild16RelativePath); + var path = Path.Combine(installPath, MsbuildCurrentRelativePath); if (File.Exists(path)) { - Msbuild16Path = path; + msbuildCurrentPath = path; } } - if (String.IsNullOrEmpty(Msbuild15Path)) + if (msbuildCurrentPath64 == null) + { + var path = Path.Combine(installPath, MsbuildCurrentRelativePath64); + if (File.Exists(path)) + { + msbuildCurrentPath64 = path; + } + } + + if (msbuild15Path == null) { var path = Path.Combine(installPath, Msbuild15RelativePath); if (File.Exists(path)) { - Msbuild15Path = path; + msbuild15Path = path; } } + + if (msbuild15Path64 == null) + { + var path = Path.Combine(installPath, Msbuild15RelativePath64); + if (File.Exists(path)) + { + msbuild15Path64 = path; + } + } + } + + if (msbuildCurrentPath != null) + { + MsbuildCurrentRunner = new MsbuildRunner(msbuildCurrentPath); + } + + if (msbuildCurrentPath64 != null) + { + MsbuildCurrentRunner64 = new MsbuildRunner(msbuildCurrentPath64); + } + + if (msbuild15Path != null) + { + Msbuild15Runner = new MsbuildRunner(msbuild15Path); + } + + if (msbuild15Path64 != null) + { + Msbuild15Runner64 = new MsbuildRunner(msbuild15Path64); } } } - var msbuildPath = !String.IsNullOrEmpty(Msbuild15Path) ? Msbuild15Path : Msbuild16Path; - - if (msbuildVersion == "15") + MsbuildRunner runner; + switch (msbuildVersion) { - msbuildPath = Msbuild15Path; - } - else if (msbuildVersion == "16") - { - msbuildPath = Msbuild16Path; + case "15": + { + runner = x64 ? Msbuild15Runner64 : Msbuild15Runner; + break; + } + case "Current": + { + runner = x64 ? MsbuildCurrentRunner64 : MsbuildCurrentRunner; + break; + } + default: + { + runner = x64 ? MsbuildCurrentRunner64 ?? Msbuild15Runner64 + : MsbuildCurrentRunner ?? Msbuild15Runner; + break; + } } - return ExecuteCore(msbuildVersion, msbuildPath, projectPath, arguments); + if (runner == null) + { + throw new InvalidOperationException($"Failed to find an installed{(x64 ? " 64-bit" : String.Empty)} MSBuild{msbuildVersion}"); + } + + return runner.ExecuteCore(projectPath, arguments); } - private static MsbuildRunnerResult ExecuteCore(string msbuildVersion, string msbuildPath, string projectPath, string[] arguments) - { - if (String.IsNullOrEmpty(msbuildPath)) - { - throw new InvalidOperationException($"Failed to find an installed MSBuild{msbuildVersion}"); - } + private MsbuildRunner(string exePath) : base(exePath) { } + private MsbuildRunnerResult ExecuteCore(string projectPath, string[] arguments) + { var total = new List { - projectPath + projectPath, }; if (arguments != null) @@ -101,69 +153,16 @@ namespace WixBuildTools.TestSupport total.AddRange(arguments); } + var args = CombineArguments(total); + var mergeErrorIntoOutput = true; var workingFolder = Path.GetDirectoryName(projectPath); - return RunProcessCaptureOutput(msbuildPath, total.ToArray(), workingFolder); - } + var result = this.Run(args, mergeErrorIntoOutput, workingFolder); - private static MsbuildRunnerResult RunProcessCaptureOutput(string executablePath, string[] arguments = null, string workingFolder = null) - { - var startInfo = new ProcessStartInfo(executablePath) + return new MsbuildRunnerResult { - Arguments = CombineArguments(arguments), - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - UseShellExecute = false, - WorkingDirectory = workingFolder, + ExitCode = result.ExitCode, + Output = result.StandardOutput, }; - - var exitCode = 0; - var output = new List(); - - using (var process = Process.Start(startInfo)) - { - process.OutputDataReceived += (s, e) => { if (e.Data != null) { output.Add(e.Data); } }; - process.ErrorDataReceived += (s, e) => { if (e.Data != null) { output.Add(e.Data); } }; - - process.BeginErrorReadLine(); - process.BeginOutputReadLine(); - - process.WaitForExit(); - exitCode = process.ExitCode; - } - - return new MsbuildRunnerResult { ExitCode = exitCode, Output = output.ToArray() }; - } - - private static string CombineArguments(string[] arguments) - { - if (arguments == null) - { - return null; - } - - var sb = new StringBuilder(); - - foreach (var arg in arguments) - { - if (sb.Length > 0) - { - sb.Append(' '); - } - - if (arg.IndexOf(' ') > -1) - { - sb.Append("\""); - sb.Append(arg); - sb.Append("\""); - } - else - { - sb.Append(arg); - } - } - - return sb.ToString(); } } } diff --git a/src/WixBuildTools.TestSupport/VswhereRunner.cs b/src/WixBuildTools.TestSupport/VswhereRunner.cs new file mode 100644 index 0000000..0197e12 --- /dev/null +++ b/src/WixBuildTools.TestSupport/VswhereRunner.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.IO; + + public class VswhereRunner : ExternalExecutable + { + private static readonly string VswhereRelativePath = @"Microsoft Visual Studio\Installer\vswhere.exe"; + + private static readonly object InitLock = new object(); + private static bool Initialized; + private static VswhereRunner Instance; + + public static ExternalExecutableResult Execute(string args, bool mergeErrorIntoOutput = false) => + InitAndExecute(args, mergeErrorIntoOutput); + + private static ExternalExecutableResult InitAndExecute(string args, bool mergeErrorIntoOutput) + { + lock (InitLock) + { + if (!Initialized) + { + Initialized = true; + var vswherePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), VswhereRelativePath); + if (!File.Exists(vswherePath)) + { + throw new InvalidOperationException($"Failed to find vswhere at: {vswherePath}"); + } + + Instance = new VswhereRunner(vswherePath); + } + } + + return Instance.Run(args, mergeErrorIntoOutput); + } + + private VswhereRunner(string exePath) : base(exePath) { } + } +} diff --git a/src/WixBuildTools.TestSupport/WixBuildTools.TestSupport.csproj b/src/WixBuildTools.TestSupport/WixBuildTools.TestSupport.csproj index 31bdf03..e6cddde 100644 --- a/src/WixBuildTools.TestSupport/WixBuildTools.TestSupport.csproj +++ b/src/WixBuildTools.TestSupport/WixBuildTools.TestSupport.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/WixBuildTools.XsdGen/WixBuildTools.XsdGen.csproj b/src/WixBuildTools.XsdGen/WixBuildTools.XsdGen.csproj index ef24420..bf9d957 100644 --- a/src/WixBuildTools.XsdGen/WixBuildTools.XsdGen.csproj +++ b/src/WixBuildTools.XsdGen/WixBuildTools.XsdGen.csproj @@ -19,7 +19,7 @@ - +