From ecf3a0cca5a424a91ab98557d963d2535963d582 Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Fri, 22 Dec 2017 15:53:01 -0800 Subject: Reintroduce binder extensions and light.exe for binding .wixouts --- src/WixToolset.Core/CommandLine/BuildCommand.cs | 62 +- src/WixToolset.Core/CommandLine/CommandLine.cs | 692 --------------------- .../CommandLine/CommandLineContext.cs | 2 +- .../CommandLine/CommandLineHelper.cs | 216 +++++++ .../CommandLine/CommandLineParser.cs | 628 +++++++++++++++++++ .../CommandLine/CommandLineResponseFile.cs | 2 +- src/WixToolset.Core/CommandLine/CompileCommand.cs | 2 +- src/WixToolset.Core/CommandLine/HelpCommand.cs | 2 +- src/WixToolset.Core/CommandLine/VersionCommand.cs | 2 +- 9 files changed, 881 insertions(+), 727 deletions(-) delete mode 100644 src/WixToolset.Core/CommandLine/CommandLine.cs create mode 100644 src/WixToolset.Core/CommandLine/CommandLineHelper.cs create mode 100644 src/WixToolset.Core/CommandLine/CommandLineParser.cs (limited to 'src/WixToolset.Core/CommandLine') diff --git a/src/WixToolset.Core/CommandLine/BuildCommand.cs b/src/WixToolset.Core/CommandLine/BuildCommand.cs index 92aa3343..5653afca 100644 --- a/src/WixToolset.Core/CommandLine/BuildCommand.cs +++ b/src/WixToolset.Core/CommandLine/BuildCommand.cs @@ -1,6 +1,6 @@ // 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 WixToolset.Core +namespace WixToolset.Core.CommandLine { using System; using System.Collections.Generic; @@ -13,7 +13,7 @@ namespace WixToolset.Core internal class BuildCommand : ICommandLineCommand { - public BuildCommand(IServiceProvider serviceProvider, IMessaging messaging, IExtensionManager extensions, IEnumerable sources, IDictionary preprocessorVariables, IEnumerable locFiles, IEnumerable libraryFiles, string outputPath, OutputType outputType, string cabCachePath, IEnumerable cultures, bool bindFiles, IEnumerable bindPaths, string intermediateFolder, string contentsFile, string outputsFile, string builtOutputsFile, string wixProjectFile) + public BuildCommand(IServiceProvider serviceProvider, IMessaging messaging, IExtensionManager extensions, IEnumerable sources, IDictionary preprocessorVariables, IEnumerable locFiles, IEnumerable libraryFiles, string outputPath, OutputType outputType, string cabCachePath, IEnumerable cultures, bool bindFiles, IEnumerable bindPaths, string intermediateFolder, string contentsFile, string outputsFile, string builtOutputsFile) { this.ServiceProvider = serviceProvider; this.Messaging = messaging; @@ -34,7 +34,6 @@ namespace WixToolset.Core this.ContentsFile = contentsFile; this.OutputsFile = outputsFile; this.BuiltOutputsFile = builtOutputsFile; - this.WixProjectFile = wixProjectFile; } public IServiceProvider ServiceProvider { get; } @@ -73,8 +72,6 @@ namespace WixToolset.Core public string BuiltOutputsFile { get; } - public string WixProjectFile { get; } - public int Execute() { var intermediates = this.CompilePhase(); @@ -207,7 +204,12 @@ namespace WixToolset.Core ResolveResult resolveResult; { - var resolver = new Resolver(this.ServiceProvider, this.BindPaths, output, this.IntermediateFolder, localizations); + var resolver = new Resolver(this.ServiceProvider); + resolver.BindPaths = this.BindPaths; + resolver.IntermediateFolder = this.IntermediateFolder; + resolver.IntermediateRepresentation = output; + resolver.Localizations = localizations; + resolveResult = resolver.Execute(); } @@ -224,28 +226,22 @@ namespace WixToolset.Core intermediateFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); } - var context = this.ServiceProvider.GetService(); - context.Messaging = this.Messaging; - //context.CabbingThreadCount = this.CabbingThreadCount; - context.CabCachePath = this.CabCachePath; - context.Codepage = resolveResult.Codepage; - //context.DefaultCompressionLevel = this.DefaultCompressionLevel; - context.DelayedFields = resolveResult.DelayedFields; - context.ExpectedEmbeddedFiles = resolveResult.ExpectedEmbeddedFiles; - //context.Ices = this.Ices; - context.IntermediateFolder = intermediateFolder; - context.IntermediateRepresentation = resolveResult.IntermediateRepresentation; - context.OutputPath = this.OutputPath; - context.OutputPdbPath = Path.ChangeExtension(this.OutputPath, ".wixpdb"); - //context.SuppressIces = this.SuppressIces; - context.SuppressValidation = true; // TODO: set this correctly - context.ContentsFile = this.ContentsFile; - context.OutputsFile = this.OutputsFile; - context.BuiltOutputsFile = this.BuiltOutputsFile; - context.WixprojectFile = this.WixProjectFile; - - var binder = new Binder(); - bindResult = binder.Bind(context); + var binder = new Binder(this.ServiceProvider); + //binder.CabbingThreadCount = this.CabbingThreadCount; + binder.CabCachePath = this.CabCachePath; + binder.Codepage = resolveResult.Codepage; + //binder.DefaultCompressionLevel = this.DefaultCompressionLevel; + binder.DelayedFields = resolveResult.DelayedFields; + binder.ExpectedEmbeddedFiles = resolveResult.ExpectedEmbeddedFiles; + binder.Ices = Array.Empty(); // TODO: set this correctly + binder.IntermediateFolder = intermediateFolder; + binder.IntermediateRepresentation = resolveResult.IntermediateRepresentation; + binder.OutputPath = this.OutputPath; + binder.OutputPdbPath = Path.ChangeExtension(this.OutputPath, ".wixpdb"); + binder.SuppressIces = Array.Empty(); // TODO: set this correctly + binder.SuppressValidation = true; // TODO: set this correctly + + bindResult = binder.Execute(); } if (this.Messaging.EncounteredError) @@ -254,8 +250,14 @@ namespace WixToolset.Core } { - // TODO: correctly set SuppressAclReset bool at the end. - var layout = new Layout(this.ServiceProvider, bindResult.FileTransfers, bindResult.ContentFilePaths, this.ContentsFile, this.OutputsFile, this.BuiltOutputsFile, false); + var layout = new Layout(this.ServiceProvider); + layout.FileTransfers = bindResult.FileTransfers; + layout.ContentFilePaths = bindResult.ContentFilePaths; + layout.ContentsFile = this.ContentsFile; + layout.OutputsFile = this.OutputsFile; + layout.BuiltOutputsFile = this.BuiltOutputsFile; + layout.SuppressAclReset = false; // TODO: correctly set SuppressAclReset + layout.Execute(); } } diff --git a/src/WixToolset.Core/CommandLine/CommandLine.cs b/src/WixToolset.Core/CommandLine/CommandLine.cs deleted file mode 100644 index 97f79755..00000000 --- a/src/WixToolset.Core/CommandLine/CommandLine.cs +++ /dev/null @@ -1,692 +0,0 @@ -// 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 WixToolset.Core -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text; - using System.Text.RegularExpressions; - using WixToolset.Data; - using WixToolset.Extensibility; - using WixToolset.Extensibility.Services; - - internal enum Commands - { - Unknown, - Build, - Preprocess, - Compile, - Link, - Bind, - } - - internal class CommandLine : ICommandLine, IParseCommandLine - { - private IServiceProvider ServiceProvider { get; set; } - - private IMessaging Messaging { get; set; } - - public static string ExpectedArgument { get; } = "expected argument"; - - public string ActiveCommand { get; private set; } - - public string[] OriginalArguments { get; private set; } - - public Queue RemainingArguments { get; } = new Queue(); - - public IExtensionManager ExtensionManager { get; private set; } - - public string ErrorArgument { get; set; } - - public bool ShowHelp { get; set; } - - public ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context) - { - this.ServiceProvider = context.ServiceProvider; - - this.Messaging = context.Messaging ?? this.ServiceProvider.GetService(); - - this.ExtensionManager = context.ExtensionManager ?? this.ServiceProvider.GetService(); - - var args = context.ParsedArguments ?? Array.Empty(); - - if (!String.IsNullOrEmpty(context.Arguments)) - { - args = CommandLine.ParseArgumentsToArray(context.Arguments).Union(args).ToArray(); - } - - return this.ParseStandardCommandLine(context, args); - } - - private ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context, string[] args) - { - var next = String.Empty; - - var command = Commands.Unknown; - var showLogo = true; - var showVersion = false; - var outputFolder = String.Empty; - var outputFile = String.Empty; - var outputType = String.Empty; - var verbose = false; - var files = new List(); - var defines = new List(); - var includePaths = new List(); - var locFiles = new List(); - var libraryFiles = new List(); - var suppressedWarnings = new List(); - - var bindFiles = false; - var bindPaths = new List(); - - var intermediateFolder = String.Empty; - - var cabCachePath = String.Empty; - var cultures = new List(); - var contentsFile = String.Empty; - var outputsFile = String.Empty; - var builtOutputsFile = String.Empty; - var wixProjectFile = String.Empty; - - this.Parse(context, args, (cmdline, arg) => Enum.TryParse(arg, true, out command), (cmdline, arg) => - { - if (cmdline.IsSwitch(arg)) - { - var parameter = arg.TrimStart(new[] { '-', '/' }); - switch (parameter.ToLowerInvariant()) - { - case "?": - case "h": - case "help": - cmdline.ShowHelp = true; - return true; - - case "bindfiles": - bindFiles = true; - return true; - - case "bindpath": - cmdline.GetNextArgumentOrError(bindPaths); - return true; - - case "cc": - cmdline.GetNextArgumentOrError(ref cabCachePath); - return true; - - case "cultures": - cmdline.GetNextArgumentOrError(cultures); - return true; - case "contentsfile": - cmdline.GetNextArgumentOrError(ref contentsFile); - return true; - case "outputsfile": - cmdline.GetNextArgumentOrError(ref outputsFile); - return true; - case "builtoutputsfile": - cmdline.GetNextArgumentOrError(ref builtOutputsFile); - return true; - case "wixprojectfile": - cmdline.GetNextArgumentOrError(ref wixProjectFile); - return true; - - case "d": - case "define": - cmdline.GetNextArgumentOrError(defines); - return true; - - case "i": - case "includepath": - cmdline.GetNextArgumentOrError(includePaths); - return true; - - case "intermediatefolder": - cmdline.GetNextArgumentOrError(ref intermediateFolder); - return true; - - case "loc": - cmdline.GetNextArgumentAsFilePathOrError(locFiles, "localization files"); - return true; - - case "lib": - cmdline.GetNextArgumentAsFilePathOrError(libraryFiles, "library files"); - return true; - - case "o": - case "out": - cmdline.GetNextArgumentOrError(ref outputFile); - return true; - - case "outputtype": - cmdline.GetNextArgumentOrError(ref outputType); - return true; - - case "nologo": - showLogo = false; - return true; - - case "v": - case "verbose": - verbose = true; - return true; - - case "version": - case "-version": - showVersion = true; - return true; - } - - return false; - } - else - { - files.AddRange(cmdline.GetFiles(arg, "source code")); - return true; - } - }); - - this.Messaging.ShowVerboseMessages = verbose; - - if (showVersion) - { - return new VersionCommand(); - } - - if (showLogo) - { - AppCommon.DisplayToolHeader(); - } - - if (this.ShowHelp) - { - return new HelpCommand(command); - } - - switch (command) - { - case Commands.Build: - { - var sourceFiles = GatherSourceFiles(files, outputFolder); - var variables = this.GatherPreprocessorVariables(defines); - var bindPathList = this.GatherBindPaths(bindPaths); - var type = CalculateOutputType(outputType, outputFile); - return new BuildCommand(this.ServiceProvider, this.Messaging, this.ExtensionManager, sourceFiles, variables, locFiles, libraryFiles, outputFile, type, cabCachePath, cultures, bindFiles, bindPathList, intermediateFolder, contentsFile, outputsFile, builtOutputsFile, wixProjectFile); - } - - case Commands.Compile: - { - var sourceFiles = GatherSourceFiles(files, outputFolder); - var variables = GatherPreprocessorVariables(defines); - return new CompileCommand(this.ServiceProvider, this.Messaging, this.ExtensionManager, sourceFiles, variables); - } - } - - return null; - } - - private static OutputType CalculateOutputType(string outputType, string outputFile) - { - if (String.IsNullOrEmpty(outputType)) - { - outputType = Path.GetExtension(outputFile); - } - - switch (outputType.ToLowerInvariant()) - { - case "bundle": - case ".exe": - return OutputType.Bundle; - - case "library": - case ".wixlib": - return OutputType.Library; - - case "module": - case ".msm": - return OutputType.Module; - - case "patch": - case ".msp": - return OutputType.Patch; - - case ".pcp": - return OutputType.PatchCreation; - - case "product": - case ".msi": - return OutputType.Product; - - case "transform": - case ".mst": - return OutputType.Transform; - - case "wixout": - case ".wixout": - return OutputType.Wixout; - } - - return OutputType.Unknown; - } - -#if UNUSED - private static CommandLine Parse(string commandLineString, Func parseArgument) - { - var arguments = CommandLine.ParseArgumentsToArray(commandLineString).ToArray(); - - return CommandLine.Parse(arguments, null, parseArgument); - } - - private static CommandLine Parse(string[] commandLineArguments, Func parseArgument) - { - return CommandLine.Parse(commandLineArguments, null, parseArgument); - } -#endif - - private ICommandLine Parse(ICommandLineContext context, string[] commandLineArguments, Func parseCommand, Func parseArgument) - { - this.FlattenArgumentsWithResponseFilesIntoOriginalArguments(commandLineArguments); - - this.QueueArgumentsAndLoadExtensions(this.OriginalArguments); - - this.ProcessRemainingArguments(context, parseArgument, parseCommand); - - return this; - } - - private static IEnumerable GatherSourceFiles(IEnumerable sourceFiles, string intermediateDirectory) - { - var files = new List(); - - foreach (var item in sourceFiles) - { - var sourcePath = item; - var outputPath = Path.Combine(intermediateDirectory, Path.GetFileNameWithoutExtension(sourcePath) + ".wir"); - - files.Add(new SourceFile(sourcePath, outputPath)); - } - - return files; - } - - private IDictionary GatherPreprocessorVariables(IEnumerable defineConstants) - { - var variables = new Dictionary(); - - foreach (var pair in defineConstants) - { - string[] value = pair.Split(new[] { '=' }, 2); - - if (variables.ContainsKey(value[0])) - { - this.Messaging.Write(ErrorMessages.DuplicateVariableDefinition(value[0], (1 == value.Length) ? String.Empty : value[1], variables[value[0]])); - continue; - } - - variables.Add(value[0], (1 == value.Length) ? String.Empty : value[1]); - } - - return variables; - } - - private IEnumerable GatherBindPaths(IEnumerable bindPaths) - { - var result = new List(); - - foreach (var bindPath in bindPaths) - { - BindPath bp = BindPath.Parse(bindPath); - - if (Directory.Exists(bp.Path)) - { - result.Add(bp); - } - else if (File.Exists(bp.Path)) - { - this.Messaging.Write(ErrorMessages.ExpectedDirectoryGotFile("-bindpath", bp.Path)); - } - } - - return result; - } - - /// - /// Get a set of files that possibly have a search pattern in the path (such as '*'). - /// - /// Search path to find files in. - /// Type of file; typically "Source". - /// An array of files matching the search path. - /// - /// This method is written in this verbose way because it needs to support ".." in the path. - /// It needs the directory path isolated from the file name in order to use Directory.GetFiles - /// or DirectoryInfo.GetFiles. The only way to get this directory path is manually since - /// Path.GetDirectoryName does not support ".." in the path. - /// - /// Throws WixFileNotFoundException if no file matching the pattern can be found. - public string[] GetFiles(string searchPath, string fileType) - { - if (null == searchPath) - { - throw new ArgumentNullException(nameof(searchPath)); - } - - // Convert alternate directory separators to the standard one. - string filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - int lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar); - string[] files = null; - - try - { - if (0 > lastSeparator) - { - files = Directory.GetFiles(".", filePath); - } - else // found directory separator - { - files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), filePath.Substring(lastSeparator + 1)); - } - } - catch (DirectoryNotFoundException) - { - // Don't let this function throw the DirectoryNotFoundException. This exception - // occurs for non-existant directories and invalid characters in the searchPattern. - } - catch (ArgumentException) - { - // Don't let this function throw the ArgumentException. This exception - // occurs in certain situations such as when passing a malformed UNC path. - } - catch (IOException) - { - throw new WixFileNotFoundException(searchPath, fileType); - } - - if (null == files || 0 == files.Length) - { - throw new WixFileNotFoundException(searchPath, fileType); - } - - return files; - } - - /// - /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity - /// - /// The list of strings to check. - /// The index (in args) of the commandline parameter to be validated. - /// True if a valid switch exists there, false if not. - public bool IsSwitch(string arg) - { - return arg != null && arg.Length > 1 && ('/' == arg[0] || '-' == arg[0]); - } - - /// - /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity - /// - /// The list of strings to check. - /// The index (in args) of the commandline parameter to be validated. - /// True if a valid switch exists there, false if not. - public bool IsSwitchAt(IEnumerable args, int index) - { - var arg = args.ElementAtOrDefault(index); - return IsSwitch(arg); - } - - public void GetNextArgumentOrError(ref string arg) - { - this.TryGetNextArgumentOrError(out arg); - } - - public void GetNextArgumentOrError(IList args) - { - if (this.TryGetNextArgumentOrError(out var arg)) - { - args.Add(arg); - } - } - - public void GetNextArgumentAsFilePathOrError(IList args, string fileType) - { - if (this.TryGetNextArgumentOrError(out var arg)) - { - foreach (var path in this.GetFiles(arg, fileType)) - { - args.Add(path); - } - } - } - - public bool TryGetNextArgumentOrError(out string arg) - { - //if (this.RemainingArguments.TryDequeue(out arg) && !this.IsSwitch(arg)) - if (TryDequeue(this.RemainingArguments, out arg) && !this.IsSwitch(arg)) - { - return true; - } - - this.ErrorArgument = arg ?? CommandLine.ExpectedArgument; - - return false; - } - - private static bool TryDequeue(Queue q, out string arg) - { - if (q.Count > 0) - { - arg = q.Dequeue(); - return true; - } - - arg = null; - return false; - } - - private void FlattenArgumentsWithResponseFilesIntoOriginalArguments(string[] commandLineArguments) - { - List args = new List(); - - foreach (var arg in commandLineArguments) - { - if ('@' == arg[0]) - { - var responseFileArguments = CommandLine.ParseResponseFile(arg.Substring(1)); - args.AddRange(responseFileArguments); - } - else - { - args.Add(arg); - } - } - - this.OriginalArguments = args.ToArray(); - } - - private void QueueArgumentsAndLoadExtensions(string[] args) - { - for (var i = 0; i < args.Length; ++i) - { - var arg = args[i]; - - if ("-ext" == arg || "/ext" == arg) - { - if (!this.IsSwitchAt(args, ++i)) - { - this.ExtensionManager.Load(args[i]); - } - else - { - this.ErrorArgument = arg; - break; - } - } - else - { - this.RemainingArguments.Enqueue(arg); - } - } - } - - private void ProcessRemainingArguments(ICommandLineContext context, Func parseArgument, Func parseCommand) - { - var extensions = this.ExtensionManager.Create(); - - foreach (var extension in extensions) - { - extension.PreParse(context); - } - - while (!this.ShowHelp && - String.IsNullOrEmpty(this.ErrorArgument) && - TryDequeue(this.RemainingArguments, out var arg)) - { - if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments. - { - continue; - } - - if ('-' == arg[0] || '/' == arg[0]) - { - if (!parseArgument(this, arg) && - !this.TryParseCommandLineArgumentWithExtension(arg, extensions)) - { - this.ErrorArgument = arg; - } - } - else if (String.IsNullOrEmpty(this.ActiveCommand) && parseCommand != null) // First non-switch must be the command, if commands are supported. - { - if (parseCommand(this, arg)) - { - this.ActiveCommand = arg; - } - else - { - this.ErrorArgument = arg; - } - } - else if (!this.TryParseCommandLineArgumentWithExtension(arg, extensions) && - !parseArgument(this, arg)) - { - this.ErrorArgument = arg; - } - } - } - - private bool TryParseCommandLineArgumentWithExtension(string arg, IEnumerable extensions) - { - foreach (var extension in extensions) - { - if (extension.TryParseArgument(this, arg)) - { - return true; - } - } - - return false; - } - - private static List ParseResponseFile(string responseFile) - { - string arguments; - - using (StreamReader reader = new StreamReader(responseFile)) - { - arguments = reader.ReadToEnd(); - } - - return CommandLine.ParseArgumentsToArray(arguments); - } - - private static List ParseArgumentsToArray(string arguments) - { - // Scan and parse the arguments string, dividing up the arguments based on whitespace. - // Unescaped quotes cause whitespace to be ignored, while the quotes themselves are removed. - // Quotes may begin and end inside arguments; they don't necessarily just surround whole arguments. - // Escaped quotes and escaped backslashes also need to be unescaped by this process. - - // Collects the final list of arguments to be returned. - var argsList = new List(); - - // True if we are inside an unescaped quote, meaning whitespace should be ignored. - var insideQuote = false; - - // Index of the start of the current argument substring; either the start of the argument - // or the start of a quoted or unquoted sequence within it. - var partStart = 0; - - // The current argument string being built; when completed it will be added to the list. - var arg = new StringBuilder(); - - for (int i = 0; i <= arguments.Length; i++) - { - if (i == arguments.Length || (Char.IsWhiteSpace(arguments[i]) && !insideQuote)) - { - // Reached a whitespace separator or the end of the string. - - // Finish building the current argument. - arg.Append(arguments.Substring(partStart, i - partStart)); - - // Skip over the whitespace character. - partStart = i + 1; - - // Add the argument to the list if it's not empty. - if (arg.Length > 0) - { - argsList.Add(CommandLine.ExpandEnvironmentVariables(arg.ToString())); - arg.Length = 0; - } - } - else if (i > partStart && arguments[i - 1] == '\\') - { - // Check the character following an unprocessed backslash. - // Unescape quotes, and backslashes followed by a quote. - if (arguments[i] == '"' || (arguments[i] == '\\' && arguments.Length > i + 1 && arguments[i + 1] == '"')) - { - // Unescape the quote or backslash by skipping the preceeding backslash. - arg.Append(arguments.Substring(partStart, i - 1 - partStart)); - arg.Append(arguments[i]); - partStart = i + 1; - } - } - else if (arguments[i] == '"') - { - // Add the quoted or unquoted section to the argument string. - arg.Append(arguments.Substring(partStart, i - partStart)); - - // And skip over the quote character. - partStart = i + 1; - - insideQuote = !insideQuote; - } - } - - return argsList; - } - - private static string ExpandEnvironmentVariables(string arguments) - { - var id = Environment.GetEnvironmentVariables(); - - var regex = new Regex("(?<=\\%)(?:[\\w\\.]+)(?=\\%)"); - MatchCollection matches = regex.Matches(arguments); - - string value = String.Empty; - for (int i = 0; i <= (matches.Count - 1); i++) - { - try - { - var key = matches[i].Value; - regex = new Regex(String.Concat("(?i)(?:\\%)(?:", key, ")(?:\\%)")); - value = id[key].ToString(); - arguments = regex.Replace(arguments, value); - } - catch (NullReferenceException) - { - // Collapse unresolved environment variables. - arguments = regex.Replace(arguments, value); - } - } - - return arguments; - } - } -} diff --git a/src/WixToolset.Core/CommandLine/CommandLineContext.cs b/src/WixToolset.Core/CommandLine/CommandLineContext.cs index cbb9af53..2ff2c1fd 100644 --- a/src/WixToolset.Core/CommandLine/CommandLineContext.cs +++ b/src/WixToolset.Core/CommandLine/CommandLineContext.cs @@ -1,6 +1,6 @@ // 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 WixToolset.Core +namespace WixToolset.Core.CommandLine { using System; using WixToolset.Extensibility.Services; diff --git a/src/WixToolset.Core/CommandLine/CommandLineHelper.cs b/src/WixToolset.Core/CommandLine/CommandLineHelper.cs new file mode 100644 index 00000000..51ece0f7 --- /dev/null +++ b/src/WixToolset.Core/CommandLine/CommandLineHelper.cs @@ -0,0 +1,216 @@ +// 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 WixToolset.Core.CommandLine +{ + using System; + using System.IO; + using WixToolset.Data; + using WixToolset.Extensibility.Services; + + public class CommandLineHelper + { + /// + /// Validates that a string is a valid directory name, and throws appropriate warnings/errors if not + /// + /// The commandline switch we're parsing (for error display purposes). + /// The messagehandler to report warnings/errors to. + /// The list of strings to check. + /// The index (in args) of the commandline parameter to be parsed. + /// The string if it is valid, null if it is invalid. + public static string GetDirectory(string commandlineSwitch, IMessaging messageHandler, string[] args, int index) + { + return GetDirectory(commandlineSwitch, messageHandler, args, index, false); + } + + /// + /// Validates that a string is a valid directory name, and throws appropriate warnings/errors if not + /// + /// The commandline switch we're parsing (for error display purposes). + /// The messagehandler to report warnings/errors to. + /// The list of strings to check. + /// The index (in args) of the commandline parameter to be parsed. + /// Indicates if a colon-delimited prefix is allowed. + /// The string if it is valid, null if it is invalid. + public static string GetDirectory(string commandlineSwitch, IMessaging messageHandler, string[] args, int index, bool allowPrefix) + { + commandlineSwitch = String.Concat("-", commandlineSwitch); + + if (!IsValidArg(args, index)) + { + messageHandler.Write(ErrorMessages.DirectoryPathRequired(commandlineSwitch)); + return null; + } + + if (File.Exists(args[index])) + { + messageHandler.Write(ErrorMessages.ExpectedDirectoryGotFile(commandlineSwitch, args[index])); + return null; + } + + return VerifyPath(messageHandler, args[index], allowPrefix); + } + + /// + /// Validates that a string is a valid filename, and throws appropriate warnings/errors if not + /// + /// The commandline switch we're parsing (for error display purposes). + /// The messagehandler to report warnings/errors to. + /// The list of strings to check. + /// The index (in args) of the commandline parameter to be parsed. + /// The string if it is valid, null if it is invalid. + public static string GetFile(string commandlineSwitch, IMessaging messageHandler, string[] args, int index) + { + commandlineSwitch = String.Concat("-", commandlineSwitch); + + if (!IsValidArg(args, index)) + { + messageHandler.Write(ErrorMessages.FilePathRequired(commandlineSwitch)); + return null; + } + + if (Directory.Exists(args[index])) + { + messageHandler.Write(ErrorMessages.ExpectedFileGotDirectory(commandlineSwitch, args[index])); + return null; + } + + return VerifyPath(messageHandler, args[index]); + } + + /// + /// Get a set of files that possibly have a search pattern in the path (such as '*'). + /// + /// Search path to find files in. + /// Type of file; typically "Source". + /// An array of files matching the search path. + /// + /// This method is written in this verbose way because it needs to support ".." in the path. + /// It needs the directory path isolated from the file name in order to use Directory.GetFiles + /// or DirectoryInfo.GetFiles. The only way to get this directory path is manually since + /// Path.GetDirectoryName does not support ".." in the path. + /// + /// Throws WixFileNotFoundException if no file matching the pattern can be found. + public static string[] GetFiles(string searchPath, string fileType) + { + if (null == searchPath) + { + throw new ArgumentNullException(nameof(searchPath)); + } + + // Convert alternate directory separators to the standard one. + string filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + int lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar); + string[] files = null; + + try + { + if (0 > lastSeparator) + { + files = Directory.GetFiles(".", filePath); + } + else // found directory separator + { + files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), filePath.Substring(lastSeparator + 1)); + } + } + catch (DirectoryNotFoundException) + { + // Don't let this function throw the DirectoryNotFoundException. This exception + // occurs for non-existant directories and invalid characters in the searchPattern. + } + catch (ArgumentException) + { + // Don't let this function throw the ArgumentException. This exception + // occurs in certain situations such as when passing a malformed UNC path. + } + catch (IOException) + { + throw new WixFileNotFoundException(searchPath, fileType); + } + + if (null == files || 0 == files.Length) + { + throw new WixFileNotFoundException(searchPath, fileType); + } + + return files; + } + + /// + /// Validates that a valid string parameter (without "/" or "-"), and returns a bool indicating its validity + /// + /// The list of strings to check. + /// The index (in args) of the commandline parameter to be validated. + /// True if a valid string parameter exists there, false if not. + public static bool IsValidArg(string[] args, int index) + { + if (args.Length <= index || String.IsNullOrEmpty(args[index]) || '/' == args[index][0] || '-' == args[index][0]) + { + return false; + } + else + { + return true; + } + } + + /// + /// Validates that a commandline parameter is a valid file or directory name, and throws appropriate warnings/errors if not + /// + /// The messagehandler to report warnings/errors to. + /// The path to test. + /// The string if it is valid, null if it is invalid. + public static string VerifyPath(IMessaging messageHandler, string path) + { + return VerifyPath(messageHandler, path, false); + } + + /// + /// Validates that a commandline parameter is a valid file or directory name, and throws appropriate warnings/errors if not + /// + /// The messagehandler to report warnings/errors to. + /// The path to test. + /// Indicates if a colon-delimited prefix is allowed. + /// The full path if it is valid, null if it is invalid. + public static string VerifyPath(IMessaging messageHandler, string path, bool allowPrefix) + { + string fullPath; + + if (0 <= path.IndexOf('\"')) + { + messageHandler.Write(ErrorMessages.PathCannotContainQuote(path)); + return null; + } + + try + { + string prefix = null; + if (allowPrefix) + { + int prefixLength = path.IndexOf('=') + 1; + if (0 != prefixLength) + { + prefix = path.Substring(0, prefixLength); + path = path.Substring(prefixLength); + } + } + + if (String.IsNullOrEmpty(prefix)) + { + fullPath = Path.GetFullPath(path); + } + else + { + fullPath = String.Concat(prefix, Path.GetFullPath(path)); + } + } + catch (Exception e) + { + messageHandler.Write(ErrorMessages.InvalidCommandLineFileName(path, e.Message)); + return null; + } + + return fullPath; + } + } +} diff --git a/src/WixToolset.Core/CommandLine/CommandLineParser.cs b/src/WixToolset.Core/CommandLine/CommandLineParser.cs new file mode 100644 index 00000000..0e7da42a --- /dev/null +++ b/src/WixToolset.Core/CommandLine/CommandLineParser.cs @@ -0,0 +1,628 @@ +// 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 WixToolset.Core.CommandLine +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using WixToolset.Data; + using WixToolset.Extensibility; + using WixToolset.Extensibility.Services; + + internal enum Commands + { + Unknown, + Build, + Preprocess, + Compile, + Link, + Bind, + } + + internal class CommandLineParser : ICommandLine, IParseCommandLine + { + private IServiceProvider ServiceProvider { get; set; } + + private IMessaging Messaging { get; set; } + + public static string ExpectedArgument { get; } = "expected argument"; + + public string ActiveCommand { get; private set; } + + public string[] OriginalArguments { get; private set; } + + public Queue RemainingArguments { get; } = new Queue(); + + public IExtensionManager ExtensionManager { get; private set; } + + public string ErrorArgument { get; set; } + + public bool ShowHelp { get; set; } + + public ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context) + { + this.ServiceProvider = context.ServiceProvider; + + this.Messaging = context.Messaging ?? this.ServiceProvider.GetService(); + + this.ExtensionManager = context.ExtensionManager ?? this.ServiceProvider.GetService(); + + var args = context.ParsedArguments ?? Array.Empty(); + + if (!String.IsNullOrEmpty(context.Arguments)) + { + args = CommandLineParser.ParseArgumentsToArray(context.Arguments).Union(args).ToArray(); + } + + return this.ParseStandardCommandLine(context, args); + } + + private ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context, string[] args) + { + var next = String.Empty; + + var command = Commands.Unknown; + var showLogo = true; + var showVersion = false; + var outputFolder = String.Empty; + var outputFile = String.Empty; + var outputType = String.Empty; + var verbose = false; + var files = new List(); + var defines = new List(); + var includePaths = new List(); + var locFiles = new List(); + var libraryFiles = new List(); + var suppressedWarnings = new List(); + + var bindFiles = false; + var bindPaths = new List(); + + var intermediateFolder = String.Empty; + + var cabCachePath = String.Empty; + var cultures = new List(); + var contentsFile = String.Empty; + var outputsFile = String.Empty; + var builtOutputsFile = String.Empty; + + this.Parse(context, args, (cmdline, arg) => Enum.TryParse(arg, true, out command), (cmdline, arg) => + { + if (cmdline.IsSwitch(arg)) + { + var parameter = arg.Substring(1); + switch (parameter.ToLowerInvariant()) + { + case "?": + case "h": + case "help": + cmdline.ShowHelp = true; + return true; + + case "bindfiles": + bindFiles = true; + return true; + + case "bindpath": + cmdline.GetNextArgumentOrError(bindPaths); + return true; + + case "cc": + cmdline.GetNextArgumentOrError(ref cabCachePath); + return true; + + case "cultures": + cmdline.GetNextArgumentOrError(cultures); + return true; + case "contentsfile": + cmdline.GetNextArgumentOrError(ref contentsFile); + return true; + case "outputsfile": + cmdline.GetNextArgumentOrError(ref outputsFile); + return true; + case "builtoutputsfile": + cmdline.GetNextArgumentOrError(ref builtOutputsFile); + return true; + + case "d": + case "define": + cmdline.GetNextArgumentOrError(defines); + return true; + + case "i": + case "includepath": + cmdline.GetNextArgumentOrError(includePaths); + return true; + + case "intermediatefolder": + cmdline.GetNextArgumentOrError(ref intermediateFolder); + return true; + + case "loc": + cmdline.GetNextArgumentAsFilePathOrError(locFiles, "localization files"); + return true; + + case "lib": + cmdline.GetNextArgumentAsFilePathOrError(libraryFiles, "library files"); + return true; + + case "o": + case "out": + cmdline.GetNextArgumentOrError(ref outputFile); + return true; + + case "outputtype": + cmdline.GetNextArgumentOrError(ref outputType); + return true; + + case "nologo": + showLogo = false; + return true; + + case "v": + case "verbose": + verbose = true; + return true; + + case "version": + case "-version": + showVersion = true; + return true; + } + + return false; + } + else + { + files.AddRange(CommandLineHelper.GetFiles(arg, "source code")); + return true; + } + }); + + this.Messaging.ShowVerboseMessages = verbose; + + if (showVersion) + { + return new VersionCommand(); + } + + if (showLogo) + { + AppCommon.DisplayToolHeader(); + } + + if (this.ShowHelp) + { + return new HelpCommand(command); + } + + switch (command) + { + case Commands.Build: + { + var sourceFiles = GatherSourceFiles(files, outputFolder); + var variables = this.GatherPreprocessorVariables(defines); + var bindPathList = this.GatherBindPaths(bindPaths); + var type = CalculateOutputType(outputType, outputFile); + return new BuildCommand(this.ServiceProvider, this.Messaging, this.ExtensionManager, sourceFiles, variables, locFiles, libraryFiles, outputFile, type, cabCachePath, cultures, bindFiles, bindPathList, intermediateFolder, contentsFile, outputsFile, builtOutputsFile); + } + + case Commands.Compile: + { + var sourceFiles = GatherSourceFiles(files, outputFolder); + var variables = GatherPreprocessorVariables(defines); + return new CompileCommand(this.ServiceProvider, this.Messaging, this.ExtensionManager, sourceFiles, variables); + } + } + + return null; + } + + private static OutputType CalculateOutputType(string outputType, string outputFile) + { + if (String.IsNullOrEmpty(outputType)) + { + outputType = Path.GetExtension(outputFile); + } + + switch (outputType.ToLowerInvariant()) + { + case "bundle": + case ".exe": + return OutputType.Bundle; + + case "library": + case ".wixlib": + return OutputType.Library; + + case "module": + case ".msm": + return OutputType.Module; + + case "patch": + case ".msp": + return OutputType.Patch; + + case ".pcp": + return OutputType.PatchCreation; + + case "product": + case ".msi": + return OutputType.Product; + + case "transform": + case ".mst": + return OutputType.Transform; + + case "wixout": + case ".wixout": + return OutputType.Wixout; + } + + return OutputType.Unknown; + } + +#if UNUSED + private static CommandLine Parse(string commandLineString, Func parseArgument) + { + var arguments = CommandLine.ParseArgumentsToArray(commandLineString).ToArray(); + + return CommandLine.Parse(arguments, null, parseArgument); + } + + private static CommandLine Parse(string[] commandLineArguments, Func parseArgument) + { + return CommandLine.Parse(commandLineArguments, null, parseArgument); + } +#endif + + private ICommandLine Parse(ICommandLineContext context, string[] commandLineArguments, Func parseCommand, Func parseArgument) + { + this.FlattenArgumentsWithResponseFilesIntoOriginalArguments(commandLineArguments); + + this.QueueArgumentsAndLoadExtensions(this.OriginalArguments); + + this.ProcessRemainingArguments(context, parseArgument, parseCommand); + + return this; + } + + private static IEnumerable GatherSourceFiles(IEnumerable sourceFiles, string intermediateDirectory) + { + var files = new List(); + + foreach (var item in sourceFiles) + { + var sourcePath = item; + var outputPath = Path.Combine(intermediateDirectory, Path.GetFileNameWithoutExtension(sourcePath) + ".wir"); + + files.Add(new SourceFile(sourcePath, outputPath)); + } + + return files; + } + + private IDictionary GatherPreprocessorVariables(IEnumerable defineConstants) + { + var variables = new Dictionary(); + + foreach (var pair in defineConstants) + { + string[] value = pair.Split(new[] { '=' }, 2); + + if (variables.ContainsKey(value[0])) + { + this.Messaging.Write(ErrorMessages.DuplicateVariableDefinition(value[0], (1 == value.Length) ? String.Empty : value[1], variables[value[0]])); + continue; + } + + variables.Add(value[0], (1 == value.Length) ? String.Empty : value[1]); + } + + return variables; + } + + private IEnumerable GatherBindPaths(IEnumerable bindPaths) + { + var result = new List(); + + foreach (var bindPath in bindPaths) + { + var bp = BindPath.Parse(bindPath); + + if (Directory.Exists(bp.Path)) + { + result.Add(bp); + } + else if (File.Exists(bp.Path)) + { + this.Messaging.Write(ErrorMessages.ExpectedDirectoryGotFile("-bindpath", bp.Path)); + } + } + + return result; + } + + /// + /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity + /// + /// The list of strings to check. + /// The index (in args) of the commandline parameter to be validated. + /// True if a valid switch exists there, false if not. + public bool IsSwitch(string arg) + { + return arg != null && arg.Length > 1 && ('/' == arg[0] || '-' == arg[0]); + } + + /// + /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity + /// + /// The list of strings to check. + /// The index (in args) of the commandline parameter to be validated. + /// True if a valid switch exists there, false if not. + public bool IsSwitchAt(IEnumerable args, int index) + { + var arg = args.ElementAtOrDefault(index); + return IsSwitch(arg); + } + + public void GetNextArgumentOrError(ref string arg) + { + this.TryGetNextArgumentOrError(out arg); + } + + public void GetNextArgumentOrError(IList args) + { + if (this.TryGetNextArgumentOrError(out var arg)) + { + args.Add(arg); + } + } + + public void GetNextArgumentAsFilePathOrError(IList args, string fileType) + { + if (this.TryGetNextArgumentOrError(out var arg)) + { + foreach (var path in CommandLineHelper.GetFiles(arg, fileType)) + { + args.Add(path); + } + } + } + + public bool TryGetNextArgumentOrError(out string arg) + { + if (TryDequeue(this.RemainingArguments, out arg) && !this.IsSwitch(arg)) + { + return true; + } + + this.ErrorArgument = arg ?? CommandLineParser.ExpectedArgument; + + return false; + } + + private static bool TryDequeue(Queue q, out string arg) + { + if (q.Count > 0) + { + arg = q.Dequeue(); + return true; + } + + arg = null; + return false; + } + + private void FlattenArgumentsWithResponseFilesIntoOriginalArguments(string[] commandLineArguments) + { + List args = new List(); + + foreach (var arg in commandLineArguments) + { + if ('@' == arg[0]) + { + var responseFileArguments = CommandLineParser.ParseResponseFile(arg.Substring(1)); + args.AddRange(responseFileArguments); + } + else + { + args.Add(arg); + } + } + + this.OriginalArguments = args.ToArray(); + } + + private void QueueArgumentsAndLoadExtensions(string[] args) + { + for (var i = 0; i < args.Length; ++i) + { + var arg = args[i]; + + if ("-ext" == arg || "/ext" == arg) + { + if (!this.IsSwitchAt(args, ++i)) + { + this.ExtensionManager.Load(args[i]); + } + else + { + this.ErrorArgument = arg; + break; + } + } + else + { + this.RemainingArguments.Enqueue(arg); + } + } + } + + private void ProcessRemainingArguments(ICommandLineContext context, Func parseArgument, Func parseCommand) + { + var extensions = this.ExtensionManager.Create(); + + foreach (var extension in extensions) + { + extension.PreParse(context); + } + + while (!this.ShowHelp && + String.IsNullOrEmpty(this.ErrorArgument) && + TryDequeue(this.RemainingArguments, out var arg)) + { + if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments. + { + continue; + } + + if ('-' == arg[0] || '/' == arg[0]) + { + if (!parseArgument(this, arg) && + !this.TryParseCommandLineArgumentWithExtension(arg, extensions)) + { + this.ErrorArgument = arg; + } + } + else if (String.IsNullOrEmpty(this.ActiveCommand) && parseCommand != null) // First non-switch must be the command, if commands are supported. + { + if (parseCommand(this, arg)) + { + this.ActiveCommand = arg; + } + else + { + this.ErrorArgument = arg; + } + } + else if (!this.TryParseCommandLineArgumentWithExtension(arg, extensions) && + !parseArgument(this, arg)) + { + this.ErrorArgument = arg; + } + } + } + + private bool TryParseCommandLineArgumentWithExtension(string arg, IEnumerable extensions) + { + foreach (var extension in extensions) + { + if (extension.TryParseArgument(this, arg)) + { + return true; + } + } + + return false; + } + + private static List ParseResponseFile(string responseFile) + { + string arguments; + + using (StreamReader reader = new StreamReader(responseFile)) + { + arguments = reader.ReadToEnd(); + } + + return CommandLineParser.ParseArgumentsToArray(arguments); + } + + private static List ParseArgumentsToArray(string arguments) + { + // Scan and parse the arguments string, dividing up the arguments based on whitespace. + // Unescaped quotes cause whitespace to be ignored, while the quotes themselves are removed. + // Quotes may begin and end inside arguments; they don't necessarily just surround whole arguments. + // Escaped quotes and escaped backslashes also need to be unescaped by this process. + + // Collects the final list of arguments to be returned. + var argsList = new List(); + + // True if we are inside an unescaped quote, meaning whitespace should be ignored. + var insideQuote = false; + + // Index of the start of the current argument substring; either the start of the argument + // or the start of a quoted or unquoted sequence within it. + var partStart = 0; + + // The current argument string being built; when completed it will be added to the list. + var arg = new StringBuilder(); + + for (int i = 0; i <= arguments.Length; i++) + { + if (i == arguments.Length || (Char.IsWhiteSpace(arguments[i]) && !insideQuote)) + { + // Reached a whitespace separator or the end of the string. + + // Finish building the current argument. + arg.Append(arguments.Substring(partStart, i - partStart)); + + // Skip over the whitespace character. + partStart = i + 1; + + // Add the argument to the list if it's not empty. + if (arg.Length > 0) + { + argsList.Add(CommandLineParser.ExpandEnvironmentVariables(arg.ToString())); + arg.Length = 0; + } + } + else if (i > partStart && arguments[i - 1] == '\\') + { + // Check the character following an unprocessed backslash. + // Unescape quotes, and backslashes followed by a quote. + if (arguments[i] == '"' || (arguments[i] == '\\' && arguments.Length > i + 1 && arguments[i + 1] == '"')) + { + // Unescape the quote or backslash by skipping the preceeding backslash. + arg.Append(arguments.Substring(partStart, i - 1 - partStart)); + arg.Append(arguments[i]); + partStart = i + 1; + } + } + else if (arguments[i] == '"') + { + // Add the quoted or unquoted section to the argument string. + arg.Append(arguments.Substring(partStart, i - partStart)); + + // And skip over the quote character. + partStart = i + 1; + + insideQuote = !insideQuote; + } + } + + return argsList; + } + + private static string ExpandEnvironmentVariables(string arguments) + { + var id = Environment.GetEnvironmentVariables(); + + var regex = new Regex("(?<=\\%)(?:[\\w\\.]+)(?=\\%)"); + MatchCollection matches = regex.Matches(arguments); + + string value = String.Empty; + for (int i = 0; i <= (matches.Count - 1); i++) + { + try + { + var key = matches[i].Value; + regex = new Regex(String.Concat("(?i)(?:\\%)(?:", key, ")(?:\\%)")); + value = id[key].ToString(); + arguments = regex.Replace(arguments, value); + } + catch (NullReferenceException) + { + // Collapse unresolved environment variables. + arguments = regex.Replace(arguments, value); + } + } + + return arguments; + } + } +} diff --git a/src/WixToolset.Core/CommandLine/CommandLineResponseFile.cs b/src/WixToolset.Core/CommandLine/CommandLineResponseFile.cs index 578c3b22..6922b246 100644 --- a/src/WixToolset.Core/CommandLine/CommandLineResponseFile.cs +++ b/src/WixToolset.Core/CommandLine/CommandLineResponseFile.cs @@ -1,6 +1,6 @@ // 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 WixToolset +namespace WixToolset.Core.CommandLine { using System; using System.Collections; diff --git a/src/WixToolset.Core/CommandLine/CompileCommand.cs b/src/WixToolset.Core/CommandLine/CompileCommand.cs index 856dd29f..f0ff5b1a 100644 --- a/src/WixToolset.Core/CommandLine/CompileCommand.cs +++ b/src/WixToolset.Core/CommandLine/CompileCommand.cs @@ -1,6 +1,6 @@ // 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 WixToolset.Core +namespace WixToolset.Core.CommandLine { using System; using System.Collections.Generic; diff --git a/src/WixToolset.Core/CommandLine/HelpCommand.cs b/src/WixToolset.Core/CommandLine/HelpCommand.cs index 2a2eab24..6e547d60 100644 --- a/src/WixToolset.Core/CommandLine/HelpCommand.cs +++ b/src/WixToolset.Core/CommandLine/HelpCommand.cs @@ -1,6 +1,6 @@ // 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 WixToolset.Core +namespace WixToolset.Core.CommandLine { using System; using WixToolset.Extensibility.Services; diff --git a/src/WixToolset.Core/CommandLine/VersionCommand.cs b/src/WixToolset.Core/CommandLine/VersionCommand.cs index 12941bdc..a04aac31 100644 --- a/src/WixToolset.Core/CommandLine/VersionCommand.cs +++ b/src/WixToolset.Core/CommandLine/VersionCommand.cs @@ -1,6 +1,6 @@ // 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 WixToolset.Core +namespace WixToolset.Core.CommandLine { using System; using WixToolset.Extensibility.Services; -- cgit v1.2.3-55-g6feb