@joebigelow / wix / commits / 678c92c5

Reorganize media assignment to correctly place facade order optimization

Rob Mensching committed Jun 15, 2020 at 16:07 UTC 678c92c50c6fb7aa9a093f0d74d4f92742abd5e8
8 files changed +208 -126
src/WixToolset.Core.WindowsInstaller/Bind/AssignMediaCommand.cs
+74 -85
@@ -35,18 +35,13 @@ namespace WixToolset.Core.WindowsInstaller.Bind
35
36 private bool FilesCompressed { get; }
37
38 - public string CabinetNameTemplate { private get; set; }
38 + private string CabinetNameTemplate { get; set; }
39
40 /// <summary>
41 /// Gets cabinets with their file rows.
42 /// </summary>
43 public Dictionary<MediaTuple, IEnumerable<FileFacade>> FileFacadesByCabinetMedia { get; private set; }
44
45 - /// <summary>
46 - /// Get media rows.
47 - /// </summary>
48 - public Dictionary<int, MediaTuple> MediaRows { get; private set; }
49 -
45 /// <summary>
46 /// Get uncompressed file rows. This will contain file rows of File elements that are marked with compression=no.
47 /// This contains all the files when Package element is marked with compression=no
@@ -55,17 +50,11 @@ namespace WixToolset.Core.WindowsInstaller.Bind
50
51 public void Execute()
52 {
58 - var filesByCabinetMedia = new Dictionary<MediaTuple, List<FileFacade>>();
59 -
60 - var mediaRows = new Dictionary<int, MediaTuple>();
61 -
62 - var uncompressedFiles = new List<FileFacade>();
63 -
64 - var mediaTable = this.Section.Tuples.OfType<MediaTuple>().ToList();
65 - var mediaTemplateTable = this.Section.Tuples.OfType<WixMediaTemplateTuple>().ToList();
53 + var mediaTuples = this.Section.Tuples.OfType<MediaTuple>().ToList();
54 + var mediaTemplateTuples = this.Section.Tuples.OfType<WixMediaTemplateTuple>().ToList();
55
67 - // If both tables are authored, it is an error.
68 - if (mediaTemplateTable.Count > 0 && mediaTable.Count > 1)
56 + // If both tuples are authored, it is an error.
57 + if (mediaTemplateTuples.Count > 0 && mediaTuples.Count > 1)
58 {
59 throw new WixException(ErrorMessages.MediaTableCollision(null));
60 }
@@ -78,34 +67,44 @@ namespace WixToolset.Core.WindowsInstaller.Bind
67 Cabinet = "#MergeModule.CABinet",
68 });
69
81 - filesByCabinetMedia.Add(mergeModuleMediaTuple, new List<FileFacade>(this.FileFacades));
70 + this.FileFacadesByCabinetMedia = new Dictionary<MediaTuple, IEnumerable<FileFacade>>
71 + {
72 + { mergeModuleMediaTuple, this.FileFacades }
73 + };
74 +
75 + this.UncompressedFileFacades = Array.Empty<FileFacade>();
76 }
83 - else if (mediaTemplateTable.Count == 0)
77 + else if (mediaTemplateTuples.Count == 0)
78 {
85 - this.ManuallyAssignFiles(mediaTable, this.FileFacades, filesByCabinetMedia, mediaRows, uncompressedFiles);
79 + var filesByCabinetMedia = new Dictionary<MediaTuple, List<FileFacade>>();
80 +
81 + var uncompressedFiles = new List<FileFacade>();
82 +
83 + this.ManuallyAssignFiles(mediaTuples, filesByCabinetMedia, uncompressedFiles);
84 +
85 + this.FileFacadesByCabinetMedia = filesByCabinetMedia.ToDictionary(kvp => kvp.Key, kvp => (IEnumerable<FileFacade>)kvp.Value);
86 +
87 + this.UncompressedFileFacades = uncompressedFiles;
88 }
89 else
90 {
89 - this.AutoAssignFiles(mediaTable, filesByCabinetMedia, mediaRows, uncompressedFiles);
90 - }
91 + var filesByCabinetMedia = new Dictionary<MediaTuple, List<FileFacade>>();
92
92 - this.FileFacadesByCabinetMedia = new Dictionary<MediaTuple, IEnumerable<FileFacade>>();
93 + var uncompressedFiles = new List<FileFacade>();
94
94 - foreach (var mediaRowWithFiles in filesByCabinetMedia)
95 - {
96 - this.FileFacadesByCabinetMedia.Add(mediaRowWithFiles.Key, mediaRowWithFiles.Value);
97 - }
95 + this.AutoAssignFiles(mediaTuples, filesByCabinetMedia, uncompressedFiles);
96
99 - this.MediaRows = mediaRows;
97 + this.FileFacadesByCabinetMedia = filesByCabinetMedia.ToDictionary(kvp => kvp.Key, kvp => (IEnumerable<FileFacade>)kvp.Value);
98
101 - this.UncompressedFileFacades = uncompressedFiles;
99 + this.UncompressedFileFacades = uncompressedFiles;
100 + }
101 }
102
103 /// <summary>
104 /// Assign files to cabinets based on MediaTemplate authoring.
105 /// </summary>
106 /// <param name="fileFacades">FileRowCollection</param>
108 - private void AutoAssignFiles(List<MediaTuple> mediaTable, Dictionary<MediaTuple, List<FileFacade>> filesByCabinetMedia, Dictionary<int, MediaTuple> mediaRows, List<FileFacade> uncompressedFiles)
107 + private void AutoAssignFiles(List<MediaTuple> mediaTable, Dictionary<MediaTuple, List<FileFacade>> filesByCabinetMedia, List<FileFacade> uncompressedFiles)
108 {
109 const int MaxCabIndex = 999;
110
@@ -158,6 +157,8 @@ namespace WixToolset.Core.WindowsInstaller.Bind
157 throw new WixException(ErrorMessages.MaximumUncompressedMediaSizeTooLarge(null, maxPreCabSizeInMB));
158 }
159
160 + var mediaTuplesByDiskId = new Dictionary<int, MediaTuple>();
161 +
162 foreach (var facade in this.FileFacades)
163 {
164 // When building a product, if the current file is not to be compressed or if
@@ -171,44 +172,38 @@ namespace WixToolset.Core.WindowsInstaller.Bind
172 if (currentCabIndex == MaxCabIndex)
173 {
174 // Associate current file with last cab (irrespective of the size) and cab index is not incremented anymore.
174 - var cabinetFiles = filesByCabinetMedia[currentMediaRow];
175 - facade.DiskId = currentCabIndex;
176 - cabinetFiles.Add(facade);
177 - continue;
178 - }
179 -
180 - // Update current cab size.
181 - currentPreCabSize += (ulong)facade.FileSize;
182 -
183 - if (currentPreCabSize > maxPreCabSizeInBytes)
184 - {
185 - // Overflow due to current file
186 - currentMediaRow = this.AddMediaRow(mediaTemplateRow, ++currentCabIndex);
187 - mediaRows.Add(currentMediaRow.DiskId, currentMediaRow);
188 - filesByCabinetMedia.Add(currentMediaRow, new List<FileFacade>());
189 -
190 - var cabinetFileRows = filesByCabinetMedia[currentMediaRow];
191 - facade.DiskId = currentCabIndex;
192 - cabinetFileRows.Add(facade);
193 - // Now files larger than MaxUncompressedMediaSize will be the only file in its cabinet so as to respect MaxUncompressedMediaSize
194 - currentPreCabSize = (ulong)facade.FileSize;
175 }
176 else
177 {
198 - // File fits in the current cab.
199 - if (currentMediaRow == null)
178 + // Update current cab size.
179 + currentPreCabSize += (ulong)facade.FileSize;
180 +
181 + // Overflow due to current file
182 + if (currentPreCabSize > maxPreCabSizeInBytes)
183 {
201 - // Create new cab and MediaRow
202 - currentMediaRow = this.AddMediaRow(mediaTemplateRow, ++currentCabIndex);
203 - mediaRows.Add(currentMediaRow.DiskId, currentMediaRow);
184 + currentMediaRow = this.AddMediaTuple(mediaTemplateRow, ++currentCabIndex);
185 + mediaTuplesByDiskId.Add(currentMediaRow.DiskId, currentMediaRow);
186 filesByCabinetMedia.Add(currentMediaRow, new List<FileFacade>());
205 - }
187
207 - // Associate current file with current cab.
208 - var cabinetFiles = filesByCabinetMedia[currentMediaRow];
209 - facade.DiskId = currentCabIndex;
210 - cabinetFiles.Add(facade);
188 + // Now files larger than MaxUncompressedMediaSize will be the only file in its cabinet so as to respect MaxUncompressedMediaSize
189 + currentPreCabSize = (ulong)facade.FileSize;
190 + }
191 + else // file fits in the current cab.
192 + {
193 + if (currentMediaRow == null)
194 + {
195 + // Create new cab and MediaRow
196 + currentMediaRow = this.AddMediaTuple(mediaTemplateRow, ++currentCabIndex);
197 + mediaTuplesByDiskId.Add(currentMediaRow.DiskId, currentMediaRow);
198 + filesByCabinetMedia.Add(currentMediaRow, new List<FileFacade>());
199 + }
200 + }
201 }
202 +
203 + // Associate current file with current cab.
204 + var cabinetFiles = filesByCabinetMedia[currentMediaRow];
205 + facade.DiskId = currentCabIndex;
206 + cabinetFiles.Add(facade);
207 }
208
209 // If there are uncompressed files and no MediaRow, create a default one.
@@ -219,51 +214,45 @@ namespace WixToolset.Core.WindowsInstaller.Bind
214 DiskId = 1,
215 });
216
222 - mediaRows.Add(1, defaultMediaRow);
217 + mediaTuplesByDiskId.Add(1, defaultMediaRow);
218 }
219 }
220
221 /// <summary>
222 /// Assign files to cabinets based on Media authoring.
223 /// </summary>
229 - /// <param name="mediaTable"></param>
230 - /// <param name="fileFacades"></param>
231 - private void ManuallyAssignFiles(List<MediaTuple> mediaTable, IEnumerable<FileFacade> fileFacades, Dictionary<MediaTuple, List<FileFacade>> filesByCabinetMedia, Dictionary<int, MediaTuple> mediaRows, List<FileFacade> uncompressedFiles)
224 + private void ManuallyAssignFiles(List<MediaTuple> mediaTuples, Dictionary<MediaTuple, List<FileFacade>> filesByCabinetMedia, List<FileFacade> uncompressedFiles)
225 {
233 - if (mediaTable.Any())
226 + var mediaTuplesByDiskId = new Dictionary<int, MediaTuple>();
227 +
228 + if (mediaTuples.Any())
229 {
235 - var cabinetMediaRows = new Dictionary<string, MediaTuple>(StringComparer.OrdinalIgnoreCase);
236 - foreach (var mediaRow in mediaTable)
230 + var cabinetMediaTuples = new Dictionary<string, MediaTuple>(StringComparer.OrdinalIgnoreCase);
231 + foreach (var mediaTuple in mediaTuples)
232 {
233 // If the Media row has a cabinet, make sure it is unique across all Media rows.
239 - if (!String.IsNullOrEmpty(mediaRow.Cabinet))
234 + if (!String.IsNullOrEmpty(mediaTuple.Cabinet))
235 {
241 - if (cabinetMediaRows.TryGetValue(mediaRow.Cabinet, out var existingRow))
236 + if (cabinetMediaTuples.TryGetValue(mediaTuple.Cabinet, out var existingRow))
237 {
243 - this.Messaging.Write(ErrorMessages.DuplicateCabinetName(mediaRow.SourceLineNumbers, mediaRow.Cabinet));
238 + this.Messaging.Write(ErrorMessages.DuplicateCabinetName(mediaTuple.SourceLineNumbers, mediaTuple.Cabinet));
239 this.Messaging.Write(ErrorMessages.DuplicateCabinetName2(existingRow.SourceLineNumbers, existingRow.Cabinet));
240 }
241 else
242 {
248 - cabinetMediaRows.Add(mediaRow.Cabinet, mediaRow);
243 + cabinetMediaTuples.Add(mediaTuple.Cabinet, mediaTuple);
244 }
250 - }
245
252 - mediaRows.Add(mediaRow.DiskId, mediaRow);
253 - }
254 - }
246 + filesByCabinetMedia.Add(mediaTuple, new List<FileFacade>());
247 + }
248
256 - foreach (var mediaRow in mediaRows.Values)
257 - {
258 - if (null != mediaRow.Cabinet)
259 - {
260 - filesByCabinetMedia.Add(mediaRow, new List<FileFacade>());
249 + mediaTuplesByDiskId.Add(mediaTuple.DiskId, mediaTuple);
250 }
251 }
252
264 - foreach (var facade in fileFacades)
253 + foreach (var facade in this.FileFacades)
254 {
266 - if (!mediaRows.TryGetValue(facade.DiskId, out var mediaRow))
255 + if (!mediaTuplesByDiskId.TryGetValue(facade.DiskId, out var mediaTuple))
256 {
257 this.Messaging.Write(ErrorMessages.MissingMedia(facade.SourceLineNumber, facade.DiskId));
258 continue;
@@ -279,7 +268,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
268 }
269 else // file is marked compressed.
270 {
282 - if (filesByCabinetMedia.TryGetValue(mediaRow, out var cabinetFiles))
271 + if (filesByCabinetMedia.TryGetValue(mediaTuple, out var cabinetFiles))
272 {
273 cabinetFiles.Add(facade);
274 }
@@ -292,12 +281,12 @@ namespace WixToolset.Core.WindowsInstaller.Bind
281 }
282
283 /// <summary>
295 - /// Adds a row to the media table with cab name template filled in.
284 + /// Adds a tuple to the section with cab name template filled in.
285 /// </summary>
286 /// <param name="mediaTable"></param>
287 /// <param name="cabIndex"></param>
288 /// <returns></returns>
300 - private MediaTuple AddMediaRow(WixMediaTemplateTuple mediaTemplateTuple, int cabIndex)
289 + private MediaTuple AddMediaTuple(WixMediaTemplateTuple mediaTemplateTuple, int cabIndex)
290 {
291 return this.Section.AddTuple(new MediaTuple(mediaTemplateTuple.SourceLineNumbers, new Identifier(AccessModifier.Private, cabIndex))
292 {
src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
+16 -16
@@ -281,19 +281,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
281 command.Execute();
282 }
283
284 - // Assign files to media.
285 - Dictionary<int, MediaTuple> assignedMediaRows;
286 - Dictionary<MediaTuple, IEnumerable<FileFacade>> filesByCabinetMedia;
287 - IEnumerable<FileFacade> uncompressedFiles;
288 - {
289 - var command = new AssignMediaCommand(section, this.Messaging, fileFacades, compressed);
290 - command.Execute();
291 -
292 - assignedMediaRows = command.MediaRows;
293 - filesByCabinetMedia = command.FileFacadesByCabinetMedia;
294 - uncompressedFiles = command.UncompressedFileFacades;
295 - }
296 -
284 // stop processing if an error previously occurred
285 if (this.Messaging.EncounteredError)
286 {
@@ -366,10 +353,23 @@ namespace WixToolset.Core.WindowsInstaller.Bind
353 command.Execute();
354 }
355
369 - // Update file sequence.
356 + // Assign files to media and update file sequences.
357 + Dictionary<MediaTuple, IEnumerable<FileFacade>> filesByCabinetMedia;
358 + IEnumerable<FileFacade> uncompressedFiles;
359 {
371 - var command = new UpdateMediaSequencesCommand(section, fileFacades);
372 - command.Execute();
360 + var order = new OptimizeFileFacadesOrderCommand(fileFacades);
361 + order.Execute();
362 +
363 + fileFacades = order.FileFacades;
364 +
365 + var assign = new AssignMediaCommand(section, this.Messaging, fileFacades, compressed);
366 + assign.Execute();
367 +
368 + filesByCabinetMedia = assign.FileFacadesByCabinetMedia;
369 + uncompressedFiles = assign.UncompressedFileFacades;
370 +
371 + var update = new UpdateMediaSequencesCommand(section, fileFacades);
372 + update.Execute();
373 }
374
375 // stop processing if an error previously occurred
src/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs
+5 -9
@@ -7,7 +7,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10 - using WixToolset.Core.Bind;
10 using WixToolset.Core.Native;
11 using WixToolset.Data;
12 using WixToolset.Extensibility.Services;
@@ -18,12 +17,13 @@ namespace WixToolset.Core.WindowsInstaller.Bind
17 /// </summary>
18 internal sealed class CabinetBuilder
19 {
21 - private Queue cabinetWorkItems;
22 - private object lockObject;
20 + private readonly object lockObject = new object();
21 +
22 + private readonly Queue cabinetWorkItems;
23 private int threadCount;
24
25 // Address of Binder's callback function for Cabinet Splitting
26 - private IntPtr newCabNamesCallBackAddress;
26 + private readonly IntPtr newCabNamesCallBackAddress;
27
28 /// <summary>
29 /// Instantiate a new CabinetBuilder.
@@ -38,7 +38,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
38 }
39
40 this.cabinetWorkItems = new Queue();
41 - this.lockObject = new object();
41 this.Messaging = messaging;
42 this.threadCount = threadCount;
43
@@ -56,10 +55,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
55 /// Enqueues a CabinetWorkItem to the queue.
56 /// </summary>
57 /// <param name="cabinetWorkItem">cabinet work item</param>
59 - public void Enqueue(CabinetWorkItem cabinetWorkItem)
60 - {
61 - this.cabinetWorkItems.Enqueue(cabinetWorkItem);
62 - }
58 + public void Enqueue(CabinetWorkItem cabinetWorkItem) => this.cabinetWorkItems.Enqueue(cabinetWorkItem);
59
60 /// <summary>
61 /// Create the queued cabinets.
src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
-1
@@ -171,7 +171,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
171 return cabbingThreadCount;
172 }
173
174 -
174 /// <summary>
175 /// Creates a work item to create a cabinet.
176 /// </summary>
src/WixToolset.Core.WindowsInstaller/Bind/OptimizeFileFacadesOrderCommand.cs new
+38
@@ -0,0 +1,38 @@
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 WixToolset.Core.Bind;
8 +
9 + internal class OptimizeFileFacadesOrderCommand
10 + {
11 + public OptimizeFileFacadesOrderCommand(List<FileFacade> fileFacades)
12 + {
13 + this.FileFacades = fileFacades;
14 + }
15 +
16 + public List<FileFacade> FileFacades { get; private set; }
17 +
18 + public List<FileFacade> Execute()
19 + {
20 + this.FileFacades.Sort(FileFacadeOptimizer.Instance);
21 +
22 + return this.FileFacades;
23 + }
24 +
25 + private class FileFacadeOptimizer : IComparer<FileFacade>
26 + {
27 + public static readonly FileFacadeOptimizer Instance = new FileFacadeOptimizer();
28 +
29 + public int Compare(FileFacade x, FileFacade y)
30 + {
31 + // TODO: Sort these facades even smarter by directory path and component id
32 + // and maybe file size or file extension and other creative ideas to
33 + // get optimal install speed out of MSI.
34 + return String.Compare(x.ComponentRef, y.ComponentRef, StringComparison.Ordinal);
35 + }
36 + }
37 + }
38 +}
src/WixToolset.Core.WindowsInstaller/Bind/UpdateMediaSequencesCommand.cs
+2 -13
@@ -29,9 +29,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
29 {
30 var lastSequence = 0;
31
32 - // Order by Component to group the files by directory.
33 - var optimized = this.OptimizedFileFacades();
34 - foreach (var facade in optimized)
32 + foreach (var facade in this.FileFacades)
33 {
34 facade.Sequence = ++lastSequence;
35 }
@@ -43,8 +41,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
41 var patchGroups = new Dictionary<int, List<FileFacade>>();
42
43 // sequence the non-patch-added files
46 - var optimized = this.OptimizedFileFacades();
47 - foreach (var facade in optimized)
44 + foreach (var facade in this.FileFacades)
45 {
46 if (null == mediaTuple)
47 {
@@ -108,13 +105,5 @@ namespace WixToolset.Core.WindowsInstaller.Bind
105 }
106 }
107 }
111 -
112 - private IEnumerable<FileFacade> OptimizedFileFacades()
113 - {
114 - // TODO: Sort these facades even smarter by directory path and component id
115 - // and maybe file size or file extension and other creative ideas to
116 - // get optimal install speed out of MSI.
117 - return this.FileFacades.OrderBy(f => f.ComponentRef);
118 - }
108 }
109 }
src/test/WixToolsetTest.CoreIntegration/CabFixture.cs new
+71
@@ -0,0 +1,71 @@
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 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 Xunit;
11 +
12 + public class CabFixture
13 + {
14 + [Fact]
15 + public void CabinetFilesSequencedCorrectly()
16 + {
17 + var folder = TestData.Get(@"TestData\MultiFileCompressed");
18 +
19 + using (var fs = new DisposableFileSystem())
20 + {
21 + var baseFolder = fs.GetFolder();
22 + var intermediateFolder = Path.Combine(baseFolder, "obj");
23 + var msiPath = Path.Combine(baseFolder, @"bin\test.msi");
24 + var cabPath = Path.Combine(baseFolder, @"bin\cab1.cab");
25 +
26 + var result = WixRunner.Execute(new[]
27 + {
28 + "build",
29 + Path.Combine(folder, "Package.wxs"),
30 + Path.Combine(folder, "PackageComponents.wxs"),
31 + "-d", "MediaTemplateCompressionLevel",
32 + "-loc", Path.Combine(folder, "Package.en-us.wxl"),
33 + "-bindpath", Path.Combine(folder, "data"),
34 + "-intermediateFolder", intermediateFolder,
35 + "-o", msiPath
36 + });
37 +
38 + result.AssertSuccess();
39 + Assert.True(File.Exists(cabPath));
40 +
41 + var fileTable = Query.QueryDatabase(msiPath, new[] { "File" });
42 + var fileRows = fileTable.Select(r => new FileRow(r)).OrderBy(f => f.Sequence).ToList();
43 +
44 + Assert.Equal(new[] { 1, 2 }, fileRows.Select(f => f.Sequence).ToArray());
45 + Assert.Equal(new[] { "test.txt", "Notepad.exe" }, fileRows.Select(f => f.Name).ToArray());
46 +
47 + var files = Query.GetCabinetFiles(cabPath);
48 + Assert.Equal(fileRows.Select(f => f.Id).ToArray(), files.Select(f => f.Name).ToArray());
49 + }
50 + }
51 +
52 + private class FileRow
53 + {
54 + public FileRow(string row)
55 + {
56 + row = row.Substring("File:".Length);
57 +
58 + var split = row.Split('\t');
59 + this.Id = split[0];
60 + this.Name = split[2];
61 + this.Sequence = Convert.ToInt32(split[7]);
62 + }
63 +
64 + public string Id { get; set; }
65 +
66 + public string Name { get; set; }
67 +
68 + public int Sequence { get; set; }
69 + }
70 + }
71 +}
src/test/WixToolsetTest.CoreIntegration/TestData/MultiFileCompressed/PackageComponents.wxs
+2 -2
@@ -3,10 +3,10 @@
3 <Fragment>
4 <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
5 <Component>
6 - <File Source="test.txt" />
6 + <File Source="$(env.WINDIR)\Notepad.exe" />
7 </Component>
8 <Component>
9 - <File Source="$(env.WINDIR)\Notepad.exe" />
9 + <File Source="test.txt" />
10 </Component>
11 </ComponentGroup>
12 </Fragment>