main
cs 364 lines 18.3 KB
Raw
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.WindowsInstaller.Bind
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using WixToolset.Data;
10 using WixToolset.Data.Symbols;
11 using WixToolset.Data.WindowsInstaller;
12 using WixToolset.Data.WindowsInstaller.Rows;
13 using WixToolset.Extensibility;
14 using WixToolset.Extensibility.Data;
15 using WixToolset.Extensibility.Services;
16
17 /// <summary>
18 /// Creates cabinet files.
19 /// </summary>
20 internal class CreateCabinetsCommand
21 {
22 public const int DefaultMaximumUncompressedMediaSize = 200; // Default value is 200 MB
23 public const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB)
24
25 private readonly CabinetResolver cabinetResolver;
26 private readonly List<IFileTransfer> fileTransfers;
27 private readonly List<ITrackedFile> trackedFiles;
28
29 public CreateCabinetsCommand(IServiceProvider serviceProvider, IMessaging messaging, IBackendHelper backendHelper, IEnumerable<IWindowsInstallerBackendBinderExtension> backendExtensions, IntermediateSection section, string cabCachePath, int cabbingThreadCount, string outputPath, string intermediateFolder, CompressionLevel? defaultCompressionLevel, bool compressed, string modularizationSuffix, Dictionary<MediaSymbol, IEnumerable<IFileFacade>> filesByCabinetMedia, WindowsInstallerData data, TableDefinitionCollection tableDefinitions, Func<MediaSymbol, string, string, string> resolveMedia)
30 {
31 this.Messaging = messaging;
32
33 this.BackendHelper = backendHelper;
34
35 this.Section = section;
36
37 this.CabbingThreadCount = cabbingThreadCount;
38
39 this.IntermediateFolder = intermediateFolder;
40 this.LayoutDirectory = Path.GetDirectoryName(outputPath);
41
42 this.DefaultCompressionLevel = defaultCompressionLevel;
43 this.ModularizationSuffix = modularizationSuffix;
44 this.FileFacadesByCabinet = filesByCabinetMedia;
45
46 this.Data = data;
47 this.TableDefinitions = tableDefinitions;
48
49 this.ResolveMedia = resolveMedia;
50
51 this.cabinetResolver = new CabinetResolver(serviceProvider, cabCachePath, backendExtensions);
52 this.fileTransfers = new List<IFileTransfer>();
53 this.trackedFiles = new List<ITrackedFile>();
54 }
55
56 private IMessaging Messaging { get; }
57
58 private IBackendHelper BackendHelper { get; }
59
60 private IntermediateSection Section { get; }
61
62 private int CabbingThreadCount { get; set; }
63
64 private string IntermediateFolder { get; }
65
66 private string LayoutDirectory { get; }
67
68 private CompressionLevel? DefaultCompressionLevel { get; }
69
70 private string ModularizationSuffix { get; }
71
72 private Dictionary<MediaSymbol, IEnumerable<IFileFacade>> FileFacadesByCabinet { get; }
73
74 private WindowsInstallerData Data { get; }
75
76 private TableDefinitionCollection TableDefinitions { get; }
77
78 private Func<MediaSymbol, string, string, string> ResolveMedia { get; }
79
80 public IEnumerable<IFileTransfer> FileTransfers => this.fileTransfers;
81
82 public IEnumerable<ITrackedFile> TrackedFiles => this.trackedFiles;
83
84 public void Execute()
85 {
86 this.GetMediaTemplateAttributes(out var maximumCabinetSizeForLargeFileSplitting, out var maximumUncompressedMediaSize);
87
88 var cabinetBuilder = new CabinetBuilder(this.Messaging, this.CabbingThreadCount, maximumCabinetSizeForLargeFileSplitting, maximumUncompressedMediaSize);
89
90 var hashesByFileId = this.Section.Symbols.OfType<MsiFileHashSymbol>().ToDictionary(s => s.Id.Id);
91
92 foreach (var entry in this.FileFacadesByCabinet)
93 {
94 var mediaSymbol = entry.Key;
95 var files = entry.Value;
96 var compressionLevel = mediaSymbol.CompressionLevel ?? this.DefaultCompressionLevel ?? CompressionLevel.Medium;
97 var cabinetDir = this.ResolveMedia(mediaSymbol, mediaSymbol.Layout, this.LayoutDirectory);
98
99 var cabinetWorkItem = this.CreateCabinetWorkItem(this.Data, cabinetDir, mediaSymbol, compressionLevel, files, hashesByFileId);
100 if (null != cabinetWorkItem)
101 {
102 cabinetBuilder.Enqueue(cabinetWorkItem);
103 }
104 }
105
106 // stop processing if an error previously occurred
107 if (this.Messaging.EncounteredError)
108 {
109 return;
110 }
111
112 // Create queued cabinets with multiple threads.
113 cabinetBuilder.CreateQueuedCabinets();
114
115 if (this.Messaging.EncounteredError)
116 {
117 return;
118 }
119
120 this.UpdateMediaWithSpannedCabinets(cabinetBuilder.CompletedCabinets);
121 }
122
123 private CabinetWorkItem CreateCabinetWorkItem(WindowsInstallerData data, string cabinetDir, MediaSymbol mediaSymbol, CompressionLevel compressionLevel, IEnumerable<IFileFacade> fileFacades, Dictionary<string, MsiFileHashSymbol> hashesByFileId)
124 {
125 CabinetWorkItem cabinetWorkItem = null;
126
127 var intermediateCabinetPath = Path.Combine(this.IntermediateFolder, mediaSymbol.Cabinet);
128
129 // check for an empty cabinet
130 if (!fileFacades.Any())
131 {
132 // Remove the leading '#' from the embedded cabinet name to make the warning easier to understand
133 var cabinetName = mediaSymbol.Cabinet.TrimStart('#');
134
135 // If building a patch, remind them to run -p for torch.
136 this.Messaging.Write(WarningMessages.EmptyCabinet(mediaSymbol.SourceLineNumbers, cabinetName, OutputType.Patch == data.Type));
137 }
138
139 var resolvedCabinet = this.cabinetResolver.ResolveCabinet(intermediateCabinetPath, fileFacades);
140
141 // Create a cabinet work item if it's not being skipped.
142 if (CabinetBuildOption.BuildAndCopy == resolvedCabinet.BuildOption || CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption)
143 {
144 // Default to the threshold for best smartcabbing (makes smallest cabinet).
145 cabinetWorkItem = new CabinetWorkItem(mediaSymbol.SourceLineNumbers, mediaSymbol.DiskId, resolvedCabinet.Path, fileFacades, hashesByFileId, maxThreshold: 0, compressionLevel: compressionLevel, modularizationSuffix: this.ModularizationSuffix);
146 }
147 else // reuse the cabinet from the cabinet cache.
148 {
149 this.Messaging.Write(VerboseMessages.ReusingCabCache(mediaSymbol.SourceLineNumbers, mediaSymbol.Cabinet, resolvedCabinet.Path));
150
151 try
152 {
153 // Ensure the cached cabinet timestamp is current to prevent perpetual incremental builds. The
154 // problematic scenario goes like this. Imagine two cabinets in the cache. Update a file that
155 // goes into one of the cabinets. One cabinet will get rebuilt, the other will be copied from
156 // the cache. Now the file (an input) has a newer timestamp than the reused cabient (an output)
157 // causing the project to look like it perpetually needs a rebuild until all of the reused
158 // cabinets get newer timestamps.
159 File.SetLastWriteTime(resolvedCabinet.Path, DateTime.Now);
160 }
161 catch (Exception e)
162 {
163 this.Messaging.Write(WarningMessages.CannotUpdateCabCache(mediaSymbol.SourceLineNumbers, resolvedCabinet.Path, e.Message));
164 }
165 }
166
167 var trackResolvedCabinet = this.BackendHelper.TrackFile(resolvedCabinet.Path, TrackedFileType.Intermediate, mediaSymbol.SourceLineNumbers);
168 this.trackedFiles.Add(trackResolvedCabinet);
169
170 if (mediaSymbol.Cabinet.StartsWith("#", StringComparison.Ordinal))
171 {
172 var streamsTable = data.EnsureTable(this.TableDefinitions["_Streams"]);
173
174 var streamRow = streamsTable.CreateRow(mediaSymbol.SourceLineNumbers);
175 streamRow[0] = mediaSymbol.Cabinet.Substring(1);
176 streamRow[1] = resolvedCabinet.Path;
177 }
178 else
179 {
180 var trackDestination = this.BackendHelper.TrackFile(Path.Combine(cabinetDir, mediaSymbol.Cabinet), TrackedFileType.BuiltContentOutput, mediaSymbol.SourceLineNumbers);
181 this.trackedFiles.Add(trackDestination);
182
183 var transfer = this.BackendHelper.CreateFileTransfer(resolvedCabinet.Path, trackDestination.Path, resolvedCabinet.BuildOption == CabinetBuildOption.BuildAndMove, mediaSymbol.SourceLineNumbers);
184 this.fileTransfers.Add(transfer);
185 }
186
187 return cabinetWorkItem;
188 }
189
190 /// <summary>
191 /// Gets Compiler Values of MediaTemplate Attributes governing Maximum Cabinet Size after applying Environment Variable Overrides
192 /// </summary>
193 private void GetMediaTemplateAttributes(out int maxCabSizeForLargeFileSplitting, out int maxUncompressedMediaSize)
194 {
195 var mediaTemplate = this.Section.Symbols.OfType<WixMediaTemplateSymbol>().FirstOrDefault();
196
197 // Supply Compile MediaTemplate Attributes to Cabinet Builder
198 if (mediaTemplate != null)
199 {
200 // Get Environment Variable Overrides for MediaTemplate Attributes governing Maximum Cabinet Size
201 var mcslfsString = Environment.GetEnvironmentVariable("WIX_MCSLFS");
202 var mumsString = Environment.GetEnvironmentVariable("WIX_MUMS");
203
204 // Get the Value for Max Cab Size for File Splitting
205 var maxCabSizeForLargeFileInMB = 0;
206 try
207 {
208 // Override authored mcslfs value if environment variable is authored.
209 maxCabSizeForLargeFileInMB = !String.IsNullOrEmpty(mcslfsString) ? Int32.Parse(mcslfsString) : mediaTemplate.MaximumCabinetSizeForLargeFileSplitting ?? MaxValueOfMaxCabSizeForLargeFileSplitting;
210
211 var testOverFlow = (ulong)maxCabSizeForLargeFileInMB * 1024 * 1024;
212 maxCabSizeForLargeFileSplitting = maxCabSizeForLargeFileInMB;
213 }
214 catch (FormatException)
215 {
216 throw new WixException(ErrorMessages.IllegalEnvironmentVariable("WIX_MCSLFS", mcslfsString));
217 }
218 catch (OverflowException)
219 {
220 throw new WixException(ErrorMessages.MaximumCabinetSizeForLargeFileSplittingTooLarge(null, maxCabSizeForLargeFileInMB, MaxValueOfMaxCabSizeForLargeFileSplitting));
221 }
222
223 var maxPreCompressedSizeInMB = 0;
224 try
225 {
226 // Override authored mums value if environment variable is authored.
227 maxPreCompressedSizeInMB = !String.IsNullOrEmpty(mumsString) ? Int32.Parse(mumsString) : mediaTemplate.MaximumUncompressedMediaSize ?? DefaultMaximumUncompressedMediaSize;
228
229 var testOverFlow = (ulong)maxPreCompressedSizeInMB * 1024 * 1024;
230 maxUncompressedMediaSize = maxPreCompressedSizeInMB;
231 }
232 catch (FormatException)
233 {
234 throw new WixException(ErrorMessages.IllegalEnvironmentVariable("WIX_MUMS", mumsString));
235 }
236 catch (OverflowException)
237 {
238 throw new WixException(ErrorMessages.MaximumUncompressedMediaSizeTooLarge(null, maxPreCompressedSizeInMB));
239 }
240 }
241 else
242 {
243 maxCabSizeForLargeFileSplitting = 0;
244 maxUncompressedMediaSize = DefaultMaximumUncompressedMediaSize;
245 }
246 }
247
248 private void UpdateMediaWithSpannedCabinets(IReadOnlyCollection<CompletedCabinetWorkItem> completedCabinetWorkItems)
249 {
250 var completedCabinetsSpanned = completedCabinetWorkItems.Where(c => c.CreatedCabinets.Count > 1).OrderBy(c => c.DiskId).ToList();
251
252 if (completedCabinetsSpanned.Count == 0)
253 {
254 return;
255 }
256
257 var fileTransfersByName = this.fileTransfers.ToDictionary(t => Path.GetFileName(t.Source), StringComparer.OrdinalIgnoreCase);
258 var mediaTable = this.Data.Tables["Media"];
259 var fileTable = this.Data.Tables["File"];
260 var mediaRows = mediaTable.Rows.Cast<MediaRow>().OrderBy(m => m.DiskId).ToList();
261 var fileRows = fileTable.Rows.Cast<FileRow>().OrderBy(f => f.Sequence).ToList();
262
263 var mediaRowsByOriginalDiskId = mediaRows.ToDictionary(m => m.DiskId);
264 var addedMediaRows = new List<MediaRow>();
265
266 foreach (var completedCabinetSpanned in completedCabinetsSpanned)
267 {
268 var cabinet = completedCabinetSpanned.CreatedCabinets.First();
269 var spannedCabinets = completedCabinetSpanned.CreatedCabinets.Skip(1);
270
271 if (!fileTransfersByName.TryGetValue(cabinet.CabinetName, out var transfer) ||
272 !mediaRowsByOriginalDiskId.TryGetValue(completedCabinetSpanned.DiskId, out var mediaRow))
273 {
274 throw new WixException(ErrorMessages.SplitCabinetCopyRegistrationFailed(spannedCabinets.First().CabinetName, cabinet.CabinetName));
275 }
276
277 var lastDiskId = mediaRow.DiskId;
278 var mediaRowsThatWillNeedDiskIdUpdated = mediaRows.OrderBy(m => m.DiskId).Where(m => m.DiskId > mediaRow.DiskId).ToList();
279
280 foreach (var spannedCabinet in spannedCabinets)
281 {
282 var spannedCabinetSourcePath = Path.Combine(Path.GetDirectoryName(transfer.Source), spannedCabinet.CabinetName);
283 var spannedCabinetTargetPath = Path.Combine(Path.GetDirectoryName(transfer.Destination), spannedCabinet.CabinetName);
284
285 var trackSource = this.BackendHelper.TrackFile(spannedCabinetSourcePath, TrackedFileType.Intermediate, transfer.SourceLineNumbers);
286 this.trackedFiles.Add(trackSource);
287
288 var trackTarget = this.BackendHelper.TrackFile(spannedCabinetTargetPath, TrackedFileType.BuiltContentOutput, transfer.SourceLineNumbers);
289 this.trackedFiles.Add(trackTarget);
290
291 var newTransfer = this.BackendHelper.CreateFileTransfer(trackSource.Path, trackTarget.Path, transfer.Move, transfer.SourceLineNumbers);
292 this.fileTransfers.Add(newTransfer);
293
294 // FDI Extract requires DiskID of Split Cabinets to be continuous. So a new Media row must inserted just
295 // after the previous spanned cabinet according to DiskID sort order, otherwise Windows Installer will
296 // encounter Error 2350 (FDI Server Error).
297 var newMediaRow = (MediaRow)mediaTable.CreateRow(mediaRow.SourceLineNumbers);
298 newMediaRow.Cabinet = spannedCabinet.CabinetName;
299 newMediaRow.DiskId = ++lastDiskId;
300 newMediaRow.LastSequence = mediaRow.LastSequence;
301
302 addedMediaRows.Add(newMediaRow);
303 }
304
305 // Increment the DiskId for all Media rows that come after the newly inserted row to ensure that the DiskId is unique
306 // and the Media rows stay in order based on last sequence.
307 foreach (var updateMediaRow in mediaRowsThatWillNeedDiskIdUpdated)
308 {
309 updateMediaRow.DiskId = ++lastDiskId;
310 }
311 }
312
313 mediaTable.ValidateRows();
314
315 var oldDiskIdToNewDiskId = mediaRowsByOriginalDiskId.Where(originalDiskIdWithMediaRow => originalDiskIdWithMediaRow.Value.DiskId != originalDiskIdWithMediaRow.Key)
316 .ToDictionary(originalDiskIdWithMediaRow => originalDiskIdWithMediaRow.Key, originalDiskIdWithMediaRow => originalDiskIdWithMediaRow.Value.DiskId);
317
318 // Update the File row and FileSymbols so the DiskIds are correct in the WixOutput, even if this
319 // data doesn't show up in the Windows Installer database.
320 foreach (var fileRow in fileRows)
321 {
322 if (oldDiskIdToNewDiskId.TryGetValue(fileRow.DiskId, out var newDiskId))
323 {
324 fileRow.DiskId = newDiskId;
325 }
326 }
327
328 foreach (var fileSymbol in this.Section.Symbols.OfType<FileSymbol>())
329 {
330 if (fileSymbol.DiskId.HasValue && oldDiskIdToNewDiskId.TryGetValue(fileSymbol.DiskId.Value, out var newDiskId))
331 {
332 fileSymbol.DiskId = newDiskId;
333 }
334 }
335
336 // Update the MediaSymbol DiskIds to the correct DiskId. Note that the MediaSymbol Id
337 // is not changed because symbol ids are not allowed to change after they are created.
338 foreach (var mediaSymbol in this.Section.Symbols.OfType<MediaSymbol>())
339 {
340 if (oldDiskIdToNewDiskId.TryGetValue(mediaSymbol.DiskId, out var newDiskId))
341 {
342 mediaSymbol.DiskId = newDiskId;
343 }
344 }
345
346 // Now that the existing MediaSymbol DiskIds are updated, add the newly created Media rows
347 // as symbols. Notice that the new MediaSymbols do not have an Id because they very likely
348 // would conflict with MediaSymbols that had their DiskIds updated but Ids could not be updated.
349 // The newly created MediaSymbols will rename anonymous.
350 foreach (var mediaRow in addedMediaRows)
351 {
352 this.Section.AddSymbol(new MediaSymbol(mediaRow.SourceLineNumbers)
353 {
354 Cabinet = mediaRow.Cabinet,
355 DiskId = mediaRow.DiskId,
356 DiskPrompt = mediaRow.DiskPrompt,
357 LastSequence = mediaRow.LastSequence,
358 Source = mediaRow.Source,
359 VolumeLabel = mediaRow.VolumeLabel
360 });
361 }
362 }
363 }
364 }