aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/api/wix/WixToolset.Data/WarningMessages.cs12
-rw-r--r--src/libs/dutil/WixToolset.DUtil/inc/cabcutil.h2
-rw-r--r--src/wix/WixToolset.Core.Native/Cabinet.cs26
-rw-r--r--src/wix/WixToolset.Core.Native/CabinetCreated.cs31
-rw-r--r--src/wix/WixToolset.Core.Native/WixNativeExe.cs4
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs30
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs65
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs11
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/CompletedCabinetWorkItem.cs20
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs400
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs26
-rw-r--r--src/wix/WixToolset.Core/ExtensibilityServices/FileFacade.cs5
-rw-r--r--src/wix/test/WixToolsetTest.Core.Native/CabinetFixture.cs42
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/MsiCabinetFixture.cs256
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/MsiFixture.cs125
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/MultiFileSpanningCabinets.wxs38
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/ced.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/hij.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/qrs.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/tuv.txt1
-rw-r--r--src/wix/wixnative/smartcab.cpp30
21 files changed, 688 insertions, 439 deletions
diff --git a/src/api/wix/WixToolset.Data/WarningMessages.cs b/src/api/wix/WixToolset.Data/WarningMessages.cs
index d39ead3d..6e2775bb 100644
--- a/src/api/wix/WixToolset.Data/WarningMessages.cs
+++ b/src/api/wix/WixToolset.Data/WarningMessages.cs
@@ -267,14 +267,14 @@ namespace WixToolset.Data
267 return Message(sourceLineNumbers, Ids.EmptyAttributeValue, "The {0}/@{1} attribute's value cannot be an empty string. If you want the value to be null or empty, simply remove the entire attribute.", elementName, attributeName); 267 return Message(sourceLineNumbers, Ids.EmptyAttributeValue, "The {0}/@{1} attribute's value cannot be an empty string. If you want the value to be null or empty, simply remove the entire attribute.", elementName, attributeName);
268 } 268 }
269 269
270 public static Message EmptyCabinet(SourceLineNumber sourceLineNumbers, string cabinetName)
271 {
272 return Message(sourceLineNumbers, Ids.EmptyCabinet, "The cabinet '{0}' does not contain any files. If this installation contains no files, this warning can likely be safely ignored. Otherwise, please add files to the cabinet or remove it.", cabinetName);
273 }
274
275 public static Message EmptyCabinet(SourceLineNumber sourceLineNumbers, string cabinetName, bool isPatch) 270 public static Message EmptyCabinet(SourceLineNumber sourceLineNumbers, string cabinetName, bool isPatch)
276 { 271 {
277 return Message(sourceLineNumbers, Ids.EmptyCabinet, "The cabinet '{0}' does not contain any files. If this patch contains no files, this warning can likely be safely ignored. Otherwise, try passing -p to torch.exe when first building the transforms, or add a ComponentRef to your PatchFamily authoring to pull changed files into the cabinet.", cabinetName, isPatch); 272 if (isPatch)
273 {
274 return Message(sourceLineNumbers, Ids.EmptyCabinet, "The cabinet '{0}' does not contain any files. If this patch contains no files, this warning can likely be safely ignored. Otherwise, try passing -p to torch.exe when first building the transforms, or add a ComponentRef to your PatchFamily authoring to pull changed files into the cabinet.", cabinetName, isPatch);
275 }
276
277 return Message(sourceLineNumbers, Ids.EmptyCabinet, "The cabinet '{0}' does not contain any files. If this installation contains no files, this warning can likely be safely ignored. Otherwise, please add files to the cabinet or remove it.", cabinetName);
278 } 278 }
279 279
280 public static Message ExpectedForeignRow(SourceLineNumber sourceLineNumbers, string tableName, string primaryKey, string columnName, string columnValue, string foreignTableName) 280 public static Message ExpectedForeignRow(SourceLineNumber sourceLineNumbers, string tableName, string primaryKey, string columnName, string columnValue, string foreignTableName)
diff --git a/src/libs/dutil/WixToolset.DUtil/inc/cabcutil.h b/src/libs/dutil/WixToolset.DUtil/inc/cabcutil.h
index 4f0c7b13..09784b7d 100644
--- a/src/libs/dutil/WixToolset.DUtil/inc/cabcutil.h
+++ b/src/libs/dutil/WixToolset.DUtil/inc/cabcutil.h
@@ -10,7 +10,7 @@
10// First argument is the name of splitting cabinet without extension e.g. "cab1" 10// First argument is the name of splitting cabinet without extension e.g. "cab1"
11// Second argument is name of the new cabinet that would be formed by splitting e.g. "cab1b.cab" 11// Second argument is name of the new cabinet that would be formed by splitting e.g. "cab1b.cab"
12// Third argument is the file token of the first file present in the splitting cabinet 12// Third argument is the file token of the first file present in the splitting cabinet
13typedef void (__stdcall * FileSplitCabNamesCallback)(LPWSTR, LPWSTR, LPWSTR); 13typedef void (__stdcall * FileSplitCabNamesCallback)(LPCWSTR, LPCWSTR, LPCWSTR);
14 14
15#define CAB_MAX_SIZE 0x7FFFFFFF // (see KB: Q174866) 15#define CAB_MAX_SIZE 0x7FFFFFFF // (see KB: Q174866)
16 16
diff --git a/src/wix/WixToolset.Core.Native/Cabinet.cs b/src/wix/WixToolset.Core.Native/Cabinet.cs
index e383d8fa..4d8ddb44 100644
--- a/src/wix/WixToolset.Core.Native/Cabinet.cs
+++ b/src/wix/WixToolset.Core.Native/Cabinet.cs
@@ -36,7 +36,8 @@ namespace WixToolset.Core.Native
36 /// <param name="compressionLevel">Level of compression to apply.</param> 36 /// <param name="compressionLevel">Level of compression to apply.</param>
37 /// <param name="maxSize">Maximum size of cabinet.</param> 37 /// <param name="maxSize">Maximum size of cabinet.</param>
38 /// <param name="maxThresh">Maximum threshold for each cabinet.</param> 38 /// <param name="maxThresh">Maximum threshold for each cabinet.</param>
39 public void Compress(IEnumerable<CabinetCompressFile> files, CompressionLevel compressionLevel, int maxSize = 0, int maxThresh = 0) 39 /// <returns>>List of CabinetCreated.</returns>
40 public IReadOnlyCollection<CabinetCreated> Compress(IEnumerable<CabinetCompressFile> files, CompressionLevel compressionLevel, int maxSize = 0, int maxThresh = 0)
40 { 41 {
41 var compressionLevelVariable = Environment.GetEnvironmentVariable(CompressionLevelVariable); 42 var compressionLevelVariable = Environment.GetEnvironmentVariable(CompressionLevelVariable);
42 43
@@ -56,7 +57,9 @@ namespace WixToolset.Core.Native
56 wixnative.AddStdinLine(file.ToWixNativeStdinLine()); 57 wixnative.AddStdinLine(file.ToWixNativeStdinLine());
57 } 58 }
58 59
59 wixnative.Run(); 60 var cabinetsCreated = wixnative.Run();
61
62 return ParseCreatedCabinets(cabinetsCreated);
60 } 63 }
61 64
62 /// <summary> 65 /// <summary>
@@ -103,5 +106,24 @@ namespace WixToolset.Core.Native
103 var wixnative = new WixNativeExe("extractcab", this.Path, outputFolder); 106 var wixnative = new WixNativeExe("extractcab", this.Path, outputFolder);
104 return wixnative.Run().Where(output => !String.IsNullOrWhiteSpace(output)); 107 return wixnative.Run().Where(output => !String.IsNullOrWhiteSpace(output));
105 } 108 }
109
110 private static IReadOnlyCollection<CabinetCreated> ParseCreatedCabinets(IReadOnlyCollection<string> cabinetsCreated)
111 {
112 var created = new List<CabinetCreated>();
113
114 foreach (var cabinetCreated in cabinetsCreated)
115 {
116 var data = cabinetCreated.Split(TextLineSplitter, StringSplitOptions.None);
117
118 if (data.Length != 3)
119 {
120 continue;
121 }
122
123 created.Add(new CabinetCreated(data[1], data[2]));
124 }
125
126 return created;
127 }
106 } 128 }
107} 129}
diff --git a/src/wix/WixToolset.Core.Native/CabinetCreated.cs b/src/wix/WixToolset.Core.Native/CabinetCreated.cs
new file mode 100644
index 00000000..635c862f
--- /dev/null
+++ b/src/wix/WixToolset.Core.Native/CabinetCreated.cs
@@ -0,0 +1,31 @@
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.Native
4{
5 /// <summary>
6 /// Cabinet created by compressing files.
7 /// </summary>
8 public sealed class CabinetCreated
9 {
10 /// <summary>
11 /// Constructs CabinetCreated.
12 /// </summary>
13 /// <param name="cabinetName">Name of cabinet.</param>
14 /// <param name="firstFileToken">Token of first file compressed in cabinet.</param>
15 public CabinetCreated(string cabinetName, string firstFileToken)
16 {
17 this.CabinetName = cabinetName;
18 this.FirstFileToken = firstFileToken;
19 }
20
21 /// <summary>
22 /// Gets the name of the cabinet.
23 /// </summary>
24 public string CabinetName { get; }
25
26 /// <summary>
27 /// Gets the token of the first file in the cabinet.
28 /// </summary>
29 public string FirstFileToken { get; }
30 }
31}
diff --git a/src/wix/WixToolset.Core.Native/WixNativeExe.cs b/src/wix/WixToolset.Core.Native/WixNativeExe.cs
index 90439c5d..b3b248c0 100644
--- a/src/wix/WixToolset.Core.Native/WixNativeExe.cs
+++ b/src/wix/WixToolset.Core.Native/WixNativeExe.cs
@@ -49,8 +49,8 @@ namespace WixToolset.Core.Native
49 49
50 using (var process = Process.Start(wixNativeInfo)) 50 using (var process = Process.Start(wixNativeInfo))
51 { 51 {
52 process.OutputDataReceived += (s, a) => outputLines.Add(a.Data); 52 process.OutputDataReceived += (s, a) => { if (a.Data != null) { outputLines.Add(a.Data); } };
53 process.ErrorDataReceived += (s, a) => outputLines.Add(a.Data); 53 process.ErrorDataReceived += (s, a) => { if (a.Data != null) { outputLines.Add(a.Data); } };
54 process.BeginOutputReadLine(); 54 process.BeginOutputReadLine();
55 process.BeginErrorReadLine(); 55 process.BeginErrorReadLine();
56 56
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
index 0e26bf6f..1589cc7a 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
@@ -410,7 +410,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
410 var modularize = new ModularizeCommand(this.WindowsInstallerBackendHelper, data, modularizationSuffix, section.Symbols.OfType<WixSuppressModularizationSymbol>()); 410 var modularize = new ModularizeCommand(this.WindowsInstallerBackendHelper, data, modularizationSuffix, section.Symbols.OfType<WixSuppressModularizationSymbol>());
411 modularize.Execute(); 411 modularize.Execute();
412 412
413 // Ensure all sequence tables in place because, mergemod.dll requires them. 413 // Ensure all sequence tables in place because mergemod.dll requires them.
414 var unsuppress = new AddBackSuppressedSequenceTablesCommand(data, tableDefinitions); 414 var unsuppress = new AddBackSuppressedSequenceTablesCommand(data, tableDefinitions);
415 suppressedTableNames = unsuppress.Execute(); 415 suppressedTableNames = unsuppress.Execute();
416 } 416 }
@@ -438,28 +438,12 @@ namespace WixToolset.Core.WindowsInstaller.Bind
438 command.Execute(); 438 command.Execute();
439 } 439 }
440 440
441 // create cabinet files and process uncompressed files 441 // Create cabinet files.
442 var layoutDirectory = Path.GetDirectoryName(this.OutputPath);
443 if (!this.SuppressLayout || OutputType.Module == data.Type) 442 if (!this.SuppressLayout || OutputType.Module == data.Type)
444 { 443 {
445 this.Messaging.Write(VerboseMessages.CreatingCabinetFiles()); 444 this.Messaging.Write(VerboseMessages.CreatingCabinetFiles());
446 445
447 var mediaTemplate = section.Symbols.OfType<WixMediaTemplateSymbol>().FirstOrDefault(); 446 var command = new CreateCabinetsCommand(this.ServiceProvider, this.Messaging, this.WindowsInstallerBackendHelper, this.BackendExtensions, section, this.CabCachePath, this.CabbingThreadCount, this.OutputPath, this.IntermediateFolder, this.DefaultCompressionLevel, compressed, modularizationSuffix, filesByCabinetMedia, data, tableDefinitions, this.ResolveMedia);
448
449 var command = new CreateCabinetsCommand(this.ServiceProvider, this.WindowsInstallerBackendHelper, mediaTemplate);
450 command.CabbingThreadCount = this.CabbingThreadCount;
451 command.CabCachePath = this.CabCachePath;
452 command.DefaultCompressionLevel = this.DefaultCompressionLevel;
453 command.Data = data;
454 command.Messaging = this.Messaging;
455 command.BackendExtensions = this.BackendExtensions;
456 command.LayoutDirectory = layoutDirectory;
457 command.Compressed = compressed;
458 command.ModularizationSuffix = modularizationSuffix;
459 command.FileFacadesByCabinet = filesByCabinetMedia;
460 command.ResolveMedia = this.ResolveMedia;
461 command.TableDefinitions = tableDefinitions;
462 command.IntermediateFolder = this.IntermediateFolder;
463 command.Execute(); 447 command.Execute();
464 448
465 fileTransfers.AddRange(command.FileTransfers); 449 fileTransfers.AddRange(command.FileTransfers);
@@ -523,13 +507,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
523 // Process uncompressed files. 507 // Process uncompressed files.
524 if (!this.SuppressLayout && uncompressedFiles.Any()) 508 if (!this.SuppressLayout && uncompressedFiles.Any())
525 { 509 {
526 var command = new ProcessUncompressedFilesCommand(section, this.WindowsInstallerBackendHelper, this.PathResolver); 510 var command = new ProcessUncompressedFilesCommand(section, this.WindowsInstallerBackendHelper, this.PathResolver, uncompressedFiles, this.OutputPath, compressed, longNames, this.ResolveMedia);
527 command.Compressed = compressed;
528 command.FileFacades = uncompressedFiles;
529 command.LayoutDirectory = layoutDirectory;
530 command.LongNamesInImage = longNames;
531 command.ResolveMedia = this.ResolveMedia;
532 command.DatabasePath = this.OutputPath;
533 command.Execute(); 511 command.Execute();
534 512
535 fileTransfers.AddRange(command.FileTransfers); 513 fileTransfers.AddRange(command.FileTransfers);
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs
index 49eaad42..9acbe475 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs
@@ -18,18 +18,9 @@ namespace WixToolset.Core.WindowsInstaller.Bind
18 internal sealed class CabinetBuilder 18 internal sealed class CabinetBuilder
19 { 19 {
20 private readonly Queue<CabinetWorkItem> cabinetWorkItems; 20 private readonly Queue<CabinetWorkItem> cabinetWorkItems;
21 private int threadCount; 21 private readonly List<CompletedCabinetWorkItem> completedCabinets;
22 22
23 // Address of Binder's callback function for Cabinet Splitting 23 public CabinetBuilder(IMessaging messaging, int threadCount, int maximumCabinetSizeForLargeFileSplitting, int maximumUncompressedMediaSize)
24 private readonly IntPtr newCabNamesCallBackAddress;
25
26 /// <summary>
27 /// Instantiate a new CabinetBuilder.
28 /// </summary>
29 /// <param name="messaging"></param>
30 /// <param name="threadCount">number of threads to use</param>
31 /// <param name="newCabNamesCallBackAddress">Address of Binder's callback function for Cabinet Splitting</param>
32 public CabinetBuilder(IMessaging messaging, int threadCount, IntPtr newCabNamesCallBackAddress)
33 { 24 {
34 if (0 >= threadCount) 25 if (0 >= threadCount)
35 { 26 {
@@ -37,24 +28,32 @@ namespace WixToolset.Core.WindowsInstaller.Bind
37 } 28 }
38 29
39 this.cabinetWorkItems = new Queue<CabinetWorkItem>(); 30 this.cabinetWorkItems = new Queue<CabinetWorkItem>();
40 this.Messaging = messaging; 31 this.completedCabinets = new List<CompletedCabinetWorkItem>();
41 this.threadCount = threadCount;
42 32
43 // Set Address of Binder's callback function for Cabinet Splitting 33 this.Messaging = messaging;
44 this.newCabNamesCallBackAddress = newCabNamesCallBackAddress; 34 this.ThreadCount = threadCount;
35 this.MaximumCabinetSizeForLargeFileSplitting = maximumCabinetSizeForLargeFileSplitting;
36 this.MaximumUncompressedMediaSize = maximumUncompressedMediaSize;
45 } 37 }
46 38
47 private IMessaging Messaging { get; } 39 private IMessaging Messaging { get; }
48 40
49 public int MaximumCabinetSizeForLargeFileSplitting { get; set; } 41 private int ThreadCount { get; }
50 42
51 public int MaximumUncompressedMediaSize { get; set; } 43 private int MaximumCabinetSizeForLargeFileSplitting { get; }
44
45 private int MaximumUncompressedMediaSize { get; }
46
47 public IReadOnlyCollection<CompletedCabinetWorkItem> CompletedCabinets => this.completedCabinets;
52 48
53 /// <summary> 49 /// <summary>
54 /// Enqueues a CabinetWorkItem to the queue. 50 /// Enqueues a CabinetWorkItem to the queue.
55 /// </summary> 51 /// </summary>
56 /// <param name="cabinetWorkItem">cabinet work item</param> 52 /// <param name="cabinetWorkItem">cabinet work item</param>
57 public void Enqueue(CabinetWorkItem cabinetWorkItem) => this.cabinetWorkItems.Enqueue(cabinetWorkItem); 53 public void Enqueue(CabinetWorkItem cabinetWorkItem)
54 {
55 this.cabinetWorkItems.Enqueue(cabinetWorkItem);
56 }
58 57
59 /// <summary> 58 /// <summary>
60 /// Create the queued cabinets. 59 /// Create the queued cabinets.
@@ -62,8 +61,20 @@ namespace WixToolset.Core.WindowsInstaller.Bind
62 /// <returns>error message number (zero if no error)</returns> 61 /// <returns>error message number (zero if no error)</returns>
63 public void CreateQueuedCabinets() 62 public void CreateQueuedCabinets()
64 { 63 {
64 if (this.cabinetWorkItems.Count == 0)
65 {
66 return;
67 }
68
69 var cabinetFolders = this.cabinetWorkItems.Select(c => Path.GetDirectoryName(c.CabinetFile)).Distinct(StringComparer.OrdinalIgnoreCase);
70
71 foreach (var folder in cabinetFolders)
72 {
73 Directory.CreateDirectory(folder);
74 }
75
65 // don't create more threads than the number of cabinets to build 76 // don't create more threads than the number of cabinets to build
66 var numberOfThreads = Math.Min(this.threadCount, this.cabinetWorkItems.Count); 77 var numberOfThreads = Math.Min(this.ThreadCount, this.cabinetWorkItems.Count);
67 78
68 if (0 < numberOfThreads) 79 if (0 < numberOfThreads)
69 { 80 {
@@ -107,8 +118,14 @@ namespace WixToolset.Core.WindowsInstaller.Bind
107 cabinetWorkItem = this.cabinetWorkItems.Dequeue(); 118 cabinetWorkItem = this.cabinetWorkItems.Dequeue();
108 } 119 }
109 120
110 // create a cabinet 121 // Create a cabinet.
111 this.CreateCabinet(cabinetWorkItem); 122 var created = this.CreateCabinet(cabinetWorkItem);
123
124 // Update the cabinet work item to report back what cabinets were created.
125 lock (this.completedCabinets)
126 {
127 this.completedCabinets.Add(new CompletedCabinetWorkItem(cabinetWorkItem.DiskId, created));
128 }
112 } 129 }
113 } 130 }
114 catch (WixException we) 131 catch (WixException we)
@@ -125,7 +142,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
125 /// Creates a cabinet using the wixcab.dll interop layer. 142 /// Creates a cabinet using the wixcab.dll interop layer.
126 /// </summary> 143 /// </summary>
127 /// <param name="cabinetWorkItem">CabinetWorkItem containing information about the cabinet to create.</param> 144 /// <param name="cabinetWorkItem">CabinetWorkItem containing information about the cabinet to create.</param>
128 private void CreateCabinet(CabinetWorkItem cabinetWorkItem) 145 private IReadOnlyCollection<CabinetCreated> CreateCabinet(CabinetWorkItem cabinetWorkItem)
129 { 146 {
130 this.Messaging.Write(VerboseMessages.CreateCabinet(cabinetWorkItem.CabinetFile)); 147 this.Messaging.Write(VerboseMessages.CreateCabinet(cabinetWorkItem.CabinetFile));
131 148
@@ -163,7 +180,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
163 .ToList(); 180 .ToList();
164 181
165 var cab = new Cabinet(cabinetPath); 182 var cab = new Cabinet(cabinetPath);
166 cab.Compress(files, cabinetWorkItem.CompressionLevel, maxCabinetSize, cabinetWorkItem.MaxThreshold); 183 var created = cab.Compress(files, cabinetWorkItem.CompressionLevel, maxCabinetSize, cabinetWorkItem.MaxThreshold);
167 184
168 // Best effort check to see if the cabinet is too large for the Windows Installer. 185 // Best effort check to see if the cabinet is too large for the Windows Installer.
169 try 186 try
@@ -177,6 +194,8 @@ namespace WixToolset.Core.WindowsInstaller.Bind
177 catch 194 catch
178 { 195 {
179 } 196 }
197
198 return created;
180 } 199 }
181 } 200 }
182} 201}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs
index b1a29834..12332c80 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs
@@ -15,14 +15,16 @@ namespace WixToolset.Core.WindowsInstaller.Bind
15 /// Instantiate a new CabinetWorkItem. 15 /// Instantiate a new CabinetWorkItem.
16 /// </summary> 16 /// </summary>
17 /// <param name="sourceLineNumber">Source line number that requires the cabinet creation.</param> 17 /// <param name="sourceLineNumber">Source line number that requires the cabinet creation.</param>
18 /// <param name="diskId"></param>
18 /// <param name="fileFacades">The collection of files in this cabinet.</param> 19 /// <param name="fileFacades">The collection of files in this cabinet.</param>
19 /// <param name="cabinetFile">The cabinet file.</param> 20 /// <param name="cabinetFile">The cabinet file.</param>
20 /// <param name="maxThreshold">Maximum threshold for each cabinet.</param> 21 /// <param name="maxThreshold">Maximum threshold for each cabinet.</param>
21 /// <param name="compressionLevel">The compression level of the cabinet.</param> 22 /// <param name="compressionLevel">The compression level of the cabinet.</param>
22 /// <param name="modularizationSuffix">Modularization suffix used when building a Merge Module.</param> 23 /// <param name="modularizationSuffix">Modularization suffix used when building a Merge Module.</param>
23 public CabinetWorkItem(SourceLineNumber sourceLineNumber, string cabinetFile, IEnumerable<IFileFacade> fileFacades, int maxThreshold, CompressionLevel compressionLevel, string modularizationSuffix) 24 public CabinetWorkItem(SourceLineNumber sourceLineNumber, int diskId, string cabinetFile, IEnumerable<IFileFacade> fileFacades, int maxThreshold, CompressionLevel compressionLevel, string modularizationSuffix)
24 { 25 {
25 this.SourceLineNumber = sourceLineNumber; 26 this.SourceLineNumber = sourceLineNumber;
27 this.DiskId = diskId;
26 this.CabinetFile = cabinetFile; 28 this.CabinetFile = cabinetFile;
27 this.CompressionLevel = compressionLevel; 29 this.CompressionLevel = compressionLevel;
28 this.ModularizationSuffix = modularizationSuffix; 30 this.ModularizationSuffix = modularizationSuffix;
@@ -36,6 +38,11 @@ namespace WixToolset.Core.WindowsInstaller.Bind
36 public SourceLineNumber SourceLineNumber { get; } 38 public SourceLineNumber SourceLineNumber { get; }
37 39
38 /// <summary> 40 /// <summary>
41 /// Gets the Media symbol's DiskId that requires the cabinet.
42 /// </summary>
43 public int DiskId { get; }
44
45 /// <summary>
39 /// Gets the cabinet file. 46 /// Gets the cabinet file.
40 /// </summary> 47 /// </summary>
41 /// <value>The cabinet file.</value> 48 /// <value>The cabinet file.</value>
@@ -56,7 +63,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
56 /// Gets the collection of files in this cabinet. 63 /// Gets the collection of files in this cabinet.
57 /// </summary> 64 /// </summary>
58 /// <value>The collection of files in this cabinet.</value> 65 /// <value>The collection of files in this cabinet.</value>
59 public IEnumerable<IFileFacade> FileFacades { get; } 66 public IEnumerable<IFileFacade> FileFacades { get; }
60 67
61 /// <summary> 68 /// <summary>
62 /// Gets the max threshold. 69 /// Gets the max threshold.
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CompletedCabinetWorkItem.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CompletedCabinetWorkItem.cs
new file mode 100644
index 00000000..e916bb61
--- /dev/null
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CompletedCabinetWorkItem.cs
@@ -0,0 +1,20 @@
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.Collections.Generic;
6 using WixToolset.Core.Native;
7
8 internal class CompletedCabinetWorkItem
9 {
10 public CompletedCabinetWorkItem(int diskId, IReadOnlyCollection<CabinetCreated> created)
11 {
12 this.DiskId = diskId;
13 this.CreatedCabinets = created;
14 }
15
16 public int DiskId { get; }
17
18 public IReadOnlyCollection<CabinetCreated> CreatedCabinets { get; }
19 }
20}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
index 2516a6fa..31198837 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
@@ -4,13 +4,12 @@ namespace WixToolset.Core.WindowsInstaller.Bind
4{ 4{
5 using System; 5 using System;
6 using System.Collections.Generic; 6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO; 7 using System.IO;
9 using System.Linq; 8 using System.Linq;
10 using System.Runtime.InteropServices;
11 using WixToolset.Data; 9 using WixToolset.Data;
12 using WixToolset.Data.Symbols; 10 using WixToolset.Data.Symbols;
13 using WixToolset.Data.WindowsInstaller; 11 using WixToolset.Data.WindowsInstaller;
12 using WixToolset.Data.WindowsInstaller.Rows;
14 using WixToolset.Extensibility; 13 using WixToolset.Extensibility;
15 using WixToolset.Extensibility.Data; 14 using WixToolset.Extensibility.Data;
16 using WixToolset.Extensibility.Services; 15 using WixToolset.Extensibility.Services;
@@ -20,70 +19,63 @@ namespace WixToolset.Core.WindowsInstaller.Bind
20 /// </summary> 19 /// </summary>
21 internal class CreateCabinetsCommand 20 internal class CreateCabinetsCommand
22 { 21 {
23 public const int DefaultMaximumUncompressedMediaSize = 200; // Default value is 200 MB 22 public const int DefaultMaximumUncompressedMediaSize = 200; // Default value is 200 MB
24 public const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB) 23 public const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB)
25 24
25 private readonly CabinetResolver cabinetResolver;
26 private readonly List<IFileTransfer> fileTransfers; 26 private readonly List<IFileTransfer> fileTransfers;
27
28 private readonly List<ITrackedFile> trackedFiles; 27 private readonly List<ITrackedFile> trackedFiles;
29 28
30 private readonly FileSplitCabNamesCallback newCabNamesCallBack; 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)
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 { 30 {
36 this.fileTransfers = new List<IFileTransfer>(); 31 this.Messaging = messaging;
37 32
38 this.trackedFiles = new List<ITrackedFile>(); 33 this.BackendHelper = backendHelper;
39
40 this.newCabNamesCallBack = this.NewCabNamesCallBack;
41 34
42 this.ServiceProvider = serviceProvider; 35 this.Section = section;
43 36
44 this.BackendHelper = backendHelper; 37 this.CabbingThreadCount = cabbingThreadCount;
45 38
46 this.MediaTemplate = mediaTemplate; 39 this.IntermediateFolder = intermediateFolder;
47 } 40 this.LayoutDirectory = Path.GetDirectoryName(outputPath);
48 41
49 private IServiceProvider ServiceProvider { get; } 42 this.DefaultCompressionLevel = defaultCompressionLevel;
43 this.ModularizationSuffix = modularizationSuffix;
44 this.FileFacadesByCabinet = filesByCabinetMedia;
50 45
51 private IBackendHelper BackendHelper { get; } 46 this.Data = data;
47 this.TableDefinitions = tableDefinitions;
52 48
53 private WixMediaTemplateSymbol MediaTemplate { get; } 49 this.ResolveMedia = resolveMedia;
54 50
55 /// <summary> 51 this.cabinetResolver = new CabinetResolver(serviceProvider, cabCachePath, backendExtensions);
56 /// Sets the number of threads to use for cabinet creation. 52 this.fileTransfers = new List<IFileTransfer>();
57 /// </summary> 53 this.trackedFiles = new List<ITrackedFile>();
58 public int CabbingThreadCount { private get; set; } 54 }
59 55
60 public string CabCachePath { private get; set; } 56 private IMessaging Messaging { get; }
61 57
62 public IMessaging Messaging { private get; set; } 58 private IBackendHelper BackendHelper { get; }
63 59
64 public string IntermediateFolder { private get; set; } 60 private IntermediateSection Section { get; }
65 61
66 /// <summary> 62 private int CabbingThreadCount { get; set; }
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 63
72 public IEnumerable<IWindowsInstallerBackendBinderExtension> BackendExtensions { private get; set; } 64 private string IntermediateFolder { get; }
73 65
74 public WindowsInstallerData Data { private get; set; } 66 private string LayoutDirectory { get; }
75 67
76 public string LayoutDirectory { private get; set; } 68 private CompressionLevel? DefaultCompressionLevel { get; }
77 69
78 public bool Compressed { private get; set; } 70 private string ModularizationSuffix { get; }
79 71
80 public string ModularizationSuffix { private get; set; } 72 private Dictionary<MediaSymbol, IEnumerable<IFileFacade>> FileFacadesByCabinet { get; }
81 73
82 public Dictionary<MediaSymbol, IEnumerable<IFileFacade>> FileFacadesByCabinet { private get; set; } 74 private WindowsInstallerData Data { get; }
83 75
84 public Func<MediaSymbol, string, string, string> ResolveMedia { private get; set; } 76 private TableDefinitionCollection TableDefinitions { get; }
85 77
86 public TableDefinitionCollection TableDefinitions { private get; set; } 78 private Func<MediaSymbol, string, string, string> ResolveMedia { get; }
87 79
88 public IEnumerable<IFileTransfer> FileTransfers => this.fileTransfers; 80 public IEnumerable<IFileTransfer> FileTransfers => this.fileTransfers;
89 81
@@ -91,8 +83,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
91 83
92 public void Execute() 84 public void Execute()
93 { 85 {
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. 86 // If the cabbing thread count wasn't provided, default the number of cabbing threads to the number of processors.
97 if (this.CabbingThreadCount <= 0) 87 if (this.CabbingThreadCount <= 0)
98 { 88 {
@@ -101,13 +91,9 @@ namespace WixToolset.Core.WindowsInstaller.Bind
101 this.Messaging.Write(VerboseMessages.SetCabbingThreadCount(this.CabbingThreadCount.ToString())); 91 this.Messaging.Write(VerboseMessages.SetCabbingThreadCount(this.CabbingThreadCount.ToString()));
102 } 92 }
103 93
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); 94 this.GetMediaTemplateAttributes(out var maximumCabinetSizeForLargeFileSplitting, out var maximumUncompressedMediaSize);
109 cabinetBuilder.MaximumCabinetSizeForLargeFileSplitting = maximumCabinetSizeForLargeFileSplitting; 95
110 cabinetBuilder.MaximumUncompressedMediaSize = maximumUncompressedMediaSize; 96 var cabinetBuilder = new CabinetBuilder(this.Messaging, this.CabbingThreadCount, maximumCabinetSizeForLargeFileSplitting, maximumUncompressedMediaSize);
111 97
112 foreach (var entry in this.FileFacadesByCabinet) 98 foreach (var entry in this.FileFacadesByCabinet)
113 { 99 {
@@ -129,12 +115,15 @@ namespace WixToolset.Core.WindowsInstaller.Bind
129 return; 115 return;
130 } 116 }
131 117
132 // create queued cabinets with multiple threads 118 // Create queued cabinets with multiple threads.
133 cabinetBuilder.CreateQueuedCabinets(); 119 cabinetBuilder.CreateQueuedCabinets();
120
134 if (this.Messaging.EncounteredError) 121 if (this.Messaging.EncounteredError)
135 { 122 {
136 return; 123 return;
137 } 124 }
125
126 this.UpdateMediaWithSpannedCabinets(cabinetBuilder.CompletedCabinets);
138 } 127 }
139 128
140 private int CalculateCabbingThreadCount() 129 private int CalculateCabbingThreadCount()
@@ -163,7 +152,8 @@ namespace WixToolset.Core.WindowsInstaller.Bind
163 private CabinetWorkItem CreateCabinetWorkItem(WindowsInstallerData data, string cabinetDir, MediaSymbol mediaSymbol, CompressionLevel compressionLevel, IEnumerable<IFileFacade> fileFacades) 152 private CabinetWorkItem CreateCabinetWorkItem(WindowsInstallerData data, string cabinetDir, MediaSymbol mediaSymbol, CompressionLevel compressionLevel, IEnumerable<IFileFacade> fileFacades)
164 { 153 {
165 CabinetWorkItem cabinetWorkItem = null; 154 CabinetWorkItem cabinetWorkItem = null;
166 var tempCabinetFileX = Path.Combine(this.IntermediateFolder, mediaSymbol.Cabinet); 155
156 var intermediateCabinetPath = Path.Combine(this.IntermediateFolder, mediaSymbol.Cabinet);
167 157
168 // check for an empty cabinet 158 // check for an empty cabinet
169 if (!fileFacades.Any()) 159 if (!fileFacades.Any())
@@ -172,25 +162,16 @@ namespace WixToolset.Core.WindowsInstaller.Bind
172 var cabinetName = mediaSymbol.Cabinet.TrimStart('#'); 162 var cabinetName = mediaSymbol.Cabinet.TrimStart('#');
173 163
174 // If building a patch, remind them to run -p for torch. 164 // If building a patch, remind them to run -p for torch.
175 if (OutputType.Patch == data.Type) 165 this.Messaging.Write(WarningMessages.EmptyCabinet(mediaSymbol.SourceLineNumbers, cabinetName, 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 } 166 }
184 167
185 var cabinetResolver = new CabinetResolver(this.ServiceProvider, this.CabCachePath, this.BackendExtensions); 168 var resolvedCabinet = this.cabinetResolver.ResolveCabinet(intermediateCabinetPath, fileFacades);
186
187 var resolvedCabinet = cabinetResolver.ResolveCabinet(tempCabinetFileX, fileFacades);
188 169
189 // create a cabinet work item if it's not being skipped 170 // Create a cabinet work item if it's not being skipped.
190 if (CabinetBuildOption.BuildAndCopy == resolvedCabinet.BuildOption || CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption) 171 if (CabinetBuildOption.BuildAndCopy == resolvedCabinet.BuildOption || CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption)
191 { 172 {
192 // Default to the threshold for best smartcabbing (makes smallest cabinet). 173 // Default to the threshold for best smartcabbing (makes smallest cabinet).
193 cabinetWorkItem = new CabinetWorkItem(mediaSymbol.SourceLineNumbers, resolvedCabinet.Path, fileFacades, maxThreshold: 0, compressionLevel: compressionLevel, modularizationSuffix: this.ModularizationSuffix); 174 cabinetWorkItem = new CabinetWorkItem(mediaSymbol.SourceLineNumbers, mediaSymbol.DiskId, resolvedCabinet.Path, fileFacades, maxThreshold: 0, compressionLevel: compressionLevel, modularizationSuffix: this.ModularizationSuffix);
194 } 175 }
195 else // reuse the cabinet from the cabinet cache. 176 else // reuse the cabinet from the cabinet cache.
196 { 177 {
@@ -235,185 +216,26 @@ namespace WixToolset.Core.WindowsInstaller.Bind
235 return cabinetWorkItem; 216 return cabinetWorkItem;
236 } 217 }
237 218
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> 219 /// <summary>
400 /// Gets Compiler Values of MediaTemplate Attributes governing Maximum Cabinet Size after applying Environment Variable Overrides 220 /// Gets Compiler Values of MediaTemplate Attributes governing Maximum Cabinet Size after applying Environment Variable Overrides
401 /// </summary> 221 /// </summary>
402 private void GetMediaTemplateAttributes(out int maxCabSizeForLargeFileSplitting, out int maxUncompressedMediaSize) 222 private void GetMediaTemplateAttributes(out int maxCabSizeForLargeFileSplitting, out int maxUncompressedMediaSize)
403 { 223 {
404 // Get Environment Variable Overrides for MediaTemplate Attributes governing Maximum Cabinet Size 224 var mediaTemplate = this.Section.Symbols.OfType<WixMediaTemplateSymbol>().FirstOrDefault();
405 var mcslfsString = Environment.GetEnvironmentVariable("WIX_MCSLFS");
406 var mumsString = Environment.GetEnvironmentVariable("WIX_MUMS");
407 225
408 // Supply Compile MediaTemplate Attributes to Cabinet Builder 226 // Supply Compile MediaTemplate Attributes to Cabinet Builder
409 if (this.MediaTemplate != null) 227 if (mediaTemplate != null)
410 { 228 {
229 // Get Environment Variable Overrides for MediaTemplate Attributes governing Maximum Cabinet Size
230 var mcslfsString = Environment.GetEnvironmentVariable("WIX_MCSLFS");
231 var mumsString = Environment.GetEnvironmentVariable("WIX_MUMS");
232
411 // Get the Value for Max Cab Size for File Splitting 233 // Get the Value for Max Cab Size for File Splitting
412 var maxCabSizeForLargeFileInMB = 0; 234 var maxCabSizeForLargeFileInMB = 0;
413 try 235 try
414 { 236 {
415 // Override authored mcslfs value if environment variable is authored. 237 // Override authored mcslfs value if environment variable is authored.
416 maxCabSizeForLargeFileInMB = !String.IsNullOrEmpty(mcslfsString) ? Int32.Parse(mcslfsString) : this.MediaTemplate.MaximumCabinetSizeForLargeFileSplitting ?? MaxValueOfMaxCabSizeForLargeFileSplitting; 238 maxCabSizeForLargeFileInMB = !String.IsNullOrEmpty(mcslfsString) ? Int32.Parse(mcslfsString) : mediaTemplate.MaximumCabinetSizeForLargeFileSplitting ?? MaxValueOfMaxCabSizeForLargeFileSplitting;
417 239
418 var testOverFlow = (ulong)maxCabSizeForLargeFileInMB * 1024 * 1024; 240 var testOverFlow = (ulong)maxCabSizeForLargeFileInMB * 1024 * 1024;
419 maxCabSizeForLargeFileSplitting = maxCabSizeForLargeFileInMB; 241 maxCabSizeForLargeFileSplitting = maxCabSizeForLargeFileInMB;
@@ -431,7 +253,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
431 try 253 try
432 { 254 {
433 // Override authored mums value if environment variable is authored. 255 // Override authored mums value if environment variable is authored.
434 maxPreCompressedSizeInMB = !String.IsNullOrEmpty(mumsString) ? Int32.Parse(mumsString) : this.MediaTemplate.MaximumUncompressedMediaSize ?? DefaultMaximumUncompressedMediaSize; 256 maxPreCompressedSizeInMB = !String.IsNullOrEmpty(mumsString) ? Int32.Parse(mumsString) : mediaTemplate.MaximumUncompressedMediaSize ?? DefaultMaximumUncompressedMediaSize;
435 257
436 var testOverFlow = (ulong)maxPreCompressedSizeInMB * 1024 * 1024; 258 var testOverFlow = (ulong)maxPreCompressedSizeInMB * 1024 * 1024;
437 maxUncompressedMediaSize = maxPreCompressedSizeInMB; 259 maxUncompressedMediaSize = maxPreCompressedSizeInMB;
@@ -451,5 +273,121 @@ namespace WixToolset.Core.WindowsInstaller.Bind
451 maxUncompressedMediaSize = DefaultMaximumUncompressedMediaSize; 273 maxUncompressedMediaSize = DefaultMaximumUncompressedMediaSize;
452 } 274 }
453 } 275 }
276
277 private void UpdateMediaWithSpannedCabinets(IReadOnlyCollection<CompletedCabinetWorkItem> completedCabinetWorkItems)
278 {
279 var completedCabinetsSpanned = completedCabinetWorkItems.Where(c => c.CreatedCabinets.Count > 1).OrderBy(c => c.DiskId).ToList();
280
281 if (completedCabinetsSpanned.Count == 0)
282 {
283 return;
284 }
285
286 var fileTransfersByName = this.fileTransfers.ToDictionary(t => Path.GetFileName(t.Source), StringComparer.OrdinalIgnoreCase);
287 var mediaTable = this.Data.Tables["Media"];
288 var fileTable = this.Data.Tables["File"];
289 var mediaRows = mediaTable.Rows.Cast<MediaRow>().OrderBy(m => m.DiskId).ToList();
290 var fileRows = fileTable.Rows.Cast<FileRow>().OrderBy(f => f.Sequence).ToList();
291
292 var mediaRowsByOriginalDiskId = mediaRows.ToDictionary(m => m.DiskId);
293 var addedMediaRows = new List<MediaRow>();
294
295 foreach (var completedCabinetSpanned in completedCabinetsSpanned)
296 {
297 var cabinet = completedCabinetSpanned.CreatedCabinets.First();
298 var spannedCabinets = completedCabinetSpanned.CreatedCabinets.Skip(1);
299
300 if (!fileTransfersByName.TryGetValue(cabinet.CabinetName, out var transfer) ||
301 !mediaRowsByOriginalDiskId.TryGetValue(completedCabinetSpanned.DiskId, out var mediaRow))
302 {
303 throw new WixException(ErrorMessages.SplitCabinetCopyRegistrationFailed(spannedCabinets.First().CabinetName, cabinet.CabinetName));
304 }
305
306 var lastDiskId = mediaRow.DiskId;
307 var mediaRowsThatWillNeedDiskIdUpdated = mediaRows.OrderBy(m => m.DiskId).Where(m => m.DiskId > mediaRow.DiskId).ToList();
308
309 foreach (var spannedCabinet in spannedCabinets)
310 {
311 var spannedCabinetSourcePath = Path.Combine(Path.GetDirectoryName(transfer.Source), spannedCabinet.CabinetName);
312 var spannedCabinetTargetPath = Path.Combine(Path.GetDirectoryName(transfer.Destination), spannedCabinet.CabinetName);
313
314 var trackSource = this.BackendHelper.TrackFile(spannedCabinetSourcePath, TrackedFileType.Intermediate, transfer.SourceLineNumbers);
315 this.trackedFiles.Add(trackSource);
316
317 var trackTarget = this.BackendHelper.TrackFile(spannedCabinetTargetPath, TrackedFileType.BuiltOutput, transfer.SourceLineNumbers);
318 this.trackedFiles.Add(trackTarget);
319
320 var newTransfer = this.BackendHelper.CreateFileTransfer(trackSource.Path, trackTarget.Path, transfer.Move, transfer.SourceLineNumbers);
321 this.fileTransfers.Add(newTransfer);
322
323 // FDI Extract requires DiskID of Split Cabinets to be continuous. So a new Media row must inserted just
324 // after the previous spanned cabinet according to DiskID sort order, otherwise Windows Installer will
325 // encounter Error 2350 (FDI Server Error).
326 var newMediaRow = (MediaRow)mediaTable.CreateRow(mediaRow.SourceLineNumbers);
327 newMediaRow.Cabinet = spannedCabinet.CabinetName;
328 newMediaRow.DiskId = ++lastDiskId;
329 newMediaRow.LastSequence = mediaRow.LastSequence;
330
331 addedMediaRows.Add(newMediaRow);
332 }
333
334 // Increment the DiskId for all Media rows that come after the newly inserted row to ensure that the DiskId is unique
335 // and the Media rows stay in order based on last sequence.
336 foreach (var updateMediaRow in mediaRowsThatWillNeedDiskIdUpdated)
337 {
338 updateMediaRow.DiskId = ++lastDiskId;
339 }
340 }
341
342 mediaTable.ValidateRows();
343
344 var oldDiskIdToNewDiskId = mediaRowsByOriginalDiskId.Where(originalDiskIdWithMediaRow => originalDiskIdWithMediaRow.Value.DiskId != originalDiskIdWithMediaRow.Key)
345 .ToDictionary(originalDiskIdWithMediaRow => originalDiskIdWithMediaRow.Key, originalDiskIdWithMediaRow => originalDiskIdWithMediaRow.Value.DiskId);
346
347 // Update the File row and FileSymbols so the DiskIds are correct in the WixOutput, even if this
348 // data doesn't show up in the Windows Installer database.
349 foreach (var fileRow in fileRows)
350 {
351 if (oldDiskIdToNewDiskId.TryGetValue(fileRow.DiskId, out var newDiskId))
352 {
353 fileRow.DiskId = newDiskId;
354 }
355 }
356
357 foreach (var fileSymbol in this.Section.Symbols.OfType<FileSymbol>())
358 {
359 if (fileSymbol.DiskId.HasValue && oldDiskIdToNewDiskId.TryGetValue(fileSymbol.DiskId.Value, out var newDiskId))
360 {
361 fileSymbol.DiskId = newDiskId;
362 }
363 }
364
365 // Update the MediaSymbol DiskIds to the correct DiskId. Note that the MediaSymbol Id
366 // is not changed because symbol ids are not allowed to change after they are created.
367 foreach (var mediaSymbol in this.Section.Symbols.OfType<MediaSymbol>())
368 {
369 if (oldDiskIdToNewDiskId.TryGetValue(mediaSymbol.DiskId, out var newDiskId))
370 {
371 mediaSymbol.DiskId = newDiskId;
372 }
373 }
374
375 // Now that the existing MediaSymbol DiskIds are updated, add the newly created Media rows
376 // as symbols. Notice that the new MediaSymbols do not have an Id because they very likely
377 // would conflict with MediaSymbols that had their DiskIds updated but Ids could not be updated.
378 // The newly created MediaSymbols will rename anonymous.
379 foreach (var mediaRow in addedMediaRows)
380 {
381 this.Section.AddSymbol(new MediaSymbol(mediaRow.SourceLineNumbers)
382 {
383 Cabinet = mediaRow.Cabinet,
384 DiskId = mediaRow.DiskId,
385 DiskPrompt = mediaRow.DiskPrompt,
386 LastSequence = mediaRow.LastSequence,
387 Source = mediaRow.Source,
388 VolumeLabel = mediaRow.VolumeLabel
389 });
390 }
391 }
454 } 392 }
455} 393}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs
index 9aad3537..6662f8f7 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs
@@ -17,30 +17,38 @@ namespace WixToolset.Core.WindowsInstaller.Bind
17 /// </summary> 17 /// </summary>
18 internal class ProcessUncompressedFilesCommand 18 internal class ProcessUncompressedFilesCommand
19 { 19 {
20 public ProcessUncompressedFilesCommand(IntermediateSection section, IBackendHelper backendHelper, IPathResolver pathResolver) 20 public ProcessUncompressedFilesCommand(IntermediateSection section, IBackendHelper backendHelper, IPathResolver pathResolver, IEnumerable<IFileFacade> fileFacades, string outputPath, bool compressed, bool longNamesInImage, Func<MediaSymbol, string, string, string> resolveMedia)
21 { 21 {
22 this.Section = section; 22 this.Section = section;
23 this.BackendHelper = backendHelper; 23 this.BackendHelper = backendHelper;
24 this.PathResolver = pathResolver; 24 this.PathResolver = pathResolver;
25
26 this.DatabasePath = outputPath;
27 this.LayoutDirectory = Path.GetDirectoryName(outputPath);
28 this.Compressed = compressed;
29 this.LongNamesInImage = longNamesInImage;
30
31 this.FileFacades = fileFacades;
32 this.ResolveMedia = resolveMedia;
25 } 33 }
26 34
27 private IntermediateSection Section { get; } 35 private IntermediateSection Section { get; }
28 36
29 public IBackendHelper BackendHelper { get; } 37 private IBackendHelper BackendHelper { get; }
30 38
31 public IPathResolver PathResolver { get; } 39 private IPathResolver PathResolver { get; }
32 40
33 public string DatabasePath { private get; set; } 41 private string DatabasePath { get; }
34 42
35 public IEnumerable<IFileFacade> FileFacades { private get; set; } 43 private string LayoutDirectory { get; }
36 44
37 public string LayoutDirectory { private get; set; } 45 private bool Compressed { get; }
38 46
39 public bool Compressed { private get; set; } 47 private bool LongNamesInImage { get; }
40 48
41 public bool LongNamesInImage { private get; set; } 49 private IEnumerable<IFileFacade> FileFacades { get; }
42 50
43 public Func<MediaSymbol, string, string, string> ResolveMedia { private get; set; } 51 private Func<MediaSymbol, string, string, string> ResolveMedia { get; }
44 52
45 public IEnumerable<IFileTransfer> FileTransfers { get; private set; } 53 public IEnumerable<IFileTransfer> FileTransfers { get; private set; }
46 54
diff --git a/src/wix/WixToolset.Core/ExtensibilityServices/FileFacade.cs b/src/wix/WixToolset.Core/ExtensibilityServices/FileFacade.cs
index f85d4842..65043658 100644
--- a/src/wix/WixToolset.Core/ExtensibilityServices/FileFacade.cs
+++ b/src/wix/WixToolset.Core/ExtensibilityServices/FileFacade.cs
@@ -167,6 +167,9 @@ namespace WixToolset.Core.ExtensibilityServices
167 /// <summary> 167 /// <summary>
168 /// Allows direct access to the underlying FileRow as requried for patching. 168 /// Allows direct access to the underlying FileRow as requried for patching.
169 /// </summary> 169 /// </summary>
170 public FileRow GetFileRow() => this.FileRow ?? throw new NotImplementedException(); 170 public FileRow GetFileRow()
171 {
172 return this.FileRow ?? throw new NotImplementedException();
173 }
171 } 174 }
172} 175}
diff --git a/src/wix/test/WixToolsetTest.Core.Native/CabinetFixture.cs b/src/wix/test/WixToolsetTest.Core.Native/CabinetFixture.cs
index c566339a..e1189549 100644
--- a/src/wix/test/WixToolsetTest.Core.Native/CabinetFixture.cs
+++ b/src/wix/test/WixToolsetTest.Core.Native/CabinetFixture.cs
@@ -2,6 +2,7 @@
2 2
3namespace WixToolsetTest.CoreNative 3namespace WixToolsetTest.CoreNative
4{ 4{
5 using System;
5 using System.IO; 6 using System.IO;
6 using System.Linq; 7 using System.Linq;
7 using WixBuildTools.TestSupport; 8 using WixBuildTools.TestSupport;
@@ -19,12 +20,45 @@ namespace WixToolsetTest.CoreNative
19 var intermediateFolder = fs.GetFolder(true); 20 var intermediateFolder = fs.GetFolder(true);
20 var cabPath = Path.Combine(intermediateFolder, "testout.cab"); 21 var cabPath = Path.Combine(intermediateFolder, "testout.cab");
21 22
22 var files = new[] { new CabinetCompressFile(TestData.Get(@"TestData\test.txt"), "test.txt") }; 23 var files = new[] { new CabinetCompressFile(TestData.Get(@"TestData", "test.txt"), "test.txt") };
23 24
24 var cabinet = new Cabinet(cabPath); 25 var cabinet = new Cabinet(cabPath);
25 cabinet.Compress(files, CompressionLevel.Low); 26 var created = cabinet.Compress(files, CompressionLevel.Low);
26 27
27 Assert.True(File.Exists(cabPath)); 28 Assert.True(File.Exists(cabPath));
29 Assert.Equal(new[]
30 {
31 "testout.cab, test.txt"
32 }, created.Select(c => String.Join(", ", c.CabinetName, c.FirstFileToken)).ToArray());
33 }
34 }
35
36 [Fact]
37 public void CanCreateSpannedFileCabinet()
38 {
39 using (var fs = new DisposableFileSystem())
40 {
41 var intermediateFolder = fs.GetFolder(true);
42
43 // Put more than non-zero bytes in a file sized just under 3MB since there is
44 // some overhead in cabinets that prevent perfect packing on the megabyte boundary.
45 var threeMBPath = Path.Combine(intermediateFolder, "_3mb.dat");
46 TestData.CreateFile(threeMBPath, (long)(2.9 * 1024 * 1024), fill: true);
47
48 var cabPath = Path.Combine(intermediateFolder, "test.cab");
49
50 var files = new[] { new CabinetCompressFile(threeMBPath, Path.GetFileNameWithoutExtension(threeMBPath)) };
51
52 var cabinet = new Cabinet(cabPath);
53 var created = cabinet.Compress(files, CompressionLevel.None, maxSize: 1);
54
55 Assert.True(File.Exists(cabPath));
56 Assert.Equal(new[]
57 {
58 "test.cab, _3mb",
59 "testa.cab, _3mb",
60 "testb.cab, _3mb"
61 }, created.Select(c => String.Join(", ", c.CabinetName, c.FirstFileToken)).ToArray());
28 } 62 }
29 } 63 }
30 64
@@ -58,8 +92,8 @@ namespace WixToolsetTest.CoreNative
58 // Compress. 92 // Compress.
59 { 93 {
60 var files = new[] { 94 var files = new[] {
61 new CabinetCompressFile(TestData.Get(@"TestData\test.txt"), "test1.txt"), 95 new CabinetCompressFile(TestData.Get(@"TestData", "test.txt"), "test1.txt"),
62 new CabinetCompressFile(TestData.Get(@"TestData\test.txt"), "test2.txt"), 96 new CabinetCompressFile(TestData.Get(@"TestData", "test.txt"), "test2.txt"),
63 }; 97 };
64 98
65 var cabinet = new Cabinet(cabinetPath); 99 var cabinet = new Cabinet(cabinetPath);
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/MsiCabinetFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/MsiCabinetFixture.cs
new file mode 100644
index 00000000..8bb790d4
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/MsiCabinetFixture.cs
@@ -0,0 +1,256 @@
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 WixToolsetTest.CoreIntegration
4{
5 using System;
6 using System.IO;
7 using System.Linq;
8 using WixBuildTools.TestSupport;
9 using WixToolset.Core.TestPackage;
10 using WixToolset.Data;
11 using WixToolset.Data.Symbols;
12 using WixToolset.Data.WindowsInstaller;
13 using WixToolset.Data.WindowsInstaller.Rows;
14 using Xunit;
15
16 public class MsiCabinetFixture
17 {
18 [Fact]
19 public void CanBuildSingleFileCompressed()
20 {
21 var folder = TestData.Get(@"TestData\SingleFileCompressed");
22
23 using (var fs = new DisposableFileSystem())
24 {
25 var intermediateFolder = fs.GetFolder();
26
27 var result = WixRunner.Execute(new[]
28 {
29 "build",
30 Path.Combine(folder, "Package.wxs"),
31 Path.Combine(folder, "PackageComponents.wxs"),
32 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
33 "-bindpath", Path.Combine(folder, "data"),
34 "-intermediateFolder", intermediateFolder,
35 "-o", Path.Combine(intermediateFolder, @"bin\test.msi")
36 });
37
38 result.AssertSuccess();
39
40 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.msi")));
41 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\example.cab")));
42 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.wixpdb")));
43
44 var intermediate = Intermediate.Load(Path.Combine(intermediateFolder, @"bin\test.wixpdb"));
45 var section = intermediate.Sections.Single();
46
47 var fileSymbol = section.Symbols.OfType<FileSymbol>().Single();
48 WixAssert.StringEqual(Path.Combine(folder, @"data\test.txt"), fileSymbol[FileSymbolFields.Source].AsPath().Path);
49 WixAssert.StringEqual(@"test.txt", fileSymbol[FileSymbolFields.Source].PreviousValue.AsPath().Path);
50 }
51 }
52
53 [Fact]
54 public void CanBuildSingleFileCompressedWithMediaTemplate()
55 {
56 var folder = TestData.Get(@"TestData\SingleFileCompressed");
57
58 using (var fs = new DisposableFileSystem())
59 {
60 var intermediateFolder = fs.GetFolder();
61
62 var result = WixRunner.Execute(new[]
63 {
64 "build",
65 Path.Combine(folder, "Package.wxs"),
66 Path.Combine(folder, "PackageComponents.wxs"),
67 "-d", "MediaTemplateCompressionLevel",
68 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
69 "-bindpath", Path.Combine(folder, "data"),
70 "-intermediateFolder", intermediateFolder,
71 "-o", Path.Combine(intermediateFolder, @"bin\test.msi")
72 });
73
74 result.AssertSuccess();
75
76 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.msi")));
77 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\cab1.cab")));
78 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.wixpdb")));
79 }
80 }
81
82 [Fact]
83 public void CanBuildSingleFileCompressedWithMediaTemplateWithLowCompression()
84 {
85 var folder = TestData.Get(@"TestData\SingleFileCompressed");
86
87 using (var fs = new DisposableFileSystem())
88 {
89 var intermediateFolder = fs.GetFolder();
90
91 var result = WixRunner.Execute(new[]
92 {
93 "build",
94 Path.Combine(folder, "Package.wxs"),
95 Path.Combine(folder, "PackageComponents.wxs"),
96 "-d", "MediaTemplateCompressionLevel=low",
97 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
98 "-bindpath", Path.Combine(folder, "data"),
99 "-intermediateFolder", intermediateFolder,
100 "-o", Path.Combine(intermediateFolder, @"bin\test.msi")
101 });
102
103 result.AssertSuccess();
104
105 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.msi")));
106 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\low1.cab")));
107 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.wixpdb")));
108 }
109 }
110
111 [Fact]
112 public void CanBuildMultipleFilesCompressed()
113 {
114 var folder = TestData.Get(@"TestData\MultiFileCompressed");
115
116 using (var fs = new DisposableFileSystem())
117 {
118 var intermediateFolder = fs.GetFolder();
119
120 var result = WixRunner.Execute(new[]
121 {
122 "build",
123 "-sw1079", // TODO: why does this test need to create a second cab which is empty?
124 Path.Combine(folder, "Package.wxs"),
125 Path.Combine(folder, "PackageComponents.wxs"),
126 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
127 "-bindpath", Path.Combine(folder, "data"),
128 "-intermediateFolder", intermediateFolder,
129 "-o", Path.Combine(intermediateFolder, @"bin\test.msi")
130 });
131
132 result.AssertSuccess();
133
134 Assert.True(File.Exists(Path.Combine(intermediateFolder, "bin", "test.msi")));
135 Assert.True(File.Exists(Path.Combine(intermediateFolder, "bin", "example1.cab")));
136 Assert.True(File.Exists(Path.Combine(intermediateFolder, "bin", "example2.cab")));
137 Assert.True(File.Exists(Path.Combine(intermediateFolder, "bin", "test.wixpdb")));
138 }
139 }
140
141 [Fact]
142 public void CanBuildMultipleFilesSpanningCabinets()
143 {
144 var folder = TestData.Get(@"TestData", "MsiCabinet");
145
146 using (var fs = new DisposableFileSystem())
147 {
148 var baseFolder = fs.GetFolder();
149 var dataFolder = Path.Combine(folder, "data");
150 var gendataFolder = Path.Combine(baseFolder, "generated-data");
151 var cabFolder = Path.Combine(baseFolder, "cab");
152 var intermediateFolder = Path.Combine(baseFolder, "obj");
153 var binFolder = Path.Combine(baseFolder, "bin");
154 var msiPath = Path.Combine(binFolder, "test.msi");
155 var wixpdbPath = Path.ChangeExtension(msiPath, "wixpdb");
156
157 TestData.CreateFile(Path.Combine(gendataFolder, "abc.gen"), (long)(25 * 1024 * 1024), fill: true);
158 TestData.CreateFile(Path.Combine(gendataFolder, "mno.gen"), (long)(45 * 1024 * 1024), fill: true);
159 TestData.CreateFile(Path.Combine(gendataFolder, "xyz.gen"), (long)(25 * 1024 * 1024), fill: true);
160
161 var result = WixRunner.Execute(new[]
162 {
163 "build",
164 Path.Combine(folder, "MultiFileSpanningCabinets.wxs"),
165 "-bindpath", dataFolder,
166 "-bindpath", gendataFolder,
167 "-intermediateFolder", intermediateFolder,
168 "-cc", cabFolder,
169 "-o", msiPath
170 });
171
172 result.AssertSuccess();
173
174 var files = Directory.GetFiles(binFolder, "*.*", SearchOption.AllDirectories).Select(s => s.Substring(binFolder.Length + 1)).OrderBy(s => s).ToArray();
175 WixAssert.CompareLineByLine(new[]
176 {
177 "cab1.cab",
178 "cab1a.cab",
179 "cab2.cab",
180 "cab3.cab",
181 "cab3a.cab",
182 "cab3b.cab",
183 "cab4.cab",
184 "cab5.cab",
185 "cab5a.cab",
186 "test.msi",
187 "test.wixpdb"
188 }, files);
189
190 var query = Query.QueryDatabase(msiPath, new[] { "Media", "File" });
191 WixAssert.CompareLineByLine(new[]
192 {
193 "File:fil2WOk5jeIBsvmL0db5z96JfeEZoU\tfil2WOk5jeIBsvmL0db5z96JfeEZoU\thij.txt\t18\t\t\t512\t3",
194 "File:filgErUV04C8ZBKWWWA0Zg5Fu_6NyM\tfilgErUV04C8ZBKWWWA0Zg5Fu_6NyM\tmno.gen\t47185920\t\t\t512\t4",
195 "File:filGqbzmUDXsQhijNpBL2rNX3dtCoo\tfilGqbzmUDXsQhijNpBL2rNX3dtCoo\tced.txt\t18\t\t\t512\t2",
196 "File:filj2uZN0Q5bCgR2YY6RRg1c9FqylQ\tfilj2uZN0Q5bCgR2YY6RRg1c9FqylQ\ttuv.txt\t18\t\t\t512\t6",
197 "File:filjEKfcIaBHXFyDRtZciPv4j105jQ\tfiljEKfcIaBHXFyDRtZciPv4j105jQ\tqrs.txt\t18\t\t\t512\t5",
198 "File:filKv93aSvdbL6M6UutiKdGim1UzcA\tfilKv93aSvdbL6M6UutiKdGim1UzcA\tabc.gen\t26214400\t\t\t512\t1",
199 "File:fillf1kS2G7fDKwbyrQIIw1OXPi.eY\tfillf1kS2G7fDKwbyrQIIw1OXPi.eY\txyz.gen\t26214400\t\t\t512\t7",
200 "Media:1\t1\t\tcab1.cab\t\t",
201 "Media:2\t1\t\tcab1a.cab\t\t",
202 "Media:3\t3\t\tcab2.cab\t\t",
203 "Media:4\t4\t\tcab3.cab\t\t",
204 "Media:5\t4\t\tcab3a.cab\t\t",
205 "Media:6\t4\t\tcab3b.cab\t\t",
206 "Media:7\t6\t\tcab4.cab\t\t",
207 "Media:8\t7\t\tcab5.cab\t\t",
208 "Media:9\t7\t\tcab5a.cab\t\t",
209 }, query);
210
211 var wixpdb = WixOutput.Read(wixpdbPath);
212
213 var data = WindowsInstallerData.Load(wixpdb);
214 var fileRows = data.Tables["File"].Rows.Cast<FileRow>().OrderBy(f => f.Sequence);
215 var mediaRows = data.Tables["Media"].Rows.Cast<MediaRow>().OrderBy(f => f.DiskId);
216
217 var intermediate = Intermediate.Load(wixpdb);
218 var section = intermediate.Sections.Single();
219 var fileSymbols = section.Symbols.OfType<FileSymbol>().OrderBy(f => f.Sequence);
220 var mediaSymbols = section.Symbols.OfType<MediaSymbol>().OrderBy(f => f.DiskId);
221
222 var expectedFiles = new[]
223 {
224 "filKv93aSvdbL6M6UutiKdGim1UzcA abc.gen 1 1",
225 "filGqbzmUDXsQhijNpBL2rNX3dtCoo ced.txt 2 3",
226 "fil2WOk5jeIBsvmL0db5z96JfeEZoU hij.txt 3 3",
227 "filgErUV04C8ZBKWWWA0Zg5Fu_6NyM mno.gen 4 4",
228 "filjEKfcIaBHXFyDRtZciPv4j105jQ qrs.txt 5 7",
229 "filj2uZN0Q5bCgR2YY6RRg1c9FqylQ tuv.txt 6 7",
230 "fillf1kS2G7fDKwbyrQIIw1OXPi.eY xyz.gen 7 8",
231 };
232
233 var expectedMedia = new[]
234 {
235 "1 cab1.cab 1",
236 "2 cab1a.cab 1",
237 "3 cab2.cab 3",
238 "4 cab3.cab 4",
239 "5 cab3a.cab 4",
240 "6 cab3b.cab 4",
241 "7 cab4.cab 6",
242 "8 cab5.cab 7",
243 "9 cab5a.cab 7",
244 };
245
246 WixAssert.CompareLineByLine(expectedFiles, fileRows.Select(r => String.Join(" ", r.File, r.FileName, r.Sequence, r.DiskId)).ToArray());
247
248 WixAssert.CompareLineByLine(expectedFiles, fileSymbols.Select(s => String.Join(" ", s.Id.Id, s.Name, s.Sequence, s.DiskId)).ToArray());
249
250 WixAssert.CompareLineByLine(expectedMedia, mediaRows.Select(r => String.Join(" ", r.DiskId, r.Cabinet, r.LastSequence)).ToArray());
251
252 WixAssert.CompareLineByLine(expectedMedia, mediaSymbols.Select(s => String.Join(" ", s.DiskId, s.Cabinet, s.LastSequence)).ToArray());
253 }
254 }
255 }
256}
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/MsiFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/MsiFixture.cs
index 0c01fa5e..60e9653a 100644
--- a/src/wix/test/WixToolsetTest.CoreIntegration/MsiFixture.cs
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/MsiFixture.cs
@@ -58,130 +58,7 @@ namespace WixToolsetTest.CoreIntegration
58 } 58 }
59 59
60 [Fact] 60 [Fact]
61 public void CanBuildSingleFileCompressed() 61 public void CannotBuildMissingFile()
62 {
63 var folder = TestData.Get(@"TestData\SingleFileCompressed");
64
65 using (var fs = new DisposableFileSystem())
66 {
67 var intermediateFolder = fs.GetFolder();
68
69 var result = WixRunner.Execute(new[]
70 {
71 "build",
72 Path.Combine(folder, "Package.wxs"),
73 Path.Combine(folder, "PackageComponents.wxs"),
74 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
75 "-bindpath", Path.Combine(folder, "data"),
76 "-intermediateFolder", intermediateFolder,
77 "-o", Path.Combine(intermediateFolder, @"bin\test.msi")
78 });
79
80 result.AssertSuccess();
81
82 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.msi")));
83 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\example.cab")));
84 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.wixpdb")));
85
86 var intermediate = Intermediate.Load(Path.Combine(intermediateFolder, @"bin\test.wixpdb"));
87 var section = intermediate.Sections.Single();
88
89 var fileSymbol = section.Symbols.OfType<FileSymbol>().Single();
90 WixAssert.StringEqual(Path.Combine(folder, @"data\test.txt"), fileSymbol[FileSymbolFields.Source].AsPath().Path);
91 WixAssert.StringEqual(@"test.txt", fileSymbol[FileSymbolFields.Source].PreviousValue.AsPath().Path);
92 }
93 }
94
95 [Fact]
96 public void CanBuildSingleFileCompressedWithMediaTemplate()
97 {
98 var folder = TestData.Get(@"TestData\SingleFileCompressed");
99
100 using (var fs = new DisposableFileSystem())
101 {
102 var intermediateFolder = fs.GetFolder();
103
104 var result = WixRunner.Execute(new[]
105 {
106 "build",
107 Path.Combine(folder, "Package.wxs"),
108 Path.Combine(folder, "PackageComponents.wxs"),
109 "-d", "MediaTemplateCompressionLevel",
110 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
111 "-bindpath", Path.Combine(folder, "data"),
112 "-intermediateFolder", intermediateFolder,
113 "-o", Path.Combine(intermediateFolder, @"bin\test.msi")
114 });
115
116 result.AssertSuccess();
117
118 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.msi")));
119 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\cab1.cab")));
120 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.wixpdb")));
121 }
122 }
123
124 [Fact]
125 public void CanBuildSingleFileCompressedWithMediaTemplateWithLowCompression()
126 {
127 var folder = TestData.Get(@"TestData\SingleFileCompressed");
128
129 using (var fs = new DisposableFileSystem())
130 {
131 var intermediateFolder = fs.GetFolder();
132
133 var result = WixRunner.Execute(new[]
134 {
135 "build",
136 Path.Combine(folder, "Package.wxs"),
137 Path.Combine(folder, "PackageComponents.wxs"),
138 "-d", "MediaTemplateCompressionLevel=low",
139 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
140 "-bindpath", Path.Combine(folder, "data"),
141 "-intermediateFolder", intermediateFolder,
142 "-o", Path.Combine(intermediateFolder, @"bin\test.msi")
143 });
144
145 result.AssertSuccess();
146
147 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.msi")));
148 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\low1.cab")));
149 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.wixpdb")));
150 }
151 }
152
153 [Fact]
154 public void CanBuildMultipleFilesCompressed()
155 {
156 var folder = TestData.Get(@"TestData\MultiFileCompressed");
157
158 using (var fs = new DisposableFileSystem())
159 {
160 var intermediateFolder = fs.GetFolder();
161
162 var result = WixRunner.Execute(new[]
163 {
164 "build",
165 "-sw1079", // TODO: why does this test need to create a second cab which is empty?
166 Path.Combine(folder, "Package.wxs"),
167 Path.Combine(folder, "PackageComponents.wxs"),
168 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
169 "-bindpath", Path.Combine(folder, "data"),
170 "-intermediateFolder", intermediateFolder,
171 "-o", Path.Combine(intermediateFolder, @"bin\test.msi")
172 });
173
174 result.AssertSuccess();
175
176 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.msi")));
177 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\example1.cab")));
178 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\example2.cab")));
179 Assert.True(File.Exists(Path.Combine(intermediateFolder, @"bin\test.wixpdb")));
180 }
181 }
182
183 [Fact]
184 public void CanFailBuildMissingFile()
185 { 62 {
186 var folder = TestData.Get(@"TestData\SingleFile"); 63 var folder = TestData.Get(@"TestData\SingleFile");
187 64
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/MultiFileSpanningCabinets.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/MultiFileSpanningCabinets.wxs
new file mode 100644
index 00000000..c7ac4699
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/MultiFileSpanningCabinets.wxs
@@ -0,0 +1,38 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Language="1033" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a" Compressed="yes" InstallerVersion="200" Scope="perMachine">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade not allowed" />
4
5 <MediaTemplate CabinetTemplate="cab{0}.cab" CompressionLevel="none" MaximumUncompressedMediaSize="20" MaximumCabinetSizeForLargeFileSplitting="20" />
6
7 <Feature Id="ProductFeature">
8 <!-- The name of the files here are important to control order of the generated media and cabinets. -->
9 <Component Directory="INSTALLFOLDER">
10 <File Source="abc.gen" />
11 </Component>
12 <Component Directory="INSTALLFOLDER">
13 <File Source="ced.txt" />
14 </Component>
15 <Component Directory="INSTALLFOLDER">
16 <File Source="hij.txt" />
17 </Component>
18 <Component Directory="INSTALLFOLDER">
19 <File Source="mno.gen" />
20 </Component>
21 <Component Directory="INSTALLFOLDER">
22 <File Source="qrs.txt" />
23 </Component>
24 <Component Directory="INSTALLFOLDER">
25 <File Source="tuv.txt" />
26 </Component>
27 <Component Directory="INSTALLFOLDER">
28 <File Source="xyz.gen" />
29 </Component>
30 </Feature>
31 </Package>
32
33 <Fragment>
34 <StandardDirectory Id="ProgramFilesFolder">
35 <Directory Id="INSTALLFOLDER" Name="MsiPackage" />
36 </StandardDirectory>
37 </Fragment>
38</Wix>
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/ced.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/ced.txt
new file mode 100644
index 00000000..6f52fa6a
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/ced.txt
@@ -0,0 +1 @@
This is ced.txt.
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/hij.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/hij.txt
new file mode 100644
index 00000000..ca53b0ae
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/hij.txt
@@ -0,0 +1 @@
This is hij.txt.
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/qrs.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/qrs.txt
new file mode 100644
index 00000000..8e33dd06
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/qrs.txt
@@ -0,0 +1 @@
This is qrs.txt.
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/tuv.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/tuv.txt
new file mode 100644
index 00000000..3c258bd2
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/MsiCabinet/data/tuv.txt
@@ -0,0 +1 @@
This is tuv.txt.
diff --git a/src/wix/wixnative/smartcab.cpp b/src/wix/wixnative/smartcab.cpp
index b5f20736..9c441a34 100644
--- a/src/wix/wixnative/smartcab.cpp
+++ b/src/wix/wixnative/smartcab.cpp
@@ -2,8 +2,8 @@
2 2
3#include "precomp.h" 3#include "precomp.h"
4 4
5static HRESULT CompressFiles(__in HANDLE hCab); 5static HRESULT CompressFiles(__in HANDLE hCab, __inout_z LPWSTR* psczFirstFileToken);
6static void __stdcall CabNamesCallback(__in_z LPWSTR wzFirstCabName, __in_z LPWSTR wzNewCabName, __in_z LPWSTR wzFileToken); 6static void __stdcall CabNamesCallback(__in_z LPCWSTR wzFirstCabName, __in_z LPCWSTR wzNewCabName, __in_z LPCWSTR wzFileToken);
7 7
8 8
9HRESULT SmartCabCommand( 9HRESULT SmartCabCommand(
@@ -20,6 +20,7 @@ HRESULT SmartCabCommand(
20 UINT uiMaxThresh = 0; 20 UINT uiMaxThresh = 0;
21 COMPRESSION_TYPE ct = COMPRESSION_TYPE_NONE; 21 COMPRESSION_TYPE ct = COMPRESSION_TYPE_NONE;
22 HANDLE hCab = NULL; 22 HANDLE hCab = NULL;
23 LPWSTR sczFirstFileToken = NULL;
23 24
24 if (argc < 1) 25 if (argc < 1)
25 { 26 {
@@ -71,16 +72,22 @@ HRESULT SmartCabCommand(
71 hr = WixNativeReadStdinPreamble(); 72 hr = WixNativeReadStdinPreamble();
72 ExitOnFailure(hr, "failed to read stdin preamble before smartcabbing"); 73 ExitOnFailure(hr, "failed to read stdin preamble before smartcabbing");
73 74
74 hr = CompressFiles(hCab); 75 hr = CompressFiles(hCab, &sczFirstFileToken);
75 ExitOnFailure(hr, "failed to compress files into cabinet: %ls", sczCabPath); 76 ExitOnFailure(hr, "failed to compress files into cabinet: %ls", sczCabPath);
77
78 CabNamesCallback(wzCabName, wzCabName, sczFirstFileToken);
79 }
80 else
81 {
82 CabNamesCallback(wzCabName, wzCabName, L"");
76 } 83 }
77 84
78 hr = CabCFinish(hCab, CabNamesCallback); 85 hr = CabCFinish(hCab, CabNamesCallback);
79 hCab = NULL; // once finish is called, the handle is invalid. 86 hCab = NULL; // once finish is called, the handle is invalid.
80 ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to compress cabinet: %ls", sczCabPath); 87 ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to compress cabinet: %ls", sczCabPath);
81 88
82
83LExit: 89LExit:
90 ReleaseStr(sczFirstFileToken);
84 if (hCab) 91 if (hCab)
85 { 92 {
86 CabCCancel(hCab); 93 CabCCancel(hCab);
@@ -93,7 +100,8 @@ LExit:
93 100
94 101
95static HRESULT CompressFiles( 102static HRESULT CompressFiles(
96 __in HANDLE hCab 103 __in HANDLE hCab,
104 __inout_z LPWSTR* psczFirstFileToken
97 ) 105 )
98{ 106{
99 HRESULT hr = S_OK; 107 HRESULT hr = S_OK;
@@ -138,6 +146,12 @@ static HRESULT CompressFiles(
138 pHashInfo = &hashInfo; 146 pHashInfo = &hashInfo;
139 } 147 }
140 148
149 if (psczFirstFileToken && !*psczFirstFileToken)
150 {
151 hr = StrAllocString(psczFirstFileToken, wzToken, 0);
152 ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to allocate first file token: %ls", wzToken);
153 }
154
141 hr = CabCAddFile(wzFilePath, wzToken, pHashInfo, hCab); 155 hr = CabCAddFile(wzFilePath, wzToken, pHashInfo, hCab);
142 ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to add file: %ls", wzFilePath); 156 ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to add file: %ls", wzFilePath);
143 157
@@ -157,9 +171,9 @@ LExit:
157// Second argument is name of the new cabinet that would be formed by splitting e.g. "cab1b.cab" 171// Second argument is name of the new cabinet that would be formed by splitting e.g. "cab1b.cab"
158// Third argument is the file token of the first file present in the splitting cabinet 172// Third argument is the file token of the first file present in the splitting cabinet
159static void __stdcall CabNamesCallback( 173static void __stdcall CabNamesCallback(
160 __in_z LPWSTR wzFirstCabName, 174 __in_z LPCWSTR wzFirstCabName,
161 __in_z LPWSTR wzNewCabName, 175 __in_z LPCWSTR wzNewCabName,
162 __in_z LPWSTR wzFileToken 176 __in_z LPCWSTR wzFileToken
163 ) 177 )
164{ 178{
165 ConsoleWriteLine(CONSOLE_COLOR_NORMAL, "%ls\t%ls\t%ls", wzFirstCabName, wzNewCabName, wzFileToken); 179 ConsoleWriteLine(CONSOLE_COLOR_NORMAL, "%ls\t%ls\t%ls", wzFirstCabName, wzNewCabName, wzFileToken);