From d3d3649a68cb1fa589fdd987a6690dbd5d671f0d Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sun, 17 Sep 2017 15:35:20 -0700 Subject: Initial code commit --- src/WixToolset.Core/CommandLine/BuildCommand.cs | 100 ++++ src/WixToolset.Core/CommandLine/CommandLine.cs | 592 +++++++++++++++++++++ .../CommandLine/CommandLineHelper.cs | 255 +++++++++ .../CommandLine/CommandLineOption.cs | 27 + .../CommandLine/CommandLineResponseFile.cs | 137 +++++ src/WixToolset.Core/CommandLine/CompileCommand.cs | 39 ++ src/WixToolset.Core/CommandLine/HelpCommand.cs | 30 ++ src/WixToolset.Core/CommandLine/ICommand.cs | 9 + src/WixToolset.Core/CommandLine/VersionCommand.cs | 17 + 9 files changed, 1206 insertions(+) create mode 100644 src/WixToolset.Core/CommandLine/BuildCommand.cs create mode 100644 src/WixToolset.Core/CommandLine/CommandLine.cs create mode 100644 src/WixToolset.Core/CommandLine/CommandLineHelper.cs create mode 100644 src/WixToolset.Core/CommandLine/CommandLineOption.cs create mode 100644 src/WixToolset.Core/CommandLine/CommandLineResponseFile.cs create mode 100644 src/WixToolset.Core/CommandLine/CompileCommand.cs create mode 100644 src/WixToolset.Core/CommandLine/HelpCommand.cs create mode 100644 src/WixToolset.Core/CommandLine/ICommand.cs create mode 100644 src/WixToolset.Core/CommandLine/VersionCommand.cs (limited to 'src/WixToolset.Core/CommandLine') diff --git a/src/WixToolset.Core/CommandLine/BuildCommand.cs b/src/WixToolset.Core/CommandLine/BuildCommand.cs new file mode 100644 index 00000000..ffb48305 --- /dev/null +++ b/src/WixToolset.Core/CommandLine/BuildCommand.cs @@ -0,0 +1,100 @@ +// 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 WixToolset.Data; + + internal class BuildCommand : ICommand + { + public BuildCommand(IEnumerable sources, IDictionary preprocessorVariables, IEnumerable locFiles, string outputPath, IEnumerable cultures, string contentsFile, string outputsFile, string builtOutputsFile, string wixProjectFile) + { + this.LocFiles = locFiles; + this.PreprocessorVariables = preprocessorVariables; + this.SourceFiles = sources; + this.OutputPath = outputPath; + + this.Cultures = cultures; + this.ContentsFile = contentsFile; + this.OutputsFile = outputsFile; + this.BuiltOutputsFile = builtOutputsFile; + this.WixProjectFile = wixProjectFile; + } + + public IEnumerable LocFiles { get; } + + private IEnumerable SourceFiles { get; } + + private IDictionary PreprocessorVariables { get; } + + private string OutputPath { get; } + + public IEnumerable Cultures { get; } + + public string ContentsFile { get; } + + public string OutputsFile { get; } + + public string BuiltOutputsFile { get; } + + public string WixProjectFile { get; } + + public int Execute() + { + var intermediates = CompilePhase(); + + var sections = intermediates.SelectMany(i => i.Sections).ToList(); + + var linker = new Linker(); + + var output = linker.Link(sections, OutputType.Product); + + var localizer = new Localizer(); + + var binder = new Binder(); + binder.TempFilesLocation = Path.GetTempPath(); + binder.WixVariableResolver = new WixVariableResolver(); + binder.WixVariableResolver.Localizer = localizer; + binder.AddExtension(new BinderFileManager()); + binder.SuppressValidation = true; + + binder.ContentsFile = this.ContentsFile; + binder.OutputsFile = this.OutputsFile; + binder.BuiltOutputsFile = this.BuiltOutputsFile; + binder.WixprojectFile = this.WixProjectFile; + + foreach (var loc in this.LocFiles) + { + var localization = Localizer.ParseLocalizationFile(loc, linker.TableDefinitions); + binder.WixVariableResolver.Localizer.AddLocalization(localization); + } + + binder.Bind(output, this.OutputPath); + + return 0; + } + + private IEnumerable CompilePhase() + { + var intermediates = new List(); + + var preprocessor = new Preprocessor(); + + var compiler = new Compiler(); + + foreach (var sourceFile in this.SourceFiles) + { + var document = preprocessor.Process(sourceFile.SourcePath, this.PreprocessorVariables); + + var intermediate = compiler.Compile(document); + + intermediates.Add(intermediate); + } + + return intermediates; + } + } +} diff --git a/src/WixToolset.Core/CommandLine/CommandLine.cs b/src/WixToolset.Core/CommandLine/CommandLine.cs new file mode 100644 index 00000000..440ae9ef --- /dev/null +++ b/src/WixToolset.Core/CommandLine/CommandLine.cs @@ -0,0 +1,592 @@ +// 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; + + internal enum Commands + { + Unknown, + Build, + Preprocess, + Compile, + Link, + Bind, + } + + public class CommandLine + { + private CommandLine() + { + } + + 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 ExtensionManager ExtensionManager { get; } = new ExtensionManager(); + + public string ErrorArgument { get; set; } + + public bool ShowHelp { get; set; } + + public static ICommand ParseStandardCommandLine(string commandLineString) + { + var args = CommandLine.ParseArgumentsToArray(commandLineString).ToArray(); + + return ParseStandardCommandLine(args); + } + + public static ICommand ParseStandardCommandLine(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 sourceFile = String.Empty; + var verbose = false; + var files = new List(); + var defines = new List(); + var includePaths = new List(); + var locFiles = new List(); + var suppressedWarnings = new List(); + + var cultures = new List(); + var contentsFile = String.Empty; + var outputsFile = String.Empty; + var builtOutputsFile = String.Empty; + var wixProjectFile = String.Empty; + + var cli = CommandLine.Parse(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 "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 "loc": + cmdline.GetNextArgumentAsFilePathOrError(locFiles, "localization files"); + return true; + + case "o": + case "out": + cmdline.GetNextArgumentOrError(ref outputFile); + 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; + } + }); + + if (showVersion) + { + return new VersionCommand(); + } + + if (showLogo) + { + AppCommon.DisplayToolHeader(); + } + + if (cli.ShowHelp) + { + return new HelpCommand(command); + } + + switch (command) + { + case Commands.Build: + { + var sourceFiles = GatherSourceFiles(files, outputFolder); + var variables = GatherPreprocessorVariables(defines); + var extensions = cli.ExtensionManager; + return new BuildCommand(sourceFiles, variables, locFiles, outputFile, cultures, contentsFile, outputsFile, builtOutputsFile, wixProjectFile); + } + + case Commands.Compile: + { + var sourceFiles = GatherSourceFiles(files, outputFolder); + var variables = GatherPreprocessorVariables(defines); + return new CompileCommand(sourceFiles, variables); + } + } + + return null; + } + + 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); + } + + private static CommandLine Parse(string[] commandLineArguments, Func parseCommand, Func parseArgument) + { + var cmdline = new CommandLine(); + + cmdline.FlattenArgumentsWithResponseFilesIntoOriginalArguments(commandLineArguments); + + cmdline.QueueArgumentsAndLoadExtensions(cmdline.OriginalArguments); + + cmdline.ProcessRemainingArguments(parseArgument, parseCommand); + + return cmdline; + } + + 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 static IDictionary GatherPreprocessorVariables(IEnumerable defineConstants) + { + var variables = new Dictionary(); + + foreach (var pair in defineConstants) + { + string[] value = pair.Split(new[] { '=' }, 2); + + if (variables.ContainsKey(value[0])) + { + Messaging.Instance.OnMessage(WixErrors.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; + } + + + /// + /// 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[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(Func parseArgument, Func parseCommand) + { + var extensions = this.ExtensionManager.Create(); + + 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.ParseArgument(this, arg)) + //{ + // return true; + //} + } + + return false; + } + + /// + /// Parses a response file. + /// + /// The file to parse. + /// The array of arguments. + private static List ParseResponseFile(string responseFile) + { + string arguments; + + using (StreamReader reader = new StreamReader(responseFile)) + { + arguments = reader.ReadToEnd(); + } + + return CommandLine.ParseArgumentsToArray(arguments); + } + + /// + /// Parses an argument string into an argument array based on whitespace and quoting. + /// + /// Argument string. + /// Argument array. + 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.ExpandEnvVars(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; + } + + /// + /// Expand enxironment variables contained in the passed string + /// + /// + /// + private static string ExpandEnvVars(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/CommandLineHelper.cs b/src/WixToolset.Core/CommandLine/CommandLineHelper.cs new file mode 100644 index 00000000..86724603 --- /dev/null +++ b/src/WixToolset.Core/CommandLine/CommandLineHelper.cs @@ -0,0 +1,255 @@ +// 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 +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using WixToolset.Data; + using WixToolset.Extensibility; + + /// + /// Common utilities for Wix command-line processing. + /// + public static class CommandLineHelper + { + /// + /// 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("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); + } + + // file could not be found or path is invalid in some way + 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 path to test. + /// The string if it is valid, null if it is invalid. + public static string VerifyPath(string path) + { + return VerifyPath(path, false); + } + + /// + /// Validates that a commandline parameter is a valid file or directory name, and throws appropriate warnings/errors if not + /// + /// 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(string path, bool allowPrefix) + { + string fullPath; + + if (0 <= path.IndexOf('\"')) + { + Messaging.Instance.OnMessage(WixErrors.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) + { + Messaging.Instance.OnMessage(WixErrors.InvalidCommandLineFileName(path, e.Message)); + return null; + } + + return fullPath; + } + + /// + /// Validates that a string is a valid bind path, and throws appropriate warnings/errors if not + /// + /// The commandline switch we're parsing (for error display purposes). + /// The list of strings to check. + /// The index (in args) of the commandline parameter to be parsed. + /// The bind path if it is valid, null if it is invalid. + public static BindPath GetBindPath(string commandlineSwitch, string[] args, int index) + { + commandlineSwitch = String.Concat("-", commandlineSwitch); + + if (!IsValidArg(args, index)) + { + Messaging.Instance.OnMessage(WixErrors.DirectoryPathRequired(commandlineSwitch)); + return null; + } + + BindPath bindPath = BindPath.Parse(args[index]); + + if (File.Exists(bindPath.Path)) + { + Messaging.Instance.OnMessage(WixErrors.ExpectedDirectoryGotFile(commandlineSwitch, bindPath.Path)); + return null; + } + + bindPath.Path = VerifyPath(bindPath.Path, true); + return String.IsNullOrEmpty(bindPath.Path) ? null : bindPath; + } + + /// + /// Validates that a commandline parameter is a valid file or 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 GetFileOrDirectory(string commandlineSwitch, string[] args, int index) + { + commandlineSwitch = String.Concat("-", commandlineSwitch); + + if (!IsValidArg(args, index)) + { + Messaging.Instance.OnMessage(WixErrors.FileOrDirectoryPathRequired(commandlineSwitch)); + return null; + } + + return VerifyPath(args[index]); + } + + /// + /// 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 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, string[] args, int index, bool allowPrefix = false) + { + commandlineSwitch = String.Concat("-", commandlineSwitch); + + if (!IsValidArg(args, index)) + { + Messaging.Instance.OnMessage(WixErrors.DirectoryPathRequired(commandlineSwitch)); + return null; + } + + if (File.Exists(args[index])) + { + Messaging.Instance.OnMessage(WixErrors.ExpectedDirectoryGotFile(commandlineSwitch, args[index])); + return null; + } + + return VerifyPath(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 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, string[] args, int index) + { + commandlineSwitch = String.Concat("-", commandlineSwitch); + + if (!IsValidArg(args, index)) + { + Messaging.Instance.OnMessage(WixErrors.FilePathRequired(commandlineSwitch)); + return null; + } + + if (Directory.Exists(args[index])) + { + Messaging.Instance.OnMessage(WixErrors.ExpectedFileGotDirectory(commandlineSwitch, args[index])); + return null; + } + + return VerifyPath(args[index]); + } + } +} diff --git a/src/WixToolset.Core/CommandLine/CommandLineOption.cs b/src/WixToolset.Core/CommandLine/CommandLineOption.cs new file mode 100644 index 00000000..85a654bf --- /dev/null +++ b/src/WixToolset.Core/CommandLine/CommandLineOption.cs @@ -0,0 +1,27 @@ +// 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 +{ + /// + /// A command line option. + /// + public struct CommandLineOption + { + public string Option; + public string Description; + public int AdditionalArguments; + + /// + /// Instantiates a new BuilderCommandLineOption. + /// + /// The option name. + /// The description of the option. + /// Count of additional arguments to require after this switch. + public CommandLineOption(string option, string description, int additionalArguments) + { + this.Option = option; + this.Description = description; + this.AdditionalArguments = additionalArguments; + } + } +} diff --git a/src/WixToolset.Core/CommandLine/CommandLineResponseFile.cs b/src/WixToolset.Core/CommandLine/CommandLineResponseFile.cs new file mode 100644 index 00000000..f27296b7 --- /dev/null +++ b/src/WixToolset.Core/CommandLine/CommandLineResponseFile.cs @@ -0,0 +1,137 @@ +// 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 +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Text; + using System.Text.RegularExpressions; + + /// + /// Common utilities for Wix command-line processing. + /// + public static class CommandLineResponseFile + { + /// + /// Parses a response file. + /// + /// The file to parse. + /// The array of arguments. + public static string[] Parse(string responseFile) + { + string arguments; + + using (StreamReader reader = new StreamReader(responseFile)) + { + arguments = reader.ReadToEnd(); + } + + return CommandLineResponseFile.ParseArgumentsToArray(arguments); + } + + /// + /// Parses an argument string into an argument array based on whitespace and quoting. + /// + /// Argument string. + /// Argument array. + public static string[] 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. + List argsList = new List(); + + // True if we are inside an unescaped quote, meaning whitespace should be ignored. + bool 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. + int partStart = 0; + + // The current argument string being built; when completed it will be added to the list. + StringBuilder 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(CommandLineResponseFile.ExpandEnvVars(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.ToArray(); + } + + /// + /// Expand enxironment variables contained in the passed string + /// + /// + /// + static private string ExpandEnvVars(string arguments) + { + IDictionary id = Environment.GetEnvironmentVariables(); + + Regex regex = new Regex("(?<=\\%)(?:[\\w\\.]+)(?=\\%)"); + MatchCollection matches = regex.Matches(arguments); + + string value = String.Empty; + for (int i = 0; i <= (matches.Count - 1); i++) + { + try + { + string 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/CompileCommand.cs b/src/WixToolset.Core/CommandLine/CompileCommand.cs new file mode 100644 index 00000000..17847b57 --- /dev/null +++ b/src/WixToolset.Core/CommandLine/CompileCommand.cs @@ -0,0 +1,39 @@ +// 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 WixToolset.Data; + + internal class CompileCommand : ICommand + { + public CompileCommand(IEnumerable sources, IDictionary preprocessorVariables) + { + this.PreprocessorVariables = preprocessorVariables; + this.SourceFiles = sources; + } + + private IEnumerable SourceFiles { get; } + + private IDictionary PreprocessorVariables { get; } + + public int Execute() + { + var preprocessor = new Preprocessor(); + + var compiler = new Compiler(); + + foreach (var sourceFile in this.SourceFiles) + { + var document = preprocessor.Process(sourceFile.SourcePath, this.PreprocessorVariables); + + var intermediate = compiler.Compile(document); + + intermediate.Save(sourceFile.OutputPath); + } + + return 0; + } + } +} diff --git a/src/WixToolset.Core/CommandLine/HelpCommand.cs b/src/WixToolset.Core/CommandLine/HelpCommand.cs new file mode 100644 index 00000000..1c101781 --- /dev/null +++ b/src/WixToolset.Core/CommandLine/HelpCommand.cs @@ -0,0 +1,30 @@ +// 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; + + internal class HelpCommand : ICommand + { + public HelpCommand(Commands command) + { + this.Command = command; + } + + public Commands Command { get; } + + public int Execute() + { + if (this.Command == Commands.Unknown) + { + Console.WriteLine(); + } + else + { + Console.WriteLine(); + } + + return -1; + } + } +} diff --git a/src/WixToolset.Core/CommandLine/ICommand.cs b/src/WixToolset.Core/CommandLine/ICommand.cs new file mode 100644 index 00000000..41abbbdc --- /dev/null +++ b/src/WixToolset.Core/CommandLine/ICommand.cs @@ -0,0 +1,9 @@ +// 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 +{ + public interface ICommand + { + int Execute(); + } +} diff --git a/src/WixToolset.Core/CommandLine/VersionCommand.cs b/src/WixToolset.Core/CommandLine/VersionCommand.cs new file mode 100644 index 00000000..a1980a2b --- /dev/null +++ b/src/WixToolset.Core/CommandLine/VersionCommand.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. + +using System; + +namespace WixToolset.Core +{ + internal class VersionCommand : ICommand + { + public int Execute() + { + Console.WriteLine("wix version {0}", ThisAssembly.AssemblyInformationalVersion); + Console.WriteLine(); + + return 0; + } + } +} -- cgit v1.2.3-55-g6feb