aboutsummaryrefslogtreecommitdiff
path: root/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs')
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs455
1 files changed, 455 insertions, 0 deletions
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
new file mode 100644
index 00000000..83a4949e
--- /dev/null
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
@@ -0,0 +1,455 @@
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.Core.WindowsInstaller.Bind
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Runtime.InteropServices;
11 using WixToolset.Data;
12 using WixToolset.Data.Symbols;
13 using WixToolset.Data.WindowsInstaller;
14 using WixToolset.Extensibility;
15 using WixToolset.Extensibility.Data;
16 using WixToolset.Extensibility.Services;
17
18 /// <summary>
19 /// Creates cabinet files.
20 /// </summary>
21 internal class CreateCabinetsCommand
22 {
23 public const int DefaultMaximumUncompressedMediaSize = 200; // Default value is 200 MB
24 public const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB)
25
26 private readonly List<IFileTransfer> fileTransfers;
27
28 private readonly List<ITrackedFile> trackedFiles;
29
30 private readonly FileSplitCabNamesCallback newCabNamesCallBack;
31
32 private Dictionary<string, string> lastCabinetAddedToMediaTable; // Key is First Cabinet Name, Value is Last Cabinet Added in the Split Sequence
33
34 public CreateCabinetsCommand(IServiceProvider serviceProvider, IBackendHelper backendHelper, WixMediaTemplateSymbol mediaTemplate)
35 {
36 this.fileTransfers = new List<IFileTransfer>();
37
38 this.trackedFiles = new List<ITrackedFile>();
39
40 this.newCabNamesCallBack = this.NewCabNamesCallBack;
41
42 this.ServiceProvider = serviceProvider;
43
44 this.BackendHelper = backendHelper;
45
46 this.MediaTemplate = mediaTemplate;
47 }
48
49 private IServiceProvider ServiceProvider { get; }
50
51 private IBackendHelper BackendHelper { get; }
52
53 private WixMediaTemplateSymbol MediaTemplate { get; }
54
55 /// <summary>
56 /// Sets the number of threads to use for cabinet creation.
57 /// </summary>
58 public int CabbingThreadCount { private get; set; }
59
60 public string CabCachePath { private get; set; }
61
62 public IMessaging Messaging { private get; set; }
63
64 public string IntermediateFolder { private get; set; }
65
66 /// <summary>
67 /// Sets the default compression level to use for cabinets
68 /// that don't have their compression level explicitly set.
69 /// </summary>
70 public CompressionLevel? DefaultCompressionLevel { private get; set; }
71
72 public IEnumerable<IWindowsInstallerBackendBinderExtension> BackendExtensions { private get; set; }
73
74 public WindowsInstallerData Data { private get; set; }
75
76 public string LayoutDirectory { private get; set; }
77
78 public bool Compressed { private get; set; }
79
80 public string ModularizationSuffix { private get; set; }
81
82 public Dictionary<MediaSymbol, IEnumerable<IFileFacade>> FileFacadesByCabinet { private get; set; }
83
84 public Func<MediaSymbol, string, string, string> ResolveMedia { private get; set; }
85
86 public TableDefinitionCollection TableDefinitions { private get; set; }
87
88 public IEnumerable<IFileTransfer> FileTransfers => this.fileTransfers;
89
90 public IEnumerable<ITrackedFile> TrackedFiles => this.trackedFiles;
91
92 public void Execute()
93 {
94 this.lastCabinetAddedToMediaTable = new Dictionary<string, string>();
95
96 // If the cabbing thread count wasn't provided, default the number of cabbing threads to the number of processors.
97 if (this.CabbingThreadCount <= 0)
98 {
99 this.CabbingThreadCount = this.CalculateCabbingThreadCount();
100
101 this.Messaging.Write(VerboseMessages.SetCabbingThreadCount(this.CabbingThreadCount.ToString()));
102 }
103
104 // Send Binder object to Facilitate NewCabNamesCallBack Callback
105 var cabinetBuilder = new CabinetBuilder(this.Messaging, this.CabbingThreadCount, Marshal.GetFunctionPointerForDelegate(this.newCabNamesCallBack));
106
107 // Supply Compile MediaTemplate Attributes to Cabinet Builder
108 this.GetMediaTemplateAttributes(out var maximumCabinetSizeForLargeFileSplitting, out var maximumUncompressedMediaSize);
109 cabinetBuilder.MaximumCabinetSizeForLargeFileSplitting = maximumCabinetSizeForLargeFileSplitting;
110 cabinetBuilder.MaximumUncompressedMediaSize = maximumUncompressedMediaSize;
111
112 foreach (var entry in this.FileFacadesByCabinet)
113 {
114 var mediaSymbol = entry.Key;
115 var files = entry.Value;
116 var compressionLevel = mediaSymbol.CompressionLevel ?? this.DefaultCompressionLevel ?? CompressionLevel.Medium;
117 var cabinetDir = this.ResolveMedia(mediaSymbol, mediaSymbol.Layout, this.LayoutDirectory);
118
119 var cabinetWorkItem = this.CreateCabinetWorkItem(this.Data, cabinetDir, mediaSymbol, compressionLevel, files);
120 if (null != cabinetWorkItem)
121 {
122 cabinetBuilder.Enqueue(cabinetWorkItem);
123 }
124 }
125
126 // stop processing if an error previously occurred
127 if (this.Messaging.EncounteredError)
128 {
129 return;
130 }
131
132 // create queued cabinets with multiple threads
133 cabinetBuilder.CreateQueuedCabinets();
134 if (this.Messaging.EncounteredError)
135 {
136 return;
137 }
138 }
139
140 private int CalculateCabbingThreadCount()
141 {
142 var cabbingThreadCount = Environment.ProcessorCount;
143
144 if (cabbingThreadCount <= 0)
145 {
146 cabbingThreadCount = 1; // reset to 1 when the environment variable is invalid.
147
148 this.Messaging.Write(WarningMessages.InvalidEnvironmentVariable("NUMBER_OF_PROCESSORS", Environment.ProcessorCount.ToString(), cabbingThreadCount.ToString()));
149 }
150
151 return cabbingThreadCount;
152 }
153
154 /// <summary>
155 /// Creates a work item to create a cabinet.
156 /// </summary>
157 /// <param name="data">Windows Installer data for the current database.</param>
158 /// <param name="cabinetDir">Directory to create cabinet in.</param>
159 /// <param name="mediaSymbol">Media symbol containing information about the cabinet.</param>
160 /// <param name="compressionLevel">Desired compression level.</param>
161 /// <param name="fileFacades">Collection of files in this cabinet.</param>
162 /// <returns>created CabinetWorkItem object</returns>
163 private CabinetWorkItem CreateCabinetWorkItem(WindowsInstallerData data, string cabinetDir, MediaSymbol mediaSymbol, CompressionLevel compressionLevel, IEnumerable<IFileFacade> fileFacades)
164 {
165 CabinetWorkItem cabinetWorkItem = null;
166 var tempCabinetFileX = Path.Combine(this.IntermediateFolder, mediaSymbol.Cabinet);
167
168 // check for an empty cabinet
169 if (!fileFacades.Any())
170 {
171 // Remove the leading '#' from the embedded cabinet name to make the warning easier to understand
172 var cabinetName = mediaSymbol.Cabinet.TrimStart('#');
173
174 // If building a patch, remind them to run -p for torch.
175 if (OutputType.Patch == data.Type)
176 {
177 this.Messaging.Write(WarningMessages.EmptyCabinet(mediaSymbol.SourceLineNumbers, cabinetName, true));
178 }
179 else
180 {
181 this.Messaging.Write(WarningMessages.EmptyCabinet(mediaSymbol.SourceLineNumbers, cabinetName));
182 }
183 }
184
185 var cabinetResolver = new CabinetResolver(this.ServiceProvider, this.CabCachePath, this.BackendExtensions);
186
187 var resolvedCabinet = cabinetResolver.ResolveCabinet(tempCabinetFileX, fileFacades);
188
189 // create a cabinet work item if it's not being skipped
190 if (CabinetBuildOption.BuildAndCopy == resolvedCabinet.BuildOption || CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption)
191 {
192 // Default to the threshold for best smartcabbing (makes smallest cabinet).
193 cabinetWorkItem = new CabinetWorkItem(fileFacades, resolvedCabinet.Path, maxThreshold: 0, compressionLevel, this.ModularizationSuffix /*, this.FileManager*/);
194 }
195 else // reuse the cabinet from the cabinet cache.
196 {
197 this.Messaging.Write(VerboseMessages.ReusingCabCache(mediaSymbol.SourceLineNumbers, mediaSymbol.Cabinet, resolvedCabinet.Path));
198
199 try
200 {
201 // Ensure the cached cabinet timestamp is current to prevent perpetual incremental builds. The
202 // problematic scenario goes like this. Imagine two cabinets in the cache. Update a file that
203 // goes into one of the cabinets. One cabinet will get rebuilt, the other will be copied from
204 // the cache. Now the file (an input) has a newer timestamp than the reused cabient (an output)
205 // causing the project to look like it perpetually needs a rebuild until all of the reused
206 // cabinets get newer timestamps.
207 File.SetLastWriteTime(resolvedCabinet.Path, DateTime.Now);
208 }
209 catch (Exception e)
210 {
211 this.Messaging.Write(WarningMessages.CannotUpdateCabCache(mediaSymbol.SourceLineNumbers, resolvedCabinet.Path, e.Message));
212 }
213 }
214
215 var trackResolvedCabinet = this.BackendHelper.TrackFile(resolvedCabinet.Path, TrackedFileType.Intermediate, mediaSymbol.SourceLineNumbers);
216 this.trackedFiles.Add(trackResolvedCabinet);
217
218 if (mediaSymbol.Cabinet.StartsWith("#", StringComparison.Ordinal))
219 {
220 var streamsTable = data.EnsureTable(this.TableDefinitions["_Streams"]);
221
222 var streamRow = streamsTable.CreateRow(mediaSymbol.SourceLineNumbers);
223 streamRow[0] = mediaSymbol.Cabinet.Substring(1);
224 streamRow[1] = resolvedCabinet.Path;
225 }
226 else
227 {
228 var trackDestination = this.BackendHelper.TrackFile(Path.Combine(cabinetDir, mediaSymbol.Cabinet), TrackedFileType.Final, mediaSymbol.SourceLineNumbers);
229 this.trackedFiles.Add(trackDestination);
230
231 var transfer = this.BackendHelper.CreateFileTransfer(resolvedCabinet.Path, trackDestination.Path, resolvedCabinet.BuildOption == CabinetBuildOption.BuildAndMove, mediaSymbol.SourceLineNumbers);
232 this.fileTransfers.Add(transfer);
233 }
234
235 return cabinetWorkItem;
236 }
237
238 //private ResolvedCabinet ResolveCabinet(string cabinetPath, IEnumerable<FileFacade> fileFacades)
239 //{
240 // ResolvedCabinet resolved = null;
241
242 // List<BindFileWithPath> filesWithPath = fileFacades.Select(f => new BindFileWithPath() { Id = f.File.File, Path = f.WixFile.Source }).ToList();
243
244 // foreach (var extension in this.BackendExtensions)
245 // {
246 // resolved = extension.ResolveCabinet(cabinetPath, filesWithPath);
247 // if (null != resolved)
248 // {
249 // break;
250 // }
251 // }
252
253 // return resolved;
254 //}
255
256 /// <summary>
257 /// Delegate for Cabinet Split Callback
258 /// </summary>
259 [UnmanagedFunctionPointer(CallingConvention.StdCall)]
260 internal delegate void FileSplitCabNamesCallback([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken);
261
262 /// <summary>
263 /// Call back to Add File Transfer for new Cab and add new Cab to Media table
264 /// This callback can come from Multiple Cabinet Builder Threads and so should be thread safe
265 /// This callback will not be called in case there is no File splitting. i.e. MaximumCabinetSizeForLargeFileSplitting was not authored
266 /// </summary>
267 /// <param name="firstCabName">The name of splitting cabinet without extention e.g. "cab1".</param>
268 /// <param name="newCabinetName">The name of the new cabinet that would be formed by splitting e.g. "cab1b.cab"</param>
269 /// <param name="fileToken">The file token of the first file present in the splitting cabinet</param>
270 internal void NewCabNamesCallBack([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabinetName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken)
271 {
272 throw new NotImplementedException();
273#if TODO_CAB_SPANNING
274 // Locking Mutex here as this callback can come from Multiple Cabinet Builder Threads
275 var mutex = new Mutex(false, "WixCabinetSplitBinderCallback");
276 try
277 {
278 if (!mutex.WaitOne(0, false)) // Check if you can get the lock
279 {
280 // Cound not get the Lock
281 this.Messaging.Write(VerboseMessages.CabinetsSplitInParallel());
282 mutex.WaitOne(); // Wait on other thread
283 }
284
285 var firstCabinetName = firstCabName + ".cab";
286 var transferAdded = false; // Used for Error Handling
287
288 // Create File Transfer for new Cabinet using transfer of Base Cabinet
289 foreach (var transfer in this.FileTransfers)
290 {
291 if (firstCabinetName.Equals(Path.GetFileName(transfer.Source), StringComparison.InvariantCultureIgnoreCase))
292 {
293 var newCabSourcePath = Path.Combine(Path.GetDirectoryName(transfer.Source), newCabinetName);
294 var newCabTargetPath = Path.Combine(Path.GetDirectoryName(transfer.Destination), newCabinetName);
295
296 var trackSource = this.BackendHelper.TrackFile(newCabSourcePath, TrackedFileType.Intermediate, transfer.SourceLineNumbers);
297 this.trackedFiles.Add(trackSource);
298
299 var trackTarget = this.BackendHelper.TrackFile(newCabTargetPath, TrackedFileType.Final, transfer.SourceLineNumbers);
300 this.trackedFiles.Add(trackTarget);
301
302 var newTransfer = this.BackendHelper.CreateFileTransfer(trackSource.Path, trackTarget.Path, transfer.Move, transfer.SourceLineNumbers);
303 this.fileTransfers.Add(newTransfer);
304
305 transferAdded = true;
306 break;
307 }
308 }
309
310 // Check if File Transfer was added
311 if (!transferAdded)
312 {
313 throw new WixException(ErrorMessages.SplitCabinetCopyRegistrationFailed(newCabinetName, firstCabinetName));
314 }
315
316 // Add the new Cabinets to media table using LastSequence of Base Cabinet
317 var mediaTable = this.Output.Tables["Media"];
318 var wixFileTable = this.Output.Tables["WixFile"];
319 var diskIDForLastSplitCabAdded = 0; // The DiskID value for the first cab in this cabinet split chain
320 var lastSequenceForLastSplitCabAdded = 0; // The LastSequence value for the first cab in this cabinet split chain
321 var lastSplitCabinetFound = false; // Used for Error Handling
322
323 var lastCabinetOfThisSequence = String.Empty;
324 // Get the Value of Last Cabinet Added in this split Sequence from Dictionary
325 if (!this.lastCabinetAddedToMediaTable.TryGetValue(firstCabinetName, out lastCabinetOfThisSequence))
326 {
327 // If there is no value for this sequence, then use first Cabinet is the last one of this split sequence
328 lastCabinetOfThisSequence = firstCabinetName;
329 }
330
331 foreach (MediaRow mediaRow in mediaTable.Rows)
332 {
333 // Get details for the Last Cabinet Added in this Split Sequence
334 if ((lastSequenceForLastSplitCabAdded == 0) && lastCabinetOfThisSequence.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase))
335 {
336 lastSequenceForLastSplitCabAdded = mediaRow.LastSequence;
337 diskIDForLastSplitCabAdded = mediaRow.DiskId;
338 lastSplitCabinetFound = true;
339 }
340
341 // Check for Name Collision for the new Cabinet added
342 if (newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase))
343 {
344 // Name Collision of generated Split Cabinet Name and user Specified Cab name for current row
345 throw new WixException(ErrorMessages.SplitCabinetNameCollision(newCabinetName, firstCabinetName));
346 }
347 }
348
349 // Check if the last Split Cabinet was found in the Media Table
350 if (!lastSplitCabinetFound)
351 {
352 throw new WixException(ErrorMessages.SplitCabinetInsertionFailed(newCabinetName, firstCabinetName, lastCabinetOfThisSequence));
353 }
354
355 // The new Row has to be inserted just after the last cab in this cabinet split chain according to DiskID Sort
356 // This is because the FDI Extract requires DiskID of Split Cabinets to be continuous. It Fails otherwise with
357 // Error 2350 (FDI Server Error) as next DiskID did not have the right split cabinet during extraction
358 MediaRow newMediaRow = (MediaRow)mediaTable.CreateRow(null);
359 newMediaRow.Cabinet = newCabinetName;
360 newMediaRow.DiskId = diskIDForLastSplitCabAdded + 1; // When Sorted with DiskID, this new Cabinet Row is an Insertion
361 newMediaRow.LastSequence = lastSequenceForLastSplitCabAdded;
362
363 // Now increment the DiskID for all rows that come after the newly inserted row to Ensure that DiskId is unique
364 foreach (MediaRow mediaRow in mediaTable.Rows)
365 {
366 // Check if this row comes after inserted row and it is not the new cabinet inserted row
367 if (mediaRow.DiskId >= newMediaRow.DiskId && !newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase))
368 {
369 mediaRow.DiskId++; // Increment DiskID
370 }
371 }
372
373 // Now Increment DiskID for All files Rows so that they refer to the right Media Row
374 foreach (WixFileRow wixFileRow in wixFileTable.Rows)
375 {
376 // Check if this row comes after inserted row and if this row is not the file that has to go into the current cabinet
377 // This check will work as we have only one large file in every splitting cabinet
378 // If we want to support splitting cabinet with more large files we need to update this code
379 if (wixFileRow.DiskId >= newMediaRow.DiskId && !wixFileRow.File.Equals(fileToken, StringComparison.InvariantCultureIgnoreCase))
380 {
381 wixFileRow.DiskId++; // Increment DiskID
382 }
383 }
384
385 // Update the Last Cabinet Added in the Split Sequence in Dictionary for future callback
386 this.lastCabinetAddedToMediaTable[firstCabinetName] = newCabinetName;
387
388 mediaTable.ValidateRows(); // Valdiates DiskDIs, throws Exception as Wix Error if validation fails
389 }
390 finally
391 {
392 // Releasing the Mutex here
393 mutex.ReleaseMutex();
394 }
395#endif
396 }
397
398
399 /// <summary>
400 /// Gets Compiler Values of MediaTemplate Attributes governing Maximum Cabinet Size after applying Environment Variable Overrides
401 /// </summary>
402 private void GetMediaTemplateAttributes(out int maxCabSizeForLargeFileSplitting, out int maxUncompressedMediaSize)
403 {
404 // Get Environment Variable Overrides for MediaTemplate Attributes governing Maximum Cabinet Size
405 var mcslfsString = Environment.GetEnvironmentVariable("WIX_MCSLFS");
406 var mumsString = Environment.GetEnvironmentVariable("WIX_MUMS");
407
408 // Supply Compile MediaTemplate Attributes to Cabinet Builder
409 if (this.MediaTemplate != null)
410 {
411 // Get the Value for Max Cab Size for File Splitting
412 var maxCabSizeForLargeFileInMB = 0;
413 try
414 {
415 // Override authored mcslfs value if environment variable is authored.
416 maxCabSizeForLargeFileInMB = !String.IsNullOrEmpty(mcslfsString) ? Int32.Parse(mcslfsString) : this.MediaTemplate.MaximumCabinetSizeForLargeFileSplitting ?? MaxValueOfMaxCabSizeForLargeFileSplitting;
417
418 var testOverFlow = (ulong)maxCabSizeForLargeFileInMB * 1024 * 1024;
419 maxCabSizeForLargeFileSplitting = maxCabSizeForLargeFileInMB;
420 }
421 catch (FormatException)
422 {
423 throw new WixException(ErrorMessages.IllegalEnvironmentVariable("WIX_MCSLFS", mcslfsString));
424 }
425 catch (OverflowException)
426 {
427 throw new WixException(ErrorMessages.MaximumCabinetSizeForLargeFileSplittingTooLarge(null, maxCabSizeForLargeFileInMB, MaxValueOfMaxCabSizeForLargeFileSplitting));
428 }
429
430 var maxPreCompressedSizeInMB = 0;
431 try
432 {
433 // Override authored mums value if environment variable is authored.
434 maxPreCompressedSizeInMB = !String.IsNullOrEmpty(mumsString) ? Int32.Parse(mumsString) : this.MediaTemplate.MaximumUncompressedMediaSize ?? DefaultMaximumUncompressedMediaSize;
435
436 var testOverFlow = (ulong)maxPreCompressedSizeInMB * 1024 * 1024;
437 maxUncompressedMediaSize = maxPreCompressedSizeInMB;
438 }
439 catch (FormatException)
440 {
441 throw new WixException(ErrorMessages.IllegalEnvironmentVariable("WIX_MUMS", mumsString));
442 }
443 catch (OverflowException)
444 {
445 throw new WixException(ErrorMessages.MaximumUncompressedMediaSizeTooLarge(null, maxPreCompressedSizeInMB));
446 }
447 }
448 else
449 {
450 maxCabSizeForLargeFileSplitting = 0;
451 maxUncompressedMediaSize = DefaultMaximumUncompressedMediaSize;
452 }
453 }
454 }
455}