@joebigelow / wix / commits / fc92b28f

Refactor command line parsing to enable extensions there in light.exe

Fixes wixtoolset/issues#5845

Rob Mensching committed Jul 12, 2018 at 22:27 UTC fc92b28f87599ac25d35399dc2df2f356a285960
13 files changed +754 -822
WixToolset.Core.sln
+14
@@ -16,6 +16,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixToolsetTest.BuildTasks",
16 EndProject
17 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixToolsetTest.CoreIntegration", "src\test\WixToolsetTest.CoreIntegration\WixToolsetTest.CoreIntegration.csproj", "{E8A08E86-1780-4ED4-8F63-AB2B52C1C16B}"
18 EndProject
19 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "light", "src\light\light.csproj", "{821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}"
20 +EndProject
21 Global
22 GlobalSection(SolutionConfigurationPlatforms) = preSolution
23 Debug|Any CPU = Debug|Any CPU
@@ -110,6 +112,18 @@ Global
112 {E8A08E86-1780-4ED4-8F63-AB2B52C1C16B}.Release|x64.Build.0 = Release|Any CPU
113 {E8A08E86-1780-4ED4-8F63-AB2B52C1C16B}.Release|x86.ActiveCfg = Release|Any CPU
114 {E8A08E86-1780-4ED4-8F63-AB2B52C1C16B}.Release|x86.Build.0 = Release|Any CPU
115 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
116 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Debug|Any CPU.Build.0 = Debug|Any CPU
117 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Debug|x64.ActiveCfg = Debug|Any CPU
118 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Debug|x64.Build.0 = Debug|Any CPU
119 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Debug|x86.ActiveCfg = Debug|Any CPU
120 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Debug|x86.Build.0 = Debug|Any CPU
121 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Release|Any CPU.ActiveCfg = Release|Any CPU
122 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Release|Any CPU.Build.0 = Release|Any CPU
123 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Release|x64.ActiveCfg = Release|Any CPU
124 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Release|x64.Build.0 = Release|Any CPU
125 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Release|x86.ActiveCfg = Release|Any CPU
126 + {821DCF25-4E4F-48B1-9DD6-80B01EBF9B16}.Release|x86.Build.0 = Release|Any CPU
127 EndGlobalSection
128 GlobalSection(SolutionProperties) = preSolution
129 HideSolutionNode = FALSE
src/WixToolset.BuildTasks/DoIt.cs
+12 -5
@@ -171,21 +171,23 @@ namespace WixToolset.BuildTasks
171
172 var serviceProvider = new WixToolsetServiceProvider();
173
174 - var context = serviceProvider.GetService<ICommandLineContext>();
175 -
174 var messaging = serviceProvider.GetService<IMessaging>();
175 messaging.SetListener(this.Listener);
176
177 + var arguments = serviceProvider.GetService<ICommandLineArguments>();
178 + arguments.Populate(commandLineString);
179 +
180 + var context = serviceProvider.GetService<ICommandLineContext>();
181 context.Messaging = messaging;
180 - context.ExtensionManager = this.CreateExtensionManagerWithStandardBackends(serviceProvider);
181 - context.Arguments = commandLineString;
182 + context.ExtensionManager = this.CreateExtensionManagerWithStandardBackends(serviceProvider, arguments.Extensions);
183 + context.Arguments = arguments;
184
185 var commandLine = serviceProvider.GetService<ICommandLine>();
186 var command = commandLine.ParseStandardCommandLine(context);
187 command?.Execute();
188 }
189
188 - private IExtensionManager CreateExtensionManagerWithStandardBackends(IServiceProvider serviceProvider)
190 + private IExtensionManager CreateExtensionManagerWithStandardBackends(IServiceProvider serviceProvider, string[] extensions)
191 {
192 var extensionManager = serviceProvider.GetService<IExtensionManager>();
193
@@ -194,6 +196,11 @@ namespace WixToolset.BuildTasks
196 extensionManager.Add(type.Assembly);
197 }
198
199 + foreach (var extension in extensions)
200 + {
201 + extensionManager.Load(extension);
202 + }
203 +
204 return extensionManager;
205 }
206
src/WixToolset.Core/CommandLine/CommandLineArguments.cs new
+211
@@ -0,0 +1,211 @@
1 +// 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.
2 +
3 +namespace WixToolset.Core.CommandLine
4 +{
5 + using System;
6 + using System.Collections.Generic;
7 + using System.IO;
8 + using System.Text;
9 + using System.Text.RegularExpressions;
10 + using WixToolset.Extensibility.Services;
11 +
12 + internal class CommandLineArguments : ICommandLineArguments
13 + {
14 + public string[] OriginalArguments { get; set; }
15 +
16 + public string[] Arguments { get; set; }
17 +
18 + public string[] Extensions { get; set; }
19 +
20 + public string ErrorArgument { get; set; }
21 +
22 + private IServiceProvider ServiceProvider { get; }
23 +
24 + public CommandLineArguments(IServiceProvider serviceProvider)
25 + {
26 + this.ServiceProvider = serviceProvider;
27 + }
28 +
29 + public void Populate(string commandLine)
30 + {
31 + var args = CommandLineArguments.ParseArgumentsToArray(commandLine);
32 +
33 + this.Populate(args.ToArray());
34 + }
35 +
36 + public void Populate(string[] args)
37 + {
38 + this.FlattenArgumentsWithResponseFilesIntoOriginalArguments(args);
39 +
40 + this.ProcessArgumentsAndParseExtensions(this.OriginalArguments);
41 + }
42 +
43 + public IParseCommandLine Parse()
44 + {
45 + var messaging = (IMessaging)this.ServiceProvider.GetService(typeof(IMessaging));
46 +
47 + return new ParseCommandLine(messaging, this.Arguments, this.ErrorArgument);
48 + }
49 +
50 + private void FlattenArgumentsWithResponseFilesIntoOriginalArguments(string[] commandLineArguments)
51 + {
52 + List<string> args = new List<string>();
53 +
54 + foreach (var arg in commandLineArguments)
55 + {
56 + if ('@' == arg[0])
57 + {
58 + var responseFileArguments = CommandLineArguments.ParseResponseFile(arg.Substring(1));
59 + args.AddRange(responseFileArguments);
60 + }
61 + else
62 + {
63 + args.Add(arg);
64 + }
65 + }
66 +
67 + this.OriginalArguments = args.ToArray();
68 + }
69 +
70 + private void ProcessArgumentsAndParseExtensions(string[] args)
71 + {
72 + var arguments = new List<string>();
73 + var extensions = new List<string>();
74 +
75 + for (var i = 0; i < args.Length; ++i)
76 + {
77 + var arg = args[i];
78 +
79 + if ("-ext" == arg || "/ext" == arg)
80 + {
81 + if (!CommandLineArguments.IsSwitchAt(args, ++i))
82 + {
83 + extensions.Add(args[i]);
84 + }
85 + else
86 + {
87 + this.ErrorArgument = arg;
88 + break;
89 + }
90 + }
91 + else
92 + {
93 + arguments.Add(arg);
94 + }
95 + }
96 +
97 + this.Arguments = arguments.ToArray();
98 + this.Extensions = extensions.ToArray();
99 + }
100 +
101 + private static List<string> ParseResponseFile(string responseFile)
102 + {
103 + string arguments;
104 +
105 + using (StreamReader reader = new StreamReader(responseFile))
106 + {
107 + arguments = reader.ReadToEnd();
108 + }
109 +
110 + return CommandLineArguments.ParseArgumentsToArray(arguments);
111 + }
112 +
113 + private static List<string> ParseArgumentsToArray(string arguments)
114 + {
115 + // Scan and parse the arguments string, dividing up the arguments based on whitespace.
116 + // Unescaped quotes cause whitespace to be ignored, while the quotes themselves are removed.
117 + // Quotes may begin and end inside arguments; they don't necessarily just surround whole arguments.
118 + // Escaped quotes and escaped backslashes also need to be unescaped by this process.
119 +
120 + // Collects the final list of arguments to be returned.
121 + var argsList = new List<string>();
122 +
123 + // True if we are inside an unescaped quote, meaning whitespace should be ignored.
124 + var insideQuote = false;
125 +
126 + // Index of the start of the current argument substring; either the start of the argument
127 + // or the start of a quoted or unquoted sequence within it.
128 + var partStart = 0;
129 +
130 + // The current argument string being built; when completed it will be added to the list.
131 + var arg = new StringBuilder();
132 +
133 + for (int i = 0; i <= arguments.Length; i++)
134 + {
135 + if (i == arguments.Length || (Char.IsWhiteSpace(arguments[i]) && !insideQuote))
136 + {
137 + // Reached a whitespace separator or the end of the string.
138 +
139 + // Finish building the current argument.
140 + arg.Append(arguments.Substring(partStart, i - partStart));
141 +
142 + // Skip over the whitespace character.
143 + partStart = i + 1;
144 +
145 + // Add the argument to the list if it's not empty.
146 + if (arg.Length > 0)
147 + {
148 + argsList.Add(CommandLineArguments.ExpandEnvironmentVariables(arg.ToString()));
149 + arg.Length = 0;
150 + }
151 + }
152 + else if (i > partStart && arguments[i - 1] == '\\')
153 + {
154 + // Check the character following an unprocessed backslash.
155 + // Unescape quotes, and backslashes followed by a quote.
156 + if (arguments[i] == '"' || (arguments[i] == '\\' && arguments.Length > i + 1 && arguments[i + 1] == '"'))
157 + {
158 + // Unescape the quote or backslash by skipping the preceeding backslash.
159 + arg.Append(arguments.Substring(partStart, i - 1 - partStart));
160 + arg.Append(arguments[i]);
161 + partStart = i + 1;
162 + }
163 + }
164 + else if (arguments[i] == '"')
165 + {
166 + // Add the quoted or unquoted section to the argument string.
167 + arg.Append(arguments.Substring(partStart, i - partStart));
168 +
169 + // And skip over the quote character.
170 + partStart = i + 1;
171 +
172 + insideQuote = !insideQuote;
173 + }
174 + }
175 +
176 + return argsList;
177 + }
178 +
179 + private static string ExpandEnvironmentVariables(string arguments)
180 + {
181 + var id = Environment.GetEnvironmentVariables();
182 +
183 + var regex = new Regex("(?<=\\%)(?:[\\w\\.]+)(?=\\%)");
184 + MatchCollection matches = regex.Matches(arguments);
185 +
186 + string value = String.Empty;
187 + for (int i = 0; i <= (matches.Count - 1); i++)
188 + {
189 + try
190 + {
191 + var key = matches[i].Value;
192 + regex = new Regex(String.Concat("(?i)(?:\\%)(?:", key, ")(?:\\%)"));
193 + value = id[key].ToString();
194 + arguments = regex.Replace(arguments, value);
195 + }
196 + catch (NullReferenceException)
197 + {
198 + // Collapse unresolved environment variables.
199 + arguments = regex.Replace(arguments, value);
200 + }
201 + }
202 +
203 + return arguments;
204 + }
205 +
206 + private static bool IsSwitchAt(string[] args, int index)
207 + {
208 + return args.Length > index && !String.IsNullOrEmpty(args[index]) && ('/' == args[index][0] || '-' == args[index][0]);
209 + }
210 + }
211 +}
src/WixToolset.Core/CommandLine/CommandLineContext.cs
+1 -3
@@ -18,8 +18,6 @@ namespace WixToolset.Core.CommandLine
18
19 public IExtensionManager ExtensionManager { get; set; }
20
21 - public string Arguments { get; set; }
22 -
23 - public string[] ParsedArguments { get; set; }
21 + public ICommandLineArguments Arguments { get; set; }
22 }
23 }
src/WixToolset.Core/CommandLine/CommandLineHelper.cs deleted
-216
@@ -1,216 +0,0 @@
1 -// 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.
2 -
3 -namespace WixToolset.Core.CommandLine
4 -{
5 - using System;
6 - using System.IO;
7 - using WixToolset.Data;
8 - using WixToolset.Extensibility.Services;
9 -
10 - public class CommandLineHelper
11 - {
12 - /// <summary>
13 - /// Validates that a string is a valid directory name, and throws appropriate warnings/errors if not
14 - /// </summary>
15 - /// <param name="commandlineSwitch">The commandline switch we're parsing (for error display purposes).</param>
16 - /// <param name="messageHandler">The messagehandler to report warnings/errors to.</param>
17 - /// <param name="args">The list of strings to check.</param>
18 - /// <param name="index">The index (in args) of the commandline parameter to be parsed.</param>
19 - /// <returns>The string if it is valid, null if it is invalid.</returns>
20 - public static string GetDirectory(string commandlineSwitch, IMessaging messageHandler, string[] args, int index)
21 - {
22 - return GetDirectory(commandlineSwitch, messageHandler, args, index, false);
23 - }
24 -
25 - /// <summary>
26 - /// Validates that a string is a valid directory name, and throws appropriate warnings/errors if not
27 - /// </summary>
28 - /// <param name="commandlineSwitch">The commandline switch we're parsing (for error display purposes).</param>
29 - /// <param name="messageHandler">The messagehandler to report warnings/errors to.</param>
30 - /// <param name="args">The list of strings to check.</param>
31 - /// <param name="index">The index (in args) of the commandline parameter to be parsed.</param>
32 - /// <param name="allowPrefix">Indicates if a colon-delimited prefix is allowed.</param>
33 - /// <returns>The string if it is valid, null if it is invalid.</returns>
34 - public static string GetDirectory(string commandlineSwitch, IMessaging messageHandler, string[] args, int index, bool allowPrefix)
35 - {
36 - commandlineSwitch = String.Concat("-", commandlineSwitch);
37 -
38 - if (!IsValidArg(args, index))
39 - {
40 - messageHandler.Write(ErrorMessages.DirectoryPathRequired(commandlineSwitch));
41 - return null;
42 - }
43 -
44 - if (File.Exists(args[index]))
45 - {
46 - messageHandler.Write(ErrorMessages.ExpectedDirectoryGotFile(commandlineSwitch, args[index]));
47 - return null;
48 - }
49 -
50 - return VerifyPath(messageHandler, args[index], allowPrefix);
51 - }
52 -
53 - /// <summary>
54 - /// Validates that a string is a valid filename, and throws appropriate warnings/errors if not
55 - /// </summary>
56 - /// <param name="commandlineSwitch">The commandline switch we're parsing (for error display purposes).</param>
57 - /// <param name="messageHandler">The messagehandler to report warnings/errors to.</param>
58 - /// <param name="args">The list of strings to check.</param>
59 - /// <param name="index">The index (in args) of the commandline parameter to be parsed.</param>
60 - /// <returns>The string if it is valid, null if it is invalid.</returns>
61 - public static string GetFile(string commandlineSwitch, IMessaging messageHandler, string[] args, int index)
62 - {
63 - commandlineSwitch = String.Concat("-", commandlineSwitch);
64 -
65 - if (!IsValidArg(args, index))
66 - {
67 - messageHandler.Write(ErrorMessages.FilePathRequired(commandlineSwitch));
68 - return null;
69 - }
70 -
71 - if (Directory.Exists(args[index]))
72 - {
73 - messageHandler.Write(ErrorMessages.ExpectedFileGotDirectory(commandlineSwitch, args[index]));
74 - return null;
75 - }
76 -
77 - return VerifyPath(messageHandler, args[index]);
78 - }
79 -
80 - /// <summary>
81 - /// Get a set of files that possibly have a search pattern in the path (such as '*').
82 - /// </summary>
83 - /// <param name="searchPath">Search path to find files in.</param>
84 - /// <param name="fileType">Type of file; typically "Source".</param>
85 - /// <returns>An array of files matching the search path.</returns>
86 - /// <remarks>
87 - /// This method is written in this verbose way because it needs to support ".." in the path.
88 - /// It needs the directory path isolated from the file name in order to use Directory.GetFiles
89 - /// or DirectoryInfo.GetFiles. The only way to get this directory path is manually since
90 - /// Path.GetDirectoryName does not support ".." in the path.
91 - /// </remarks>
92 - /// <exception cref="WixFileNotFoundException">Throws WixFileNotFoundException if no file matching the pattern can be found.</exception>
93 - public static string[] GetFiles(string searchPath, string fileType)
94 - {
95 - if (null == searchPath)
96 - {
97 - throw new ArgumentNullException(nameof(searchPath));
98 - }
99 -
100 - // Convert alternate directory separators to the standard one.
101 - string filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
102 - int lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar);
103 - string[] files = null;
104 -
105 - try
106 - {
107 - if (0 > lastSeparator)
108 - {
109 - files = Directory.GetFiles(".", filePath);
110 - }
111 - else // found directory separator
112 - {
113 - files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), filePath.Substring(lastSeparator + 1));
114 - }
115 - }
116 - catch (DirectoryNotFoundException)
117 - {
118 - // Don't let this function throw the DirectoryNotFoundException. This exception
119 - // occurs for non-existant directories and invalid characters in the searchPattern.
120 - }
121 - catch (ArgumentException)
122 - {
123 - // Don't let this function throw the ArgumentException. This exception
124 - // occurs in certain situations such as when passing a malformed UNC path.
125 - }
126 - catch (IOException)
127 - {
128 - throw new WixFileNotFoundException(searchPath, fileType);
129 - }
130 -
131 - if (null == files || 0 == files.Length)
132 - {
133 - throw new WixFileNotFoundException(searchPath, fileType);
134 - }
135 -
136 - return files;
137 - }
138 -
139 - /// <summary>
140 - /// Validates that a valid string parameter (without "/" or "-"), and returns a bool indicating its validity
141 - /// </summary>
142 - /// <param name="args">The list of strings to check.</param>
143 - /// <param name="index">The index (in args) of the commandline parameter to be validated.</param>
144 - /// <returns>True if a valid string parameter exists there, false if not.</returns>
145 - public static bool IsValidArg(string[] args, int index)
146 - {
147 - if (args.Length <= index || String.IsNullOrEmpty(args[index]) || '/' == args[index][0] || '-' == args[index][0])
148 - {
149 - return false;
150 - }
151 - else
152 - {
153 - return true;
154 - }
155 - }
156 -
157 - /// <summary>
158 - /// Validates that a commandline parameter is a valid file or directory name, and throws appropriate warnings/errors if not
159 - /// </summary>
160 - /// <param name="messageHandler">The messagehandler to report warnings/errors to.</param>
161 - /// <param name="path">The path to test.</param>
162 - /// <returns>The string if it is valid, null if it is invalid.</returns>
163 - public static string VerifyPath(IMessaging messageHandler, string path)
164 - {
165 - return VerifyPath(messageHandler, path, false);
166 - }
167 -
168 - /// <summary>
169 - /// Validates that a commandline parameter is a valid file or directory name, and throws appropriate warnings/errors if not
170 - /// </summary>
171 - /// <param name="messageHandler">The messagehandler to report warnings/errors to.</param>
172 - /// <param name="path">The path to test.</param>
173 - /// <param name="allowPrefix">Indicates if a colon-delimited prefix is allowed.</param>
174 - /// <returns>The full path if it is valid, null if it is invalid.</returns>
175 - public static string VerifyPath(IMessaging messageHandler, string path, bool allowPrefix)
176 - {
177 - string fullPath;
178 -
179 - if (0 <= path.IndexOf('\"'))
180 - {
181 - messageHandler.Write(ErrorMessages.PathCannotContainQuote(path));
182 - return null;
183 - }
184 -
185 - try
186 - {
187 - string prefix = null;
188 - if (allowPrefix)
189 - {
190 - int prefixLength = path.IndexOf('=') + 1;
191 - if (0 != prefixLength)
192 - {
193 - prefix = path.Substring(0, prefixLength);
194 - path = path.Substring(prefixLength);
195 - }
196 - }
197 -
198 - if (String.IsNullOrEmpty(prefix))
199 - {
200 - fullPath = Path.GetFullPath(path);
201 - }
202 - else
203 - {
204 - fullPath = String.Concat(prefix, Path.GetFullPath(path));
205 - }
206 - }
207 - catch (Exception e)
208 - {
209 - messageHandler.Write(ErrorMessages.InvalidCommandLineFileName(path, e.Message));
210 - return null;
211 - }
212 -
213 - return fullPath;
214 - }
215 - }
216 -}
src/WixToolset.Core/CommandLine/CommandLineParser.cs
+165 -428
@@ -5,9 +5,6 @@ namespace WixToolset.Core.CommandLine
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 - using System.Linq;
9 - using System.Text;
10 - using System.Text.RegularExpressions;
8 using WixToolset.Data;
9 using WixToolset.Extensibility;
10 using WixToolset.Extensibility.Services;
@@ -22,7 +19,7 @@ namespace WixToolset.Core.CommandLine
19 Bind,
20 }
21
25 - internal class CommandLineParser : ICommandLine, IParseCommandLine
22 + internal class CommandLineParser : ICommandLine
23 {
24 private IServiceProvider ServiceProvider { get; set; }
25
@@ -32,14 +29,8 @@ namespace WixToolset.Core.CommandLine
29
30 public string ActiveCommand { get; private set; }
31
35 - public string[] OriginalArguments { get; private set; }
36 -
37 - public Queue<string> RemainingArguments { get; } = new Queue<string>();
38 -
32 public IExtensionManager ExtensionManager { get; private set; }
33
41 - public string ErrorArgument { get; set; }
42 -
34 public bool ShowHelp { get; set; }
35
36 public ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context)
@@ -50,18 +41,6 @@ namespace WixToolset.Core.CommandLine
41
42 this.ExtensionManager = context.ExtensionManager ?? this.ServiceProvider.GetService<IExtensionManager>();
43
53 - var args = context.ParsedArguments ?? Array.Empty<string>();
54 -
55 - if (!String.IsNullOrEmpty(context.Arguments))
56 - {
57 - args = CommandLineParser.ParseArgumentsToArray(context.Arguments).Concat(args).ToArray();
58 - }
59 -
60 - return this.ParseStandardCommandLine(context, args);
61 - }
62 -
63 - private ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context, string[] args)
64 - {
44 var next = String.Empty;
45
46 var command = Commands.Unknown;
@@ -89,99 +68,99 @@ namespace WixToolset.Core.CommandLine
68 var outputsFile = String.Empty;
69 var builtOutputsFile = String.Empty;
70
92 - this.Parse(context, args, (cmdline, arg) => Enum.TryParse(arg, true, out command), (cmdline, arg) =>
71 + this.Parse(context, (cmdline, arg) => Enum.TryParse(arg, true, out command), (cmdline, parser, arg) =>
72 {
94 - if (cmdline.IsSwitch(arg))
73 + if (parser.IsSwitch(arg))
74 {
75 var parameter = arg.Substring(1);
76 switch (parameter.ToLowerInvariant())
77 {
99 - case "?":
100 - case "h":
101 - case "help":
102 - cmdline.ShowHelp = true;
103 - return true;
104 -
105 - case "bindfiles":
106 - bindFiles = true;
107 - return true;
108 -
109 - case "bindpath":
110 - cmdline.GetNextArgumentOrError(bindPaths);
111 - return true;
112 -
113 - case "cc":
114 - cmdline.GetNextArgumentOrError(ref cabCachePath);
115 - return true;
116 -
117 - case "culture":
118 - cmdline.GetNextArgumentOrError(cultures);
119 - return true;
120 - case "contentsfile":
121 - cmdline.GetNextArgumentOrError(ref contentsFile);
122 - return true;
123 - case "outputsfile":
124 - cmdline.GetNextArgumentOrError(ref outputsFile);
125 - return true;
126 - case "builtoutputsfile":
127 - cmdline.GetNextArgumentOrError(ref builtOutputsFile);
128 - return true;
129 -
130 - case "d":
131 - case "define":
132 - cmdline.GetNextArgumentOrError(defines);
133 - return true;
134 -
135 - case "i":
136 - case "includepath":
137 - cmdline.GetNextArgumentOrError(includePaths);
138 - return true;
139 -
140 - case "intermediatefolder":
141 - cmdline.GetNextArgumentOrError(ref intermediateFolder);
142 - return true;
143 -
144 - case "loc":
145 - cmdline.GetNextArgumentAsFilePathOrError(locFiles, "localization files");
146 - return true;
147 -
148 - case "lib":
149 - cmdline.GetNextArgumentAsFilePathOrError(libraryFiles, "library files");
150 - return true;
151 -
152 - case "o":
153 - case "out":
154 - cmdline.GetNextArgumentOrError(ref outputFile);
155 - return true;
156 -
157 - case "outputtype":
158 - cmdline.GetNextArgumentOrError(ref outputType);
159 - return true;
160 -
161 - case "nologo":
162 - showLogo = false;
163 - return true;
164 -
165 - case "v":
166 - case "verbose":
167 - verbose = true;
168 - return true;
169 -
170 - case "version":
171 - case "-version":
172 - showVersion = true;
173 - return true;
174 -
175 - case "sval":
176 - // todo: implement
177 - return true;
78 + case "?":
79 + case "h":
80 + case "help":
81 + cmdline.ShowHelp = true;
82 + return true;
83 +
84 + case "bindfiles":
85 + bindFiles = true;
86 + return true;
87 +
88 + case "bindpath":
89 + parser.GetNextArgumentOrError(arg, bindPaths);
90 + return true;
91 +
92 + case "cc":
93 + cabCachePath = parser.GetNextArgumentOrError(arg);
94 + return true;
95 +
96 + case "culture":
97 + parser.GetNextArgumentOrError(arg, cultures);
98 + return true;
99 + case "contentsfile":
100 + contentsFile = parser.GetNextArgumentAsFilePathOrError(arg);
101 + return true;
102 + case "outputsfile":
103 + outputsFile = parser.GetNextArgumentAsFilePathOrError(arg);
104 + return true;
105 + case "builtoutputsfile":
106 + builtOutputsFile = parser.GetNextArgumentAsFilePathOrError(arg);
107 + return true;
108 +
109 + case "d":
110 + case "define":
111 + parser.GetNextArgumentOrError(arg, defines);
112 + return true;
113 +
114 + case "i":
115 + case "includepath":
116 + parser.GetNextArgumentOrError(arg, includePaths);
117 + return true;
118 +
119 + case "intermediatefolder":
120 + intermediateFolder = parser.GetNextArgumentAsDirectoryOrError(arg);
121 + return true;
122 +
123 + case "loc":
124 + parser.GetNextArgumentAsFilePathOrError(arg, "localization files", locFiles);
125 + return true;
126 +
127 + case "lib":
128 + parser.GetNextArgumentAsFilePathOrError(arg, "library files", libraryFiles);
129 + return true;
130 +
131 + case "o":
132 + case "out":
133 + outputFile = parser.GetNextArgumentAsFilePathOrError(arg);
134 + return true;
135 +
136 + case "outputtype":
137 + outputType= parser.GetNextArgumentOrError(arg);
138 + return true;
139 +
140 + case "nologo":
141 + showLogo = false;
142 + return true;
143 +
144 + case "v":
145 + case "verbose":
146 + verbose = true;
147 + return true;
148 +
149 + case "version":
150 + case "-version":
151 + showVersion = true;
152 + return true;
153 +
154 + case "sval":
155 + // todo: implement
156 + return true;
157 }
158
159 return false;
160 }
161 else
162 {
184 - files.AddRange(CommandLineHelper.GetFiles(arg, "source code"));
163 + parser.GetArgumentAsFilePathOrError(arg, "source code", files);
164 return true;
165 }
166 });
@@ -205,22 +184,22 @@ namespace WixToolset.Core.CommandLine
184
185 switch (command)
186 {
208 - case Commands.Build:
209 - {
210 - var sourceFiles = GatherSourceFiles(files, outputFolder);
211 - var variables = this.GatherPreprocessorVariables(defines);
212 - var bindPathList = this.GatherBindPaths(bindPaths);
213 - var filterCultures = CalculateFilterCultures(cultures);
214 - var type = CalculateOutputType(outputType, outputFile);
215 - return new BuildCommand(this.ServiceProvider, sourceFiles, variables, locFiles, libraryFiles, filterCultures, outputFile, type, cabCachePath, bindFiles, bindPathList, includePaths, intermediateFolder, contentsFile, outputsFile, builtOutputsFile);
216 - }
187 + case Commands.Build:
188 + {
189 + var sourceFiles = GatherSourceFiles(files, outputFolder);
190 + var variables = this.GatherPreprocessorVariables(defines);
191 + var bindPathList = this.GatherBindPaths(bindPaths);
192 + var filterCultures = CalculateFilterCultures(cultures);
193 + var type = CalculateOutputType(outputType, outputFile);
194 + return new BuildCommand(this.ServiceProvider, sourceFiles, variables, locFiles, libraryFiles, filterCultures, outputFile, type, cabCachePath, bindFiles, bindPathList, includePaths, intermediateFolder, contentsFile, outputsFile, builtOutputsFile);
195 + }
196
218 - case Commands.Compile:
219 - {
220 - var sourceFiles = GatherSourceFiles(files, outputFolder);
221 - var variables = GatherPreprocessorVariables(defines);
222 - return new CompileCommand(this.ServiceProvider, sourceFiles, variables);
223 - }
197 + case Commands.Compile:
198 + {
199 + var sourceFiles = GatherSourceFiles(files, outputFolder);
200 + var variables = GatherPreprocessorVariables(defines);
201 + return new CompileCommand(this.ServiceProvider, sourceFiles, variables);
202 + }
203 }
204
205 return null;
@@ -262,63 +241,87 @@ namespace WixToolset.Core.CommandLine
241
242 switch (outputType.ToLowerInvariant())
243 {
265 - case "bundle":
266 - case ".exe":
267 - return OutputType.Bundle;
244 + case "bundle":
245 + case ".exe":
246 + return OutputType.Bundle;
247
269 - case "library":
270 - case ".wixlib":
271 - return OutputType.Library;
248 + case "library":
249 + case ".wixlib":
250 + return OutputType.Library;
251
273 - case "module":
274 - case ".msm":
275 - return OutputType.Module;
252 + case "module":
253 + case ".msm":
254 + return OutputType.Module;
255
277 - case "patch":
278 - case ".msp":
279 - return OutputType.Patch;
256 + case "patch":
257 + case ".msp":
258 + return OutputType.Patch;
259
281 - case ".pcp":
282 - return OutputType.PatchCreation;
260 + case ".pcp":
261 + return OutputType.PatchCreation;
262
284 - case "product":
285 - case "package":
286 - case ".msi":
287 - return OutputType.Product;
263 + case "product":
264 + case "package":
265 + case ".msi":
266 + return OutputType.Product;
267
289 - case "transform":
290 - case ".mst":
291 - return OutputType.Transform;
268 + case "transform":
269 + case ".mst":
270 + return OutputType.Transform;
271
293 - case "intermediatepostlink":
294 - case ".wixipl":
295 - return OutputType.IntermediatePostLink;
272 + case "intermediatepostlink":
273 + case ".wixipl":
274 + return OutputType.IntermediatePostLink;
275 }
276
277 return OutputType.Unknown;
278 }
279
301 -#if UNUSED
302 - private static CommandLine Parse(string commandLineString, Func<CommandLine, string, bool> parseArgument)
280 + private ICommandLine Parse(ICommandLineContext context, Func<CommandLineParser, string, bool> parseCommand, Func<CommandLineParser, IParseCommandLine, string, bool> parseArgument)
281 {
304 - var arguments = CommandLine.ParseArgumentsToArray(commandLineString).ToArray();
305 -
306 - return CommandLine.Parse(arguments, null, parseArgument);
307 - }
282 + var extensions = this.ExtensionManager.Create<IExtensionCommandLine>();
283
309 - private static CommandLine Parse(string[] commandLineArguments, Func<CommandLine, string, bool> parseArgument)
310 - {
311 - return CommandLine.Parse(commandLineArguments, null, parseArgument);
312 - }
313 -#endif
284 + foreach (var extension in extensions)
285 + {
286 + extension.PreParse(context);
287 + }
288
315 - private ICommandLine Parse(ICommandLineContext context, string[] commandLineArguments, Func<CommandLineParser, string, bool> parseCommand, Func<CommandLineParser, string, bool> parseArgument)
316 - {
317 - this.FlattenArgumentsWithResponseFilesIntoOriginalArguments(commandLineArguments);
289 + var parser = context.Arguments.Parse();
290
319 - this.QueueArgumentsAndLoadExtensions(this.OriginalArguments);
291 + while (!this.ShowHelp &&
292 + String.IsNullOrEmpty(parser.ErrorArgument) &&
293 + parser.TryGetNextSwitchOrArgument(out var arg))
294 + {
295 + if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments.
296 + {
297 + continue;
298 + }
299
321 - this.ProcessRemainingArguments(context, parseArgument, parseCommand);
300 + if (parser.IsSwitch(arg))
301 + {
302 + if (!parseArgument(this, parser, arg) &&
303 + !this.TryParseCommandLineArgumentWithExtension(arg, parser, extensions))
304 + {
305 + parser.ErrorArgument = arg;
306 + }
307 + }
308 + else if (String.IsNullOrEmpty(this.ActiveCommand) && parseCommand != null) // First non-switch must be the command, if commands are supported.
309 + {
310 + if (parseCommand(this, arg))
311 + {
312 + this.ActiveCommand = arg;
313 + }
314 + else
315 + {
316 + parser.ErrorArgument = arg;
317 + }
318 + }
319 + else if (!this.TryParseCommandLineArgumentWithExtension(arg, parser, extensions) &&
320 + !parseArgument(this, parser, arg))
321 + {
322 + parser.ErrorArgument = arg;
323 + }
324 + }
325
326 return this;
327 }
@@ -358,7 +361,7 @@ namespace WixToolset.Core.CommandLine
361 return variables;
362 }
363
361 - private IEnumerable<BindPath> GatherBindPaths(IEnumerable<string> bindPaths)
364 + private IEnumerable<BindPath> GatherBindPaths(IEnumerable<string> bindPaths)
365 {
366 var result = new List<BindPath>();
367
@@ -379,172 +382,11 @@ namespace WixToolset.Core.CommandLine
382 return result;
383 }
384
382 - /// <summary>
383 - /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity
384 - /// </summary>
385 - /// <param name="args">The list of strings to check.</param>
386 - /// <param name="index">The index (in args) of the commandline parameter to be validated.</param>
387 - /// <returns>True if a valid switch exists there, false if not.</returns>
388 - public bool IsSwitch(string arg)
389 - {
390 - return arg != null && arg.Length > 1 && ('/' == arg[0] || '-' == arg[0]);
391 - }
392 -
393 - /// <summary>
394 - /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity
395 - /// </summary>
396 - /// <param name="args">The list of strings to check.</param>
397 - /// <param name="index">The index (in args) of the commandline parameter to be validated.</param>
398 - /// <returns>True if a valid switch exists there, false if not.</returns>
399 - public bool IsSwitchAt(IEnumerable<string> args, int index)
400 - {
401 - var arg = args.ElementAtOrDefault(index);
402 - return IsSwitch(arg);
403 - }
404 -
405 - public void GetNextArgumentOrError(ref string arg)
406 - {
407 - this.TryGetNextArgumentOrError(out arg);
408 - }
409 -
410 - public void GetNextArgumentOrError(IList<string> args)
411 - {
412 - if (this.TryGetNextArgumentOrError(out var arg))
413 - {
414 - args.Add(arg);
415 - }
416 - }
417 -
418 - public void GetNextArgumentAsFilePathOrError(IList<string> args, string fileType)
419 - {
420 - if (this.TryGetNextArgumentOrError(out var arg))
421 - {
422 - foreach (var path in CommandLineHelper.GetFiles(arg, fileType))
423 - {
424 - args.Add(path);
425 - }
426 - }
427 - }
428 -
429 - public bool TryGetNextArgumentOrError(out string arg)
430 - {
431 - if (TryDequeue(this.RemainingArguments, out arg) && !this.IsSwitch(arg))
432 - {
433 - return true;
434 - }
435 -
436 - this.ErrorArgument = arg ?? CommandLineParser.ExpectedArgument;
437 -
438 - return false;
439 - }
440 -
441 - private static bool TryDequeue(Queue<string> q, out string arg)
442 - {
443 - if (q.Count > 0)
444 - {
445 - arg = q.Dequeue();
446 - return true;
447 - }
448 -
449 - arg = null;
450 - return false;
451 - }
452 -
453 - private void FlattenArgumentsWithResponseFilesIntoOriginalArguments(string[] commandLineArguments)
454 - {
455 - List<string> args = new List<string>();
456 -
457 - foreach (var arg in commandLineArguments)
458 - {
459 - if ('@' == arg[0])
460 - {
461 - var responseFileArguments = CommandLineParser.ParseResponseFile(arg.Substring(1));
462 - args.AddRange(responseFileArguments);
463 - }
464 - else
465 - {
466 - args.Add(arg);
467 - }
468 - }
469 -
470 - this.OriginalArguments = args.ToArray();
471 - }
472 -
473 - private void QueueArgumentsAndLoadExtensions(string[] args)
385 + private bool TryParseCommandLineArgumentWithExtension(string arg, IParseCommandLine parse, IEnumerable<IExtensionCommandLine> extensions)
386 {
475 - for (var i = 0; i < args.Length; ++i)
476 - {
477 - var arg = args[i];
478 -
479 - if ("-ext" == arg || "/ext" == arg)
480 - {
481 - if (!this.IsSwitchAt(args, ++i))
482 - {
483 - this.ExtensionManager.Load(args[i]);
484 - }
485 - else
486 - {
487 - this.ErrorArgument = arg;
488 - break;
489 - }
490 - }
491 - else
492 - {
493 - this.RemainingArguments.Enqueue(arg);
494 - }
495 - }
496 - }
497 -
498 - private void ProcessRemainingArguments(ICommandLineContext context, Func<CommandLineParser, string, bool> parseArgument, Func<CommandLineParser, string, bool> parseCommand)
499 - {
500 - var extensions = this.ExtensionManager.Create<IExtensionCommandLine>();
501 -
387 foreach (var extension in extensions)
388 {
504 - extension.PreParse(context);
505 - }
506 -
507 - while (!this.ShowHelp &&
508 - String.IsNullOrEmpty(this.ErrorArgument) &&
509 - TryDequeue(this.RemainingArguments, out var arg))
510 - {
511 - if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments.
512 - {
513 - continue;
514 - }
515 -
516 - if ('-' == arg[0] || '/' == arg[0])
517 - {
518 - if (!parseArgument(this, arg) &&
519 - !this.TryParseCommandLineArgumentWithExtension(arg, extensions))
520 - {
521 - this.ErrorArgument = arg;
522 - }
523 - }
524 - else if (String.IsNullOrEmpty(this.ActiveCommand) && parseCommand != null) // First non-switch must be the command, if commands are supported.
525 - {
526 - if (parseCommand(this, arg))
527 - {
528 - this.ActiveCommand = arg;
529 - }
530 - else
531 - {
532 - this.ErrorArgument = arg;
533 - }
534 - }
535 - else if (!this.TryParseCommandLineArgumentWithExtension(arg, extensions) &&
536 - !parseArgument(this, arg))
537 - {
538 - this.ErrorArgument = arg;
539 - }
540 - }
541 - }
542 -
543 - private bool TryParseCommandLineArgumentWithExtension(string arg, IEnumerable<IExtensionCommandLine> extensions)
544 - {
545 - foreach (var extension in extensions)
546 - {
547 - if (extension.TryParseArgument(this, arg))
389 + if (extension.TryParseArgument(parse, arg))
390 {
391 return true;
392 }
@@ -552,110 +394,5 @@ namespace WixToolset.Core.CommandLine
394
395 return false;
396 }
555 -
556 - private static List<string> ParseResponseFile(string responseFile)
557 - {
558 - string arguments;
559 -
560 - using (StreamReader reader = new StreamReader(responseFile))
561 - {
562 - arguments = reader.ReadToEnd();
563 - }
564 -
565 - return CommandLineParser.ParseArgumentsToArray(arguments);
566 - }
567 -
568 - private static List<string> ParseArgumentsToArray(string arguments)
569 - {
570 - // Scan and parse the arguments string, dividing up the arguments based on whitespace.
571 - // Unescaped quotes cause whitespace to be ignored, while the quotes themselves are removed.
572 - // Quotes may begin and end inside arguments; they don't necessarily just surround whole arguments.
573 - // Escaped quotes and escaped backslashes also need to be unescaped by this process.
574 -
575 - // Collects the final list of arguments to be returned.
576 - var argsList = new List<string>();
577 -
578 - // True if we are inside an unescaped quote, meaning whitespace should be ignored.
579 - var insideQuote = false;
580 -
581 - // Index of the start of the current argument substring; either the start of the argument
582 - // or the start of a quoted or unquoted sequence within it.
583 - var partStart = 0;
584 -
585 - // The current argument string being built; when completed it will be added to the list.
586 - var arg = new StringBuilder();
587 -
588 - for (int i = 0; i <= arguments.Length; i++)
589 - {
590 - if (i == arguments.Length || (Char.IsWhiteSpace(arguments[i]) && !insideQuote))
591 - {
592 - // Reached a whitespace separator or the end of the string.
593 -
594 - // Finish building the current argument.
595 - arg.Append(arguments.Substring(partStart, i - partStart));
596 -
597 - // Skip over the whitespace character.
598 - partStart = i + 1;
599 -
600 - // Add the argument to the list if it's not empty.
601 - if (arg.Length > 0)
602 - {
603 - argsList.Add(CommandLineParser.ExpandEnvironmentVariables(arg.ToString()));
604 - arg.Length = 0;
605 - }
606 - }
607 - else if (i > partStart && arguments[i - 1] == '\\')
608 - {
609 - // Check the character following an unprocessed backslash.
610 - // Unescape quotes, and backslashes followed by a quote.
611 - if (arguments[i] == '"' || (arguments[i] == '\\' && arguments.Length > i + 1 && arguments[i + 1] == '"'))
612 - {
613 - // Unescape the quote or backslash by skipping the preceeding backslash.
614 - arg.Append(arguments.Substring(partStart, i - 1 - partStart));
615 - arg.Append(arguments[i]);
616 - partStart = i + 1;
617 - }
618 - }
619 - else if (arguments[i] == '"')
620 - {
621 - // Add the quoted or unquoted section to the argument string.
622 - arg.Append(arguments.Substring(partStart, i - partStart));
623 -
624 - // And skip over the quote character.
625 - partStart = i + 1;
626 -
627 - insideQuote = !insideQuote;
628 - }
629 - }
630 -
631 - return argsList;
632 - }
633 -
634 - private static string ExpandEnvironmentVariables(string arguments)
635 - {
636 - var id = Environment.GetEnvironmentVariables();
637 -
638 - var regex = new Regex("(?<=\\%)(?:[\\w\\.]+)(?=\\%)");
639 - MatchCollection matches = regex.Matches(arguments);
640 -
641 - string value = String.Empty;
642 - for (int i = 0; i <= (matches.Count - 1); i++)
643 - {
644 - try
645 - {
646 - var key = matches[i].Value;
647 - regex = new Regex(String.Concat("(?i)(?:\\%)(?:", key, ")(?:\\%)"));
648 - value = id[key].ToString();
649 - arguments = regex.Replace(arguments, value);
650 - }
651 - catch (NullReferenceException)
652 - {
653 - // Collapse unresolved environment variables.
654 - arguments = regex.Replace(arguments, value);
655 - }
656 - }
657 -
658 - return arguments;
659 - }
397 }
398 }
src/WixToolset.Core/CommandLine/ParseCommandLine.cs new
+257
@@ -0,0 +1,257 @@
1 +// 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.
2 +
3 +namespace WixToolset.Core.CommandLine
4 +{
5 + using System;
6 + using System.Collections.Generic;
7 + using System.IO;
8 + using WixToolset.Data;
9 + using WixToolset.Extensibility.Services;
10 +
11 + internal class ParseCommandLine : IParseCommandLine
12 + {
13 + private const string ExpectedArgument = "expected argument";
14 +
15 + public string ErrorArgument { get; set; }
16 +
17 + private Queue<string> RemainingArguments { get; }
18 +
19 + private IMessaging Messaging { get; }
20 +
21 + public ParseCommandLine(IMessaging messaging, string[] arguments, string errorArgument)
22 + {
23 + this.Messaging = messaging;
24 + this.RemainingArguments = new Queue<string>(arguments);
25 + this.ErrorArgument = errorArgument;
26 + }
27 +
28 + public bool IsSwitch(string arg) => !String.IsNullOrEmpty(arg) && ('/' == arg[0] || '-' == arg[0]);
29 +
30 + public void GetArgumentAsFilePathOrError(string argument, string fileType, IList<string> paths)
31 + {
32 + foreach (var path in GetFiles(argument, fileType))
33 + {
34 + paths.Add(path);
35 + }
36 + }
37 +
38 + public string GetNextArgumentOrError(string commandLineSwitch)
39 + {
40 + if (this.TryGetNextNonSwitchArgumentOrError(out var argument))
41 + {
42 + return argument;
43 + }
44 +
45 + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
46 + return null;
47 + }
48 +
49 + public bool GetNextArgumentOrError(string commandLineSwitch, IList<string> args)
50 + {
51 + if (this.TryGetNextNonSwitchArgumentOrError(out var arg))
52 + {
53 + args.Add(arg);
54 + return true;
55 + }
56 +
57 + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
58 + return false;
59 + }
60 +
61 + public string GetNextArgumentAsDirectoryOrError(string commandLineSwitch)
62 + {
63 + if (this.TryGetNextNonSwitchArgumentOrError(out var arg) && TryGetDirectory(commandLineSwitch, this.Messaging, arg, out var directory))
64 + {
65 + return directory;
66 + }
67 +
68 + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
69 + return null;
70 + }
71 +
72 + public bool GetNextArgumentAsDirectoryOrError(string commandLineSwitch, IList<string> directories)
73 + {
74 + if (this.TryGetNextNonSwitchArgumentOrError(out var arg) && TryGetDirectory(commandLineSwitch, this.Messaging, arg, out var directory))
75 + {
76 + directories.Add(directory);
77 + return true;
78 + }
79 +
80 + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
81 + return false;
82 + }
83 +
84 + public string GetNextArgumentAsFilePathOrError(string commandLineSwitch)
85 + {
86 + if (this.TryGetNextNonSwitchArgumentOrError(out var arg) && this.TryGetFile(commandLineSwitch, arg, out var path))
87 + {
88 + return path;
89 + }
90 +
91 + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
92 + return null;
93 + }
94 +
95 + public bool GetNextArgumentAsFilePathOrError(string commandLineSwitch, string fileType, IList<string> paths)
96 + {
97 + if (this.TryGetNextNonSwitchArgumentOrError(out var arg))
98 + {
99 + foreach (var path in GetFiles(arg, fileType))
100 + {
101 + paths.Add(path);
102 + }
103 +
104 + return true;
105 + }
106 +
107 + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
108 + return false;
109 + }
110 +
111 + public bool TryGetNextSwitchOrArgument(out string arg)
112 + {
113 + return TryDequeue(this.RemainingArguments, out arg);
114 + }
115 +
116 + private bool TryGetNextNonSwitchArgumentOrError(out string arg)
117 + {
118 + var result = this.TryGetNextSwitchOrArgument(out arg);
119 +
120 + if (!result && !this.IsSwitch(arg))
121 + {
122 + this.ErrorArgument = arg ?? ParseCommandLine.ExpectedArgument;
123 + }
124 +
125 + return result;
126 + }
127 +
128 + private static bool IsValidArg(string arg) => !(String.IsNullOrEmpty(arg) || '/' == arg[0] || '-' == arg[0]);
129 +
130 + private static bool TryDequeue(Queue<string> q, out string arg)
131 + {
132 + if (q.Count > 0)
133 + {
134 + arg = q.Dequeue();
135 + return true;
136 + }
137 +
138 + arg = null;
139 + return false;
140 + }
141 +
142 + private bool TryGetDirectory(string commandlineSwitch, IMessaging messageHandler, string arg, out string directory)
143 + {
144 + directory = null;
145 +
146 + if (File.Exists(arg))
147 + {
148 + this.Messaging.Write(ErrorMessages.ExpectedDirectoryGotFile(commandlineSwitch, arg));
149 + return false;
150 + }
151 +
152 + directory = this.VerifyPath(arg);
153 + return directory != null;
154 + }
155 +
156 + private bool TryGetFile(string commandlineSwitch, string arg, out string path)
157 + {
158 + path = null;
159 +
160 + if (!IsValidArg(arg))
161 + {
162 + this.Messaging.Write(ErrorMessages.FilePathRequired(commandlineSwitch));
163 + }
164 + else if (Directory.Exists(arg))
165 + {
166 + this.Messaging.Write(ErrorMessages.ExpectedFileGotDirectory(commandlineSwitch, arg));
167 + }
168 + else
169 + {
170 + path = this.VerifyPath(arg);
171 + }
172 +
173 + return path != null;
174 + }
175 +
176 + /// <summary>
177 + /// Get a set of files that possibly have a search pattern in the path (such as '*').
178 + /// </summary>
179 + /// <param name="searchPath">Search path to find files in.</param>
180 + /// <param name="fileType">Type of file; typically "Source".</param>
181 + /// <returns>An array of files matching the search path.</returns>
182 + /// <remarks>
183 + /// This method is written in this verbose way because it needs to support ".." in the path.
184 + /// It needs the directory path isolated from the file name in order to use Directory.GetFiles
185 + /// or DirectoryInfo.GetFiles. The only way to get this directory path is manually since
186 + /// Path.GetDirectoryName does not support ".." in the path.
187 + /// </remarks>
188 + /// <exception cref="WixFileNotFoundException">Throws WixFileNotFoundException if no file matching the pattern can be found.</exception>
189 + private string[] GetFiles(string searchPath, string fileType)
190 + {
191 + if (null == searchPath)
192 + {
193 + throw new ArgumentNullException(nameof(searchPath));
194 + }
195 +
196 + // Convert alternate directory separators to the standard one.
197 + string filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
198 + int lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar);
199 + var files = new string[0];
200 +
201 + try
202 + {
203 + if (0 > lastSeparator)
204 + {
205 + files = Directory.GetFiles(".", filePath);
206 + }
207 + else // found directory separator
208 + {
209 + files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), filePath.Substring(lastSeparator + 1));
210 + }
211 + }
212 + catch (DirectoryNotFoundException)
213 + {
214 + // Don't let this function throw the DirectoryNotFoundException. This exception
215 + // occurs for non-existant directories and invalid characters in the searchPattern.
216 + }
217 + catch (ArgumentException)
218 + {
219 + // Don't let this function throw the ArgumentException. This exception
220 + // occurs in certain situations such as when passing a malformed UNC path.
221 + }
222 + catch (IOException)
223 + {
224 + }
225 +
226 + if (0 == files.Length)
227 + {
228 + this.Messaging.Write(ErrorMessages.FileNotFound(null, searchPath, fileType));
229 + }
230 +
231 + return files;
232 + }
233 +
234 + private string VerifyPath(string path)
235 + {
236 + string fullPath;
237 +
238 + if (0 <= path.IndexOf('\"'))
239 + {
240 + this.Messaging.Write(ErrorMessages.PathCannotContainQuote(path));
241 + return null;
242 + }
243 +
244 + try
245 + {
246 + fullPath = Path.GetFullPath(path);
247 + }
248 + catch (Exception e)
249 + {
250 + this.Messaging.Write(ErrorMessages.InvalidCommandLineFileName(path, e.Message));
251 + return null;
252 + }
253 +
254 + return fullPath;
255 + }
256 + }
257 +}
src/WixToolset.Core/Preprocessor.cs
+1 -1
@@ -644,7 +644,7 @@ namespace WixToolset.Core
644
645 if (null == includeFile)
646 {
647 - throw new WixException(ErrorMessages.FileNotFound(sourceLineNumbers, includePath, "include"));
647 + throw new WixFileNotFoundException(sourceLineNumbers, includePath, "include");
648 }
649
650 using (XmlReader reader = XmlReader.Create(includeFile, DocumentXmlReaderSettings))
src/WixToolset.Core/WixToolsetServiceProvider.cs
+1
@@ -25,6 +25,7 @@ namespace WixToolset.Core
25 { typeof(IWindowsInstallerBackendHelper), (provider, singletons) => AddSingleton(singletons, typeof(IWindowsInstallerBackendHelper), new WindowsInstallerBackendHelper(provider)) },
26
27 // Transients.
28 + { typeof(ICommandLineArguments), (provider, singletons) => new CommandLineArguments(provider) },
29 { typeof(ICommandLineContext), (provider, singletons) => new CommandLineContext(provider) },
30 { typeof(ICommandLine), (provider, singletons) => new CommandLineParser() },
31 { typeof(IPreprocessContext), (provider, singletons) => new PreprocessContext(provider) },
src/light/LightCommandLine.cs
+66 -130
@@ -6,8 +6,8 @@ namespace WixToolset.Tools
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 - using WixToolset.Core.CommandLine;
9 using WixToolset.Data;
10 + using WixToolset.Extensibility;
11 using WixToolset.Extensibility.Services;
12
13 public class LightCommandLine
@@ -22,7 +22,6 @@ namespace WixToolset.Tools
22 this.SuppressIces = new List<string>();
23 this.Ices = new List<string>();
24 this.BindPaths = new List<BindPath>();
25 - this.Extensions = new List<string>();
25 this.Files = new List<string>();
26 this.LocalizationFiles = new List<string>();
27 this.Variables = new Dictionary<string, string>();
@@ -80,8 +79,6 @@ namespace WixToolset.Tools
79
80 public List<BindPath> BindPaths { get; private set; }
81
83 - public List<string> Extensions { get; private set; }
84 -
82 public List<string> Files { get; private set; }
83
84 public List<string> LocalizationFiles { get; private set; }
@@ -96,35 +93,40 @@ namespace WixToolset.Tools
93 /// Parse the commandline arguments.
94 /// </summary>
95 /// <param name="args">Commandline arguments.</param>
99 - public string[] Parse(string[] args)
96 + public string[] Parse(ICommandLineContext context)
97 {
101 - List<string> unprocessed = new List<string>();
98 + var unprocessed = new List<string>();
99 +
100 + var extensions = context.ExtensionManager.Create<IExtensionCommandLine>();
101 +
102 + foreach (var extension in extensions)
103 + {
104 + extension.PreParse(context);
105 + }
106 +
107 + var parser = context.Arguments.Parse();
108
103 - for (int i = 0; i < args.Length; ++i)
109 + while (!this.ShowHelp &&
110 + String.IsNullOrEmpty(parser.ErrorArgument) &&
111 + parser.TryGetNextSwitchOrArgument(out var arg))
112 {
105 - string arg = args[i];
106 - if (String.IsNullOrEmpty(arg)) // skip blank arguments
113 + if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments.
114 {
115 continue;
116 }
117
111 - if (1 == arg.Length) // treat '-' and '@' as filenames when by themselves.
118 + if (parser.IsSwitch(arg))
119 {
113 - unprocessed.Add(arg);
114 - }
115 - else if ('-' == arg[0] || '/' == arg[0])
116 - {
117 - string parameter = arg.Substring(1);
120 + var parameter = arg.Substring(1);
121 if (parameter.Equals("b", StringComparison.Ordinal))
122 {
120 - if (!CommandLineHelper.IsValidArg(args, ++i))
123 + var result = parser.GetNextArgumentOrError(arg);
124 + if (!String.IsNullOrEmpty(result))
125 {
122 - break;
123 - }
126 + var bindPath = BindPath.Parse(result);
127
125 - var bindPath = BindPath.Parse(args[i]);
126 -
127 - this.BindPaths.Add(bindPath);
128 + this.BindPaths.Add(bindPath);
129 + }
130 }
131 else if (parameter.StartsWith("cultures:", StringComparison.Ordinal))
132 {
@@ -184,25 +186,9 @@ namespace WixToolset.Tools
186 this.Variables.Add(value[0], value[1]);
187 }
188 }
187 - else if (parameter.Equals("ext", StringComparison.Ordinal))
188 - {
189 - if (!CommandLineHelper.IsValidArg(args, ++i))
190 - {
191 - this.Messaging.Write(ErrorMessages.TypeSpecificationForExtensionRequired("-ext"));
192 - break;
193 - }
194 -
195 - this.Extensions.Add(args[i]);
196 - }
189 else if (parameter.Equals("loc", StringComparison.Ordinal))
190 {
199 - string locFile = CommandLineHelper.GetFile(parameter, this.Messaging, args, ++i);
200 - if (String.IsNullOrEmpty(locFile))
201 - {
202 - break;
203 - }
204 -
205 - this.LocalizationFiles.Add(locFile);
191 + parser.GetNextArgumentAsFilePathOrError(arg, "localization files", this.LocalizationFiles);
192 }
193 else if (parameter.Equals("nologo", StringComparison.Ordinal))
194 {
@@ -214,11 +200,7 @@ namespace WixToolset.Tools
200 }
201 else if ("o" == parameter || "out" == parameter)
202 {
217 - this.OutputFile = CommandLineHelper.GetFile(parameter, this.Messaging, args, ++i);
218 - if (String.IsNullOrEmpty(this.OutputFile))
219 - {
220 - break;
221 - }
203 + this.OutputFile = parser.GetNextArgumentAsFilePathOrError(arg);
204 }
205 else if (parameter.Equals("pedantic", StringComparison.Ordinal))
206 {
@@ -230,12 +212,7 @@ namespace WixToolset.Tools
212 }
213 else if (parameter.Equals("usf", StringComparison.Ordinal))
214 {
233 - this.UnreferencedSymbolsFile = CommandLineHelper.GetFile(parameter, this.Messaging, args, ++i);
234 -
235 - if (String.IsNullOrEmpty(this.UnreferencedSymbolsFile))
236 - {
237 - break;
238 - }
215 + this.UnreferencedSymbolsFile = parser.GetNextArgumentAsDirectoryOrError(arg);
216 }
217 else if (parameter.Equals("xo", StringComparison.Ordinal))
218 {
@@ -243,41 +220,27 @@ namespace WixToolset.Tools
220 }
221 else if (parameter.Equals("cc", StringComparison.Ordinal))
222 {
246 - this.CabCachePath = CommandLineHelper.GetDirectory(parameter, this.Messaging, args, ++i);
247 -
248 - if (String.IsNullOrEmpty(this.CabCachePath))
249 - {
250 - break;
251 - }
223 + this.CabCachePath = parser.GetNextArgumentAsDirectoryOrError(arg);
224 }
225 else if (parameter.Equals("ct", StringComparison.Ordinal))
226 {
255 - if (!CommandLineHelper.IsValidArg(args, ++i))
227 + var result = parser.GetNextArgumentOrError(arg);
228 + if (!String.IsNullOrEmpty(result))
229 {
257 - this.Messaging.Write(ErrorMessages.IllegalCabbingThreadCount(String.Empty));
258 - break;
259 - }
230 + if (!Int32.TryParse(result, out var ct) || 0 >= ct)
231 + {
232 + this.Messaging.Write(ErrorMessages.IllegalCabbingThreadCount(result));
233 + parser.ErrorArgument = arg;
234 + break;
235 + }
236
261 - int ct = 0;
262 - if (!Int32.TryParse(args[i], out ct) || 0 >= ct)
263 - {
264 - this.Messaging.Write(ErrorMessages.IllegalCabbingThreadCount(args[i]));
265 - break;
237 + this.CabbingThreadCount = ct;
238 + this.Messaging.Write(VerboseMessages.SetCabbingThreadCount(this.CabbingThreadCount.ToString()));
239 }
267 -
268 - this.CabbingThreadCount = ct;
269 - this.Messaging.Write(VerboseMessages.SetCabbingThreadCount(this.CabbingThreadCount.ToString()));
240 }
241 else if (parameter.Equals("cub", StringComparison.Ordinal))
242 {
273 - string cubeFile = CommandLineHelper.GetFile(parameter, this.Messaging, args, ++i);
274 -
275 - if (String.IsNullOrEmpty(cubeFile))
276 - {
277 - break;
278 - }
279 -
280 - this.CubeFiles.Add(cubeFile);
243 + parser.GetNextArgumentAsFilePathOrError(arg, "static validation files", this.CubeFiles);
244 }
245 else if (parameter.StartsWith("ice:", StringComparison.Ordinal))
246 {
@@ -285,57 +248,27 @@ namespace WixToolset.Tools
248 }
249 else if (parameter.Equals("intermediatefolder", StringComparison.OrdinalIgnoreCase))
250 {
288 - this.IntermediateFolder = CommandLineHelper.GetDirectory(parameter, this.Messaging, args, ++i);
289 -
290 - if (String.IsNullOrEmpty(this.IntermediateFolder))
291 - {
292 - break;
293 - }
251 + this.IntermediateFolder = parser.GetNextArgumentAsDirectoryOrError(arg);
252 }
253 else if (parameter.Equals("contentsfile", StringComparison.Ordinal))
254 {
297 - this.ContentsFile = CommandLineHelper.GetFile(parameter, this.Messaging, args, ++i);
298 -
299 - if (String.IsNullOrEmpty(this.ContentsFile))
300 - {
301 - break;
302 - }
255 + this.ContentsFile = parser.GetNextArgumentAsFilePathOrError(arg);
256 }
257 else if (parameter.Equals("outputsfile", StringComparison.Ordinal))
258 {
306 - this.OutputsFile = CommandLineHelper.GetFile(parameter, this.Messaging, args, ++i);
307 -
308 - if (String.IsNullOrEmpty(this.OutputsFile))
309 - {
310 - break;
311 - }
259 + this.OutputsFile = parser.GetNextArgumentAsFilePathOrError(arg);
260 }
261 else if (parameter.Equals("builtoutputsfile", StringComparison.Ordinal))
262 {
315 - this.BuiltOutputsFile = CommandLineHelper.GetFile(parameter, this.Messaging, args, ++i);
316 -
317 - if (String.IsNullOrEmpty(this.BuiltOutputsFile))
318 - {
319 - break;
320 - }
263 + this.BuiltOutputsFile = parser.GetNextArgumentAsFilePathOrError(arg);
264 }
265 else if (parameter.Equals("wixprojectfile", StringComparison.Ordinal))
266 {
324 - this.WixprojectFile = CommandLineHelper.GetFile(parameter, this.Messaging, args, ++i);
325 -
326 - if (String.IsNullOrEmpty(this.WixprojectFile))
327 - {
328 - break;
329 - }
267 + this.WixprojectFile = parser.GetNextArgumentAsFilePathOrError(arg);
268 }
269 else if (parameter.Equals("pdbout", StringComparison.Ordinal))
270 {
333 - this.PdbFile = CommandLineHelper.GetFile(parameter, this.Messaging, args, ++i);
334 -
335 - if (String.IsNullOrEmpty(this.PdbFile))
336 - {
337 - break;
338 - }
271 + this.PdbFile = parser.GetNextArgumentAsFilePathOrError(arg);
272 }
273 else if (parameter.StartsWith("sice:", StringComparison.Ordinal))
274 {
@@ -410,45 +343,35 @@ namespace WixToolset.Tools
343 this.ShowHelp = true;
344 break;
345 }
413 - else
346 + else if (!this.TryParseCommandLineArgumentWithExtension(arg, parser, extensions))
347 {
348 unprocessed.Add(arg);
349 }
350 }
418 - else if ('@' == arg[0])
419 - {
420 - string[] parsedArgs = CommandLineResponseFile.Parse(arg.Substring(1));
421 - string[] unparsedArgs = this.Parse(parsedArgs);
422 - unprocessed.AddRange(unparsedArgs);
423 - }
424 - else
351 + else if (!this.TryParseCommandLineArgumentWithExtension(arg, parser, extensions))
352 {
353 unprocessed.Add(arg);
354 }
355 }
356
430 - return unprocessed.ToArray();
357 + return this.ParsePostExtensions(parser, unprocessed.ToArray());
358 }
359
433 - public string[] ParsePostExtensions(string[] remaining)
360 + private string[] ParsePostExtensions(IParseCommandLine parser, string[] remaining)
361 {
435 - List<string> unprocessed = new List<string>();
362 + var unprocessed = new List<string>();
363
364 for (int i = 0; i < remaining.Length; ++i)
365 {
439 - string arg = remaining[i];
440 - if (String.IsNullOrEmpty(arg)) // skip blank arguments
441 - {
442 - continue;
443 - }
366 + var arg = remaining[i];
367
445 - if (1 < arg.Length && ('-' == arg[0] || '/' == arg[0]))
368 + if (parser.IsSwitch(arg))
369 {
370 unprocessed.Add(arg);
371 }
372 else
373 {
451 - this.Files.AddRange(CommandLineHelper.GetFiles(arg, "Source"));
374 + parser.GetArgumentAsFilePathOrError(arg, "source files", this.Files);
375 }
376 }
377
@@ -469,7 +392,7 @@ namespace WixToolset.Tools
392 // Add the directories of the input files as unnamed bind paths.
393 foreach (string file in this.Files)
394 {
472 - BindPath bindPath = new BindPath(Path.GetDirectoryName(Path.GetFullPath(file)));
395 + var bindPath = new BindPath(Path.GetDirectoryName(Path.GetFullPath(file)));
396 this.BindPaths.Add(bindPath);
397 }
398 }
@@ -481,5 +404,18 @@ namespace WixToolset.Tools
404
405 return unprocessed.ToArray();
406 }
407 +
408 + private bool TryParseCommandLineArgumentWithExtension(string arg, IParseCommandLine parser, IEnumerable<IExtensionCommandLine> extensions)
409 + {
410 + foreach (var extension in extensions)
411 + {
412 + if (extension.TryParseArgument(parser, arg))
413 + {
414 + return true;
415 + }
416 + }
417 +
418 + return false;
419 + }
420 }
421 }
src/light/light.cs
+14 -35
@@ -22,7 +22,6 @@ namespace WixToolset.Tools
22 public sealed class Light
23 {
24 LightCommandLine commandLine;
25 - private IEnumerable<IExtensionData> extensionData;
25 //private IEnumerable<IBinderExtension> binderExtensions;
26 //private IEnumerable<IBinderFileManager> fileManagers;
27
@@ -101,45 +100,20 @@ namespace WixToolset.Tools
100 /// <param name="args">Command line arguments to be parsed.</param>
101 private IEnumerable<string> ParseCommandLineAndLoadExtensions(IServiceProvider serviceProvider, IMessaging messaging, string[] args)
102 {
104 - this.commandLine = new LightCommandLine(messaging);
103 + var arguments = serviceProvider.GetService<ICommandLineArguments>();
104 + arguments.Populate(args);
105
106 - string[] unprocessed = this.commandLine.Parse(args);
107 - if (messaging.EncounteredError)
108 - {
109 - return unprocessed;
110 - }
106 + var extensionManager = CreateExtensionManagerWithStandardBackends(serviceProvider, arguments.Extensions);
107
112 - // Load extensions.
113 - var extensionManager = CreateExtensionManagerWithStandardBackends(serviceProvider);
114 - foreach (string extension in this.commandLine.Extensions)
115 - {
116 - extensionManager.Load(extension);
117 - }
118 -
119 - // Extension data command line processing.
108 var context = serviceProvider.GetService<ICommandLineContext>();
121 - context.Arguments = null;
109 context.ExtensionManager = extensionManager;
110 context.Messaging = messaging;
124 - context.ParsedArguments = args;
125 -
126 - var commandLineExtensions = extensionManager.Create<IExtensionCommandLine>();
127 - foreach (var extension in commandLineExtensions)
128 - {
129 - extension.PreParse(context);
130 - }
111 + context.Arguments = arguments;
112
132 - // Process unproccessed arguments.
133 - List<string> actuallyUnprocessed = new List<string>();
134 - foreach (var arg in unprocessed)
135 - {
136 - if (!this.TryParseCommandLineArgumentWithExtension(arg, commandLineExtensions))
137 - {
138 - actuallyUnprocessed.Add(arg);
139 - }
140 - }
113 + this.commandLine = new LightCommandLine(messaging);
114 + var unprocessed = this.commandLine.Parse(context);
115
142 - return this.commandLine.ParsePostExtensions(actuallyUnprocessed.ToArray());
116 + return unprocessed;
117 }
118
119 private void Bind(IServiceProvider serviceProvider, IMessaging messaging)
@@ -194,7 +168,7 @@ namespace WixToolset.Tools
168 binder.IntermediateRepresentation = resolveResult.IntermediateRepresentation;
169 binder.OutputPath = this.commandLine.OutputFile;
170 binder.OutputPdbPath = Path.ChangeExtension(this.commandLine.OutputFile, ".wixpdb");
197 - binder.SuppressIces = this.commandLine.SuppressIces;
171 + binder.SuppressIces = this.commandLine.SuppressIces;
172 binder.SuppressValidation = this.commandLine.SuppressValidation;
173
174 bindResult = binder.Execute();
@@ -526,7 +500,7 @@ namespace WixToolset.Tools
500 return Intermediate.Load(path);
501 }
502
529 - private static IExtensionManager CreateExtensionManagerWithStandardBackends(IServiceProvider serviceProvider)
503 + private static IExtensionManager CreateExtensionManagerWithStandardBackends(IServiceProvider serviceProvider, IEnumerable<string> extensions)
504 {
505 var extensionManager = serviceProvider.GetService<IExtensionManager>();
506
@@ -535,6 +509,11 @@ namespace WixToolset.Tools
509 extensionManager.Add(type.Assembly);
510 }
511
512 + foreach (var extension in extensions)
513 + {
514 + extensionManager.Load(extension);
515 + }
516 +
517 return extensionManager;
518 }
519
src/test/TestData/Example.Extension/ExamplePreprocessorExtensionAndCommandLine.cs
+1 -1
@@ -26,7 +26,7 @@ namespace Example.Extension
26 {
27 if (parseCommandLine.IsSwitch(arg) && arg.Substring(1).Equals("example", StringComparison.OrdinalIgnoreCase))
28 {
29 - parseCommandLine.GetNextArgumentOrError(ref this.exampleValueFromCommandLine);
29 + this.exampleValueFromCommandLine = parseCommandLine.GetNextArgumentOrError(arg);
30 return true;
31 }
32
src/wix/Program.cs
+11 -3
@@ -42,17 +42,20 @@ namespace WixToolset.Core
42 var messaging = serviceProvider.GetService<IMessaging>();
43 messaging.SetListener(listener);
44
45 + var arguments = serviceProvider.GetService<ICommandLineArguments>();
46 + arguments.Populate(args);
47 +
48 var context = serviceProvider.GetService<ICommandLineContext>();
49 context.Messaging = messaging;
47 - context.ExtensionManager = CreateExtensionManagerWithStandardBackends(serviceProvider);
48 - context.ParsedArguments = args;
50 + context.ExtensionManager = CreateExtensionManagerWithStandardBackends(serviceProvider, arguments.Extensions);
51 + context.Arguments = arguments;
52
53 var commandLine = serviceProvider.GetService<ICommandLine>();
54 var command = commandLine.ParseStandardCommandLine(context);
55 return command?.Execute() ?? 1;
56 }
57
55 - private static IExtensionManager CreateExtensionManagerWithStandardBackends(IServiceProvider serviceProvider)
58 + private static IExtensionManager CreateExtensionManagerWithStandardBackends(IServiceProvider serviceProvider, string[] extensions)
59 {
60 var extensionManager = serviceProvider.GetService<IExtensionManager>();
61
@@ -61,6 +64,11 @@ namespace WixToolset.Core
64 extensionManager.Add(type.Assembly);
65 }
66
67 + foreach (var extension in extensions)
68 + {
69 + extensionManager.Load(extension);
70 + }
71 +
72 return extensionManager;
73 }
74