aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.BuildTasks/WixToolTask.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.BuildTasks/WixToolTask.cs')
-rw-r--r--src/WixToolset.BuildTasks/WixToolTask.cs406
1 files changed, 406 insertions, 0 deletions
diff --git a/src/WixToolset.BuildTasks/WixToolTask.cs b/src/WixToolset.BuildTasks/WixToolTask.cs
new file mode 100644
index 00000000..2e5e8705
--- /dev/null
+++ b/src/WixToolset.BuildTasks/WixToolTask.cs
@@ -0,0 +1,406 @@
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
3namespace WixToolset.BuildTasks
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Diagnostics.CodeAnalysis;
9 using System.Globalization;
10 using System.IO;
11 using System.Reflection;
12 using System.Text;
13 using System.Threading;
14
15 using Microsoft.Build.Framework;
16 using Microsoft.Build.Utilities;
17
18 /// <summary>
19 /// Base class for WiX tool tasks; executes tools in-process
20 /// so that repeated invocations are much faster.
21 /// </summary>
22 public abstract class WixToolTask : ToolTask, IDisposable
23 {
24 private string additionalOptions;
25 private bool disposed;
26 private bool noLogo;
27 private bool runAsSeparateProcess;
28 private bool suppressAllWarnings;
29 private string[] suppressSpecificWarnings;
30 private string[] treatSpecificWarningsAsErrors;
31 private bool treatWarningsAsErrors;
32 private bool verboseOutput;
33 private Queue<string> messageQueue;
34 private ManualResetEvent messagesAvailable;
35 private ManualResetEvent toolExited;
36 private int exitCode;
37
38 /// <summary>
39 /// Gets or sets additional options that are appended the the tool command-line.
40 /// </summary>
41 /// <remarks>
42 /// This allows the task to support extended options in the tool which are not
43 /// explicitly implemented as properties on the task.
44 /// </remarks>
45 public string AdditionalOptions
46 {
47 get { return this.additionalOptions; }
48 set { this.additionalOptions = value; }
49 }
50
51 /// <summary>
52 /// Gets or sets a flag indicating whether the task should be run as separate
53 /// process instead of in-proc with MSBuild which is the default.
54 /// </summary>
55 public bool RunAsSeparateProcess
56 {
57 get { return this.runAsSeparateProcess; }
58 set { this.runAsSeparateProcess = value; }
59 }
60
61#region Common Options
62 /// <summary>
63 /// Gets or sets whether all warnings should be suppressed.
64 /// </summary>
65 public bool SuppressAllWarnings
66 {
67 get { return this.suppressAllWarnings; }
68 set { this.suppressAllWarnings = value; }
69 }
70
71 /// <summary>
72 /// Gets or sets a list of specific warnings to be suppressed.
73 /// </summary>
74 public string[] SuppressSpecificWarnings
75 {
76 get { return this.suppressSpecificWarnings; }
77 set { this.suppressSpecificWarnings = value; }
78 }
79
80 /// <summary>
81 /// Gets or sets whether all warnings should be treated as errors.
82 /// </summary>
83 public bool TreatWarningsAsErrors
84 {
85 get { return this.treatWarningsAsErrors; }
86 set { this.treatWarningsAsErrors = value; }
87 }
88
89 /// <summary>
90 /// Gets or sets a list of specific warnings to treat as errors.
91 /// </summary>
92 public string[] TreatSpecificWarningsAsErrors
93 {
94 get { return this.treatSpecificWarningsAsErrors; }
95 set { this.treatSpecificWarningsAsErrors = value; }
96 }
97
98 /// <summary>
99 /// Gets or sets whether to display verbose output.
100 /// </summary>
101 public bool VerboseOutput
102 {
103 get { return this.verboseOutput; }
104 set { this.verboseOutput = value; }
105 }
106
107 /// <summary>
108 /// Gets or sets whether to display the logo.
109 /// </summary>
110 public bool NoLogo
111 {
112 get { return this.noLogo; }
113 set { this.noLogo = value; }
114 }
115#endregion
116
117 /// <summary>
118 /// Cleans up the ManualResetEvent members
119 /// </summary>
120 public void Dispose()
121 {
122 if (!this.disposed)
123 {
124 this.Dispose(true);
125 GC.SuppressFinalize(this);
126 disposed = true;
127 }
128 }
129
130 /// <summary>
131 /// Cleans up the ManualResetEvent members
132 /// </summary>
133 protected virtual void Dispose(bool disposing)
134 {
135 if (disposing)
136 {
137 messagesAvailable.Close();
138 toolExited.Close();
139 }
140 }
141
142 /// <summary>
143 /// Generate the command line arguments to write to the response file from the properties.
144 /// </summary>
145 /// <returns>Command line string.</returns>
146 protected override string GenerateResponseFileCommands()
147 {
148 WixCommandLineBuilder commandLineBuilder = new WixCommandLineBuilder();
149 this.BuildCommandLine(commandLineBuilder);
150 return commandLineBuilder.ToString();
151 }
152
153 /// <summary>
154 /// Builds a command line from options in this and derivative tasks.
155 /// </summary>
156 /// <remarks>
157 /// Derivative classes should call BuildCommandLine() on the base class to ensure that common command line options are added to the command.
158 /// </remarks>
159 protected virtual void BuildCommandLine(WixCommandLineBuilder commandLineBuilder)
160 {
161 commandLineBuilder.AppendIfTrue("-nologo", this.NoLogo);
162 commandLineBuilder.AppendArrayIfNotNull("-sw", this.SuppressSpecificWarnings);
163 commandLineBuilder.AppendIfTrue("-sw", this.SuppressAllWarnings);
164 commandLineBuilder.AppendIfTrue("-v", this.VerboseOutput);
165 commandLineBuilder.AppendArrayIfNotNull("-wx", this.TreatSpecificWarningsAsErrors);
166 commandLineBuilder.AppendIfTrue("-wx", this.TreatWarningsAsErrors);
167 }
168
169 /// <summary>
170 /// Executes a tool in-process by loading the tool assembly and invoking its entrypoint.
171 /// </summary>
172 /// <param name="pathToTool">Path to the tool to be executed; must be a managed executable.</param>
173 /// <param name="responseFileCommands">Commands to be written to a response file.</param>
174 /// <param name="commandLineCommands">Commands to be passed directly on the command-line.</param>
175 /// <returns>The tool exit code.</returns>
176 protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands)
177 {
178 if (this.RunAsSeparateProcess)
179 {
180 return base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands);
181 }
182
183 this.messageQueue = new Queue<string>();
184 this.messagesAvailable = new ManualResetEvent(false);
185 this.toolExited = new ManualResetEvent(false);
186
187 Util.RunningInMsBuild = true;
188
189 WixToolTaskLogger logger = new WixToolTaskLogger(this.messageQueue, this.messagesAvailable);
190 TextWriter saveConsoleOut = Console.Out;
191 TextWriter saveConsoleError = Console.Error;
192 Console.SetOut(logger);
193 Console.SetError(logger);
194
195 string responseFile = null;
196 try
197 {
198 string responseFileSwitch;
199 responseFile = this.GetTemporaryResponseFile(responseFileCommands, out responseFileSwitch);
200 if (!String.IsNullOrEmpty(responseFileSwitch))
201 {
202 commandLineCommands = commandLineCommands + " " + responseFileSwitch;
203 }
204
205 string[] arguments = CommandLineResponseFile.ParseArgumentsToArray(commandLineCommands);
206
207 Thread toolThread = new Thread(new ParameterizedThreadStart(this.ExecuteToolThread));
208 toolThread.Start(new object[] { pathToTool, arguments });
209
210 this.HandleToolMessages();
211
212 if (this.exitCode == 0 && this.Log.HasLoggedErrors)
213 {
214 this.exitCode = -1;
215 }
216
217 return this.exitCode;
218 }
219 finally
220 {
221 if (responseFile != null)
222 {
223 File.Delete(responseFile);
224 }
225
226 Console.SetOut(saveConsoleOut);
227 Console.SetError(saveConsoleError);
228 }
229 }
230
231 /// <summary>
232 /// Called by a new thread to execute the tool in that thread.
233 /// </summary>
234 /// <param name="parameters">Tool path and arguments array.</param>
235 private void ExecuteToolThread(object parameters)
236 {
237 try
238 {
239 object[] pathAndArguments = (object[])parameters;
240 Assembly toolAssembly = Assembly.LoadFrom((string)pathAndArguments[0]);
241 this.exitCode = (int)toolAssembly.EntryPoint.Invoke(null, new object[] { pathAndArguments[1] });
242 }
243 catch (FileNotFoundException fnfe)
244 {
245 Log.LogError("Unable to load tool from path {0}. Consider setting the ToolPath parameter to $(WixToolPath).", fnfe.FileName);
246 this.exitCode = -1;
247 }
248 catch (Exception ex)
249 {
250 this.exitCode = -1;
251 this.LogEventsFromTextOutput(ex.Message, MessageImportance.High);
252 foreach (string stackTraceLine in ex.StackTrace.Split('\n'))
253 {
254 this.LogEventsFromTextOutput(stackTraceLine.TrimEnd(), MessageImportance.High);
255 }
256
257 throw;
258 }
259 finally
260 {
261 this.toolExited.Set();
262 }
263 }
264
265 /// <summary>
266 /// Waits for messages from the tool thread and sends them to the MSBuild logger on the original thread.
267 /// Returns when the tool thread exits.
268 /// </summary>
269 private void HandleToolMessages()
270 {
271 WaitHandle[] waitHandles = new WaitHandle[] { this.messagesAvailable, this.toolExited };
272 while (WaitHandle.WaitAny(waitHandles) == 0)
273 {
274 lock (this.messageQueue)
275 {
276 while (this.messageQueue.Count > 0)
277 {
278 this.LogEventsFromTextOutput(messageQueue.Dequeue(), MessageImportance.Normal);
279 }
280
281 this.messagesAvailable.Reset();
282 }
283 }
284 }
285
286 /// <summary>
287 /// Creates a temporary response file for tool execution.
288 /// </summary>
289 /// <returns>Path to the response file.</returns>
290 /// <remarks>
291 /// The temporary file should be deleted after the tool execution is finished.
292 /// </remarks>
293 private string GetTemporaryResponseFile(string responseFileCommands, out string responseFileSwitch)
294 {
295 string responseFile = null;
296 responseFileSwitch = null;
297
298 if (!String.IsNullOrEmpty(responseFileCommands))
299 {
300 responseFile = Path.GetTempFileName();
301 using (StreamWriter writer = new StreamWriter(responseFile, false, this.ResponseFileEncoding))
302 {
303 writer.Write(responseFileCommands);
304 }
305 responseFileSwitch = this.GetResponseFileSwitch(responseFile);
306 }
307 return responseFile;
308 }
309
310 /// <summary>
311 /// Cycles thru each task to find correct path of the file in question.
312 /// Looks at item spec, hintpath and then in user defined Reference Paths
313 /// </summary>
314 /// <param name="tasks">Input task array</param>
315 /// <param name="referencePaths">SemiColon delimited directories to search</param>
316 /// <returns>List of task item file paths</returns>
317 [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
318 protected static List<string> AdjustFilePaths(ITaskItem[] tasks, string[] referencePaths)
319 {
320 List<string> sourceFilePaths = new List<string>();
321
322 if (tasks == null)
323 {
324 return sourceFilePaths;
325 }
326
327 foreach (ITaskItem task in tasks)
328 {
329 string filePath = task.ItemSpec;
330 if (!File.Exists(filePath))
331 {
332 filePath = task.GetMetadata("HintPath");
333 if (!File.Exists(filePath))
334 {
335 string searchPath = FileSearchHelperMethods.SearchFilePaths(referencePaths, filePath);
336 if (!String.IsNullOrEmpty(searchPath))
337 {
338 filePath = searchPath;
339 }
340 }
341 }
342 sourceFilePaths.Add(filePath);
343 }
344
345 return sourceFilePaths;
346 }
347
348 /// <summary>
349 /// Used as a replacement for Console.Out to capture output from a tool
350 /// and redirect it to the MSBuild logging system.
351 /// </summary>
352 private class WixToolTaskLogger : TextWriter
353 {
354 private StringBuilder buffer;
355 private Queue<string> messageQueue;
356 private ManualResetEvent messagesAvailable;
357
358 /// <summary>
359 /// Creates a new logger that sends tool output to the tool task's log handler.
360 /// </summary>
361 public WixToolTaskLogger(Queue<string> messageQueue, ManualResetEvent messagesAvailable) : base(CultureInfo.CurrentCulture)
362 {
363 this.messageQueue = messageQueue;
364 this.messagesAvailable = messagesAvailable;
365 this.buffer = new StringBuilder();
366 }
367
368 /// <summary>
369 /// Gets the encoding of the logger.
370 /// </summary>
371 public override Encoding Encoding
372 {
373 get { return Encoding.Unicode; }
374 }
375
376 /// <summary>
377 /// Redirects output to a buffer; watches for newlines and sends each line to the
378 /// MSBuild logging system.
379 /// </summary>
380 /// <param name="value">Character being written.</param>
381 /// <remarks>All other Write() variants eventually call into this one.</remarks>
382 public override void Write(char value)
383 {
384 lock (this.messageQueue)
385 {
386 if (value == '\n')
387 {
388 if (this.buffer.Length > 0 && this.buffer[this.buffer.Length - 1] == '\r')
389 {
390 this.buffer.Length = this.buffer.Length - 1;
391 }
392
393 this.messageQueue.Enqueue(this.buffer.ToString());
394 this.messagesAvailable.Set();
395
396 this.buffer.Length = 0;
397 }
398 else
399 {
400 this.buffer.Append(value);
401 }
402 }
403 }
404 }
405 }
406}