aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2021-04-10 15:04:04 -0700
committerRob Mensching <rob@firegiant.com>2021-04-12 10:24:01 -0700
commit3441afb46c4dc056493ab84f9b27434c4185d713 (patch)
treeec5d6b43292d34e256ebcbf6bf8795b4da46bc80 /src
parente7b745b08c51b173c80253432a82583e73c46157 (diff)
downloadwix-3441afb46c4dc056493ab84f9b27434c4185d713.tar.gz
wix-3441afb46c4dc056493ab84f9b27434c4185d713.tar.bz2
wix-3441afb46c4dc056493ab84f9b27434c4185d713.zip
Improve implicit Component/@Id generation and duplicate GUID errors
Diffstat (limited to 'src')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs9
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CalculateComponentGuids.cs181
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/FinalizeComponentGuids.cs262
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/ValidateComponentGuidsCommand.cs63
-rw-r--r--src/WixToolset.Core/Compiler.cs23
-rw-r--r--src/test/WixToolsetTest.CoreIntegration/ComponentFixture.cs45
-rw-r--r--src/test/WixToolsetTest.CoreIntegration/TestData/Component/GuidCollision.wxs14
7 files changed, 336 insertions, 261 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
index 06b51ba1..9f36cd78 100644
--- a/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
@@ -354,14 +354,9 @@ namespace WixToolset.Core.WindowsInstaller.Bind
354 } 354 }
355 } 355 }
356 356
357 // Set generated component guids. 357 // Set generated component guids and validate all guids.
358 { 358 {
359 var command = new CalculateComponentGuids(this.Messaging, this.WindowsInstallerBackendHelper, this.PathResolver, section, platform); 359 var command = new FinalizeComponentGuids(this.Messaging, this.WindowsInstallerBackendHelper, this.PathResolver, section, platform);
360 command.Execute();
361 }
362
363 {
364 var command = new ValidateComponentGuidsCommand(this.Messaging, section);
365 command.Execute(); 360 command.Execute();
366 } 361 }
367 362
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CalculateComponentGuids.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CalculateComponentGuids.cs
deleted file mode 100644
index 5cb297e5..00000000
--- a/src/WixToolset.Core.WindowsInstaller/Bind/CalculateComponentGuids.cs
+++ /dev/null
@@ -1,181 +0,0 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Core.WindowsInstaller.Bind
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using WixToolset.Data;
10 using WixToolset.Data.Symbols;
11 using WixToolset.Extensibility.Data;
12 using WixToolset.Extensibility.Services;
13
14 /// <summary>
15 /// Set the guids for components with generatable guids.
16 /// </summary>
17 internal class CalculateComponentGuids
18 {
19 internal CalculateComponentGuids(IMessaging messaging, IBackendHelper helper, IPathResolver pathResolver, IntermediateSection section, Platform platform)
20 {
21 this.Messaging = messaging;
22 this.BackendHelper = helper;
23 this.PathResolver = pathResolver;
24 this.Section = section;
25 this.Platform = platform;
26 }
27
28 private IMessaging Messaging { get; }
29
30 private IBackendHelper BackendHelper { get; }
31
32 private IPathResolver PathResolver { get; }
33
34 private IntermediateSection Section { get; }
35
36 private Platform Platform { get; }
37
38 public void Execute()
39 {
40 Dictionary<string, RegistrySymbol> registryKeyRows = null;
41 Dictionary<string, IResolvedDirectory> targetPathsByDirectoryId = null;
42 Dictionary<string, string> componentIdGenSeeds = null;
43 Dictionary<string, List<FileSymbol>> filesByComponentId = null;
44
45 // Find components with generatable guids.
46 foreach (var componentSymbol in this.Section.Symbols.OfType<ComponentSymbol>())
47 {
48 // Skip components that do not specify generate guid.
49 if (componentSymbol.ComponentId != "*")
50 {
51 continue;
52 }
53
54 if (String.IsNullOrEmpty(componentSymbol.KeyPath) || ComponentKeyPathType.OdbcDataSource == componentSymbol.KeyPathType)
55 {
56 this.Messaging.Write(ErrorMessages.IllegalComponentWithAutoGeneratedGuid(componentSymbol.SourceLineNumbers));
57 continue;
58 }
59
60 if (ComponentKeyPathType.Registry == componentSymbol.KeyPathType)
61 {
62 if (registryKeyRows is null)
63 {
64 registryKeyRows = this.Section.Symbols.OfType<RegistrySymbol>().ToDictionary(t => t.Id.Id);
65 }
66
67 if (registryKeyRows.TryGetValue(componentSymbol.KeyPath, out var foundRow))
68 {
69 var bitness = componentSymbol.Win64 ? "64" : String.Empty;
70 var regkey = String.Concat(bitness, foundRow.Root, "\\", foundRow.Key, "\\", foundRow.Name);
71 componentSymbol.ComponentId = this.BackendHelper.CreateGuid(BindDatabaseCommand.WixComponentGuidNamespace, regkey.ToLowerInvariant());
72 }
73 }
74 else // must be a File KeyPath.
75 {
76 // If the directory table hasn't been loaded into an indexed hash
77 // of directory ids to target names do that now.
78 if (targetPathsByDirectoryId is null)
79 {
80 var directories = this.Section.Symbols.OfType<DirectorySymbol>().ToList();
81
82 targetPathsByDirectoryId = new Dictionary<string, IResolvedDirectory>(directories.Count);
83
84 // Get the target paths for all directories.
85 foreach (var directory in directories)
86 {
87 // If the directory Id already exists, we will skip it here since
88 // checking for duplicate primary keys is done later when importing tables
89 // into database
90 if (targetPathsByDirectoryId.ContainsKey(directory.Id.Id))
91 {
92 continue;
93 }
94
95 var resolvedDirectory = this.BackendHelper.CreateResolvedDirectory(directory.ParentDirectoryRef, directory.Name);
96 targetPathsByDirectoryId.Add(directory.Id.Id, resolvedDirectory);
97 }
98 }
99
100 // If the component id generation seeds have not been indexed
101 // from the Directory symbols do that now.
102 if (componentIdGenSeeds is null)
103 {
104 // If there are any Directory symbols, build up the Component Guid
105 // generation seeds indexed by Directory/@Id.
106 componentIdGenSeeds = this.Section.Symbols.OfType<DirectorySymbol>()
107 .Where(t => !String.IsNullOrEmpty(t.ComponentGuidGenerationSeed))
108 .ToDictionary(t => t.Id.Id, t => t.ComponentGuidGenerationSeed);
109 }
110
111 // if the file rows have not been indexed by File.Component yet
112 // then do that now
113 if (filesByComponentId is null)
114 {
115 var files = this.Section.Symbols.OfType<FileSymbol>().ToList();
116
117 filesByComponentId = new Dictionary<string, List<FileSymbol>>(files.Count);
118
119 foreach (var file in files)
120 {
121 if (!filesByComponentId.TryGetValue(file.ComponentRef, out var componentFiles))
122 {
123 componentFiles = new List<FileSymbol>();
124 filesByComponentId.Add(file.ComponentRef, componentFiles);
125 }
126
127 componentFiles.Add(file);
128 }
129 }
130
131 // validate component meets all the conditions to have a generated guid
132 var currentComponentFiles = filesByComponentId[componentSymbol.Id.Id];
133 var numFilesInComponent = currentComponentFiles.Count;
134 string path = null;
135
136 foreach (var fileRow in currentComponentFiles)
137 {
138 if (fileRow.Id.Id == componentSymbol.KeyPath)
139 {
140 // calculate the key file's canonical target path
141 var directoryPath = this.PathResolver.GetCanonicalDirectoryPath(targetPathsByDirectoryId, componentIdGenSeeds, componentSymbol.DirectoryRef, this.Platform);
142 var fileName = this.BackendHelper.GetMsiFileName(fileRow.Name, false, true).ToLowerInvariant();
143 path = Path.Combine(directoryPath, fileName);
144
145 // find paths that are not canonicalized
146 if (path.StartsWith(@"PersonalFolder\my pictures", StringComparison.Ordinal) ||
147 path.StartsWith(@"ProgramFilesFolder\common files", StringComparison.Ordinal) ||
148 path.StartsWith(@"ProgramMenuFolder\startup", StringComparison.Ordinal) ||
149 path.StartsWith("TARGETDIR", StringComparison.Ordinal) ||
150 path.StartsWith(@"StartMenuFolder\programs", StringComparison.Ordinal) ||
151 path.StartsWith(@"WindowsFolder\fonts", StringComparison.Ordinal))
152 {
153 this.Messaging.Write(ErrorMessages.IllegalPathForGeneratedComponentGuid(componentSymbol.SourceLineNumbers, fileRow.ComponentRef, path));
154 }
155
156 // if component has more than one file, the key path must be versioned
157 if (1 < numFilesInComponent && String.IsNullOrEmpty(fileRow.Version))
158 {
159 this.Messaging.Write(ErrorMessages.IllegalGeneratedGuidComponentUnversionedKeypath(componentSymbol.SourceLineNumbers));
160 }
161 }
162 else
163 {
164 // not a key path, so it must be an unversioned file if component has more than one file
165 if (1 < numFilesInComponent && !String.IsNullOrEmpty(fileRow.Version))
166 {
167 this.Messaging.Write(ErrorMessages.IllegalGeneratedGuidComponentVersionedNonkeypath(componentSymbol.SourceLineNumbers));
168 }
169 }
170 }
171
172 // if the rules were followed, reward with a generated guid
173 if (!this.Messaging.EncounteredError)
174 {
175 componentSymbol.ComponentId = this.BackendHelper.CreateGuid(BindDatabaseCommand.WixComponentGuidNamespace, path);
176 }
177 }
178 }
179 }
180 }
181}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/FinalizeComponentGuids.cs b/src/WixToolset.Core.WindowsInstaller/Bind/FinalizeComponentGuids.cs
new file mode 100644
index 00000000..3cdc0c28
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/FinalizeComponentGuids.cs
@@ -0,0 +1,262 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Core.WindowsInstaller.Bind
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using WixToolset.Data;
10 using WixToolset.Data.Symbols;
11 using WixToolset.Extensibility.Data;
12 using WixToolset.Extensibility.Services;
13
14 /// <summary>
15 /// Set the guids for components with generatable guids and validate all are appropriately unique.
16 /// </summary>
17 internal class FinalizeComponentGuids
18 {
19 internal FinalizeComponentGuids(IMessaging messaging, IBackendHelper helper, IPathResolver pathResolver, IntermediateSection section, Platform platform)
20 {
21 this.Messaging = messaging;
22 this.BackendHelper = helper;
23 this.PathResolver = pathResolver;
24 this.Section = section;
25 this.Platform = platform;
26 }
27
28 private IMessaging Messaging { get; }
29
30 private IBackendHelper BackendHelper { get; }
31
32 private IPathResolver PathResolver { get; }
33
34 private IntermediateSection Section { get; }
35
36 private Platform Platform { get; }
37
38 private Dictionary<string, string> ComponentIdGenSeeds { get; set; }
39
40 private ILookup<string, FileSymbol> FilesByComponentId { get; set; }
41
42 private Dictionary<string, RegistrySymbol> RegistrySymbolsById { get; set; }
43
44 private Dictionary<string, IResolvedDirectory> TargetPathsByDirectoryId { get; set; }
45
46 public void Execute()
47 {
48 var componentGuidConditions = new Dictionary<string, List<ComponentSymbol>>(StringComparer.OrdinalIgnoreCase);
49 var guidCollisions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
50
51 foreach (var componentSymbol in this.Section.Symbols.OfType<ComponentSymbol>())
52 {
53 if (componentSymbol.ComponentId == "*")
54 {
55 this.GenerateComponentGuid(componentSymbol);
56 }
57
58 // Now check for GUID collisions, but we don't care about unmanaged components and
59 // if there's a * GUID remaining, there's already an error that explained why it
60 // was not replaced with a real GUID.
61 if (!String.IsNullOrEmpty(componentSymbol.ComponentId) && componentSymbol.ComponentId != "*")
62 {
63 if (!componentGuidConditions.TryGetValue(componentSymbol.ComponentId, out var components))
64 {
65 components = new List<ComponentSymbol>();
66 componentGuidConditions.Add(componentSymbol.ComponentId, components);
67 }
68
69 components.Add(componentSymbol);
70 if (components.Count > 1)
71 {
72 guidCollisions.Add(componentSymbol.ComponentId);
73 }
74 }
75 }
76
77 if (guidCollisions.Count > 0)
78 {
79 this.ReportGuidCollisions(guidCollisions, componentGuidConditions);
80 }
81 }
82
83 private void GenerateComponentGuid(ComponentSymbol componentSymbol)
84 {
85 if (String.IsNullOrEmpty(componentSymbol.KeyPath) || ComponentKeyPathType.OdbcDataSource == componentSymbol.KeyPathType)
86 {
87 this.Messaging.Write(ErrorMessages.IllegalComponentWithAutoGeneratedGuid(componentSymbol.SourceLineNumbers));
88 return;
89 }
90
91 if (ComponentKeyPathType.Registry == componentSymbol.KeyPathType)
92 {
93 if (this.RegistrySymbolsById is null)
94 {
95 this.RegistrySymbolsById = this.Section.Symbols.OfType<RegistrySymbol>().ToDictionary(t => t.Id.Id);
96 }
97
98 if (this.RegistrySymbolsById.TryGetValue(componentSymbol.KeyPath, out var registrySymbol))
99 {
100 var bitness = componentSymbol.Win64 ? "64" : String.Empty;
101 var regkey = String.Concat(bitness, registrySymbol.Root, "\\", registrySymbol.Key, "\\", registrySymbol.Name);
102 componentSymbol.ComponentId = this.BackendHelper.CreateGuid(BindDatabaseCommand.WixComponentGuidNamespace, regkey.ToLowerInvariant());
103 }
104 }
105 else // must be a File KeyPath.
106 {
107 // If the directory table hasn't been loaded into an indexed hash
108 // of directory ids to target names do that now.
109 if (this.TargetPathsByDirectoryId is null)
110 {
111 this.TargetPathsByDirectoryId = this.ResolveDirectoryTargetPaths();
112 }
113
114 // If the component id generation seeds have not been indexed
115 // from the Directory symbols do that now.
116 if (this.ComponentIdGenSeeds is null)
117 {
118 // If there are any Directory symbols, build up the Component Guid
119 // generation seeds indexed by Directory/@Id.
120 this.ComponentIdGenSeeds = this.Section.Symbols.OfType<DirectorySymbol>()
121 .Where(t => !String.IsNullOrEmpty(t.ComponentGuidGenerationSeed))
122 .ToDictionary(t => t.Id.Id, t => t.ComponentGuidGenerationSeed);
123 }
124
125 // If the file symbols have not been indexed by File's ComponentRef yet
126 // then do that now.
127 if (this.FilesByComponentId is null)
128 {
129 this.FilesByComponentId = this.Section.Symbols.OfType<FileSymbol>().ToLookup(f => f.ComponentRef);
130 }
131
132 // validate component meets all the conditions to have a generated guid
133 var currentComponentFiles = this.FilesByComponentId[componentSymbol.Id.Id];
134 var numFilesInComponent = currentComponentFiles.Count();
135 string path = null;
136
137 foreach (var fileSymbol in currentComponentFiles)
138 {
139 if (fileSymbol.Id.Id == componentSymbol.KeyPath)
140 {
141 // calculate the key file's canonical target path
142 var directoryPath = this.PathResolver.GetCanonicalDirectoryPath(this.TargetPathsByDirectoryId, this.ComponentIdGenSeeds, componentSymbol.DirectoryRef, this.Platform);
143 var fileName = this.BackendHelper.GetMsiFileName(fileSymbol.Name, false, true).ToLowerInvariant();
144 path = Path.Combine(directoryPath, fileName);
145
146 // find paths that are not canonicalized
147 if (path.StartsWith(@"PersonalFolder\my pictures", StringComparison.Ordinal) ||
148 path.StartsWith(@"ProgramFilesFolder\common files", StringComparison.Ordinal) ||
149 path.StartsWith(@"ProgramMenuFolder\startup", StringComparison.Ordinal) ||
150 path.StartsWith("TARGETDIR", StringComparison.Ordinal) ||
151 path.StartsWith(@"StartMenuFolder\programs", StringComparison.Ordinal) ||
152 path.StartsWith(@"WindowsFolder\fonts", StringComparison.Ordinal))
153 {
154 this.Messaging.Write(ErrorMessages.IllegalPathForGeneratedComponentGuid(componentSymbol.SourceLineNumbers, fileSymbol.ComponentRef, path));
155 }
156
157 // if component has more than one file, the key path must be versioned
158 if (1 < numFilesInComponent && String.IsNullOrEmpty(fileSymbol.Version))
159 {
160 this.Messaging.Write(ErrorMessages.IllegalGeneratedGuidComponentUnversionedKeypath(componentSymbol.SourceLineNumbers));
161 }
162 }
163 else
164 {
165 // not a key path, so it must be an unversioned file if component has more than one file
166 if (1 < numFilesInComponent && !String.IsNullOrEmpty(fileSymbol.Version))
167 {
168 this.Messaging.Write(ErrorMessages.IllegalGeneratedGuidComponentVersionedNonkeypath(componentSymbol.SourceLineNumbers));
169 }
170 }
171 }
172
173 // if the rules were followed, reward with a generated guid
174 if (!this.Messaging.EncounteredError)
175 {
176 componentSymbol.ComponentId = this.BackendHelper.CreateGuid(BindDatabaseCommand.WixComponentGuidNamespace, path);
177 }
178 }
179 }
180
181 private void ReportGuidCollisions(HashSet<string> guidCollisions, Dictionary<string, List<ComponentSymbol>> componentGuidConditions)
182 {
183 Dictionary<string, FileSymbol> fileSymbolsById = null;
184
185 foreach (var guid in guidCollisions)
186 {
187 var collidingComponents = componentGuidConditions[guid];
188 var allComponentsHaveConditions = collidingComponents.All(c => !String.IsNullOrEmpty(c.Condition));
189
190 foreach (var componentSymbol in collidingComponents)
191 {
192 string path;
193 string type;
194
195 if (componentSymbol.KeyPathType == ComponentKeyPathType.File)
196 {
197 if (fileSymbolsById is null)
198 {
199 fileSymbolsById = this.Section.Symbols.OfType<FileSymbol>().ToDictionary(t => t.Id.Id);
200 }
201
202 path = fileSymbolsById.TryGetValue(componentSymbol.KeyPath, out var fileSymbol) ? fileSymbol.Source.Path : componentSymbol.KeyPath;
203 type = "source path";
204 }
205 else if (componentSymbol.KeyPathType == ComponentKeyPathType.Registry)
206 {
207 if (this.RegistrySymbolsById is null)
208 {
209 this.RegistrySymbolsById = this.Section.Symbols.OfType<RegistrySymbol>().ToDictionary(t => t.Id.Id);
210 }
211
212 path = this.RegistrySymbolsById.TryGetValue(componentSymbol.KeyPath, out var registrySymbol) ? String.Concat(registrySymbol.Key, "\\", registrySymbol.Name) : componentSymbol.KeyPath;
213 type = "registry path";
214 }
215 else
216 {
217 if (this.TargetPathsByDirectoryId is null)
218 {
219 this.TargetPathsByDirectoryId = this.ResolveDirectoryTargetPaths();
220 }
221
222 path = this.PathResolver.GetCanonicalDirectoryPath(this.TargetPathsByDirectoryId, componentIdGenSeeds: null, componentSymbol.DirectoryRef, this.Platform);
223 type = "directory";
224 }
225
226 if (allComponentsHaveConditions)
227 {
228 this.Messaging.Write(WarningMessages.DuplicateComponentGuidsMustHaveMutuallyExclusiveConditions(componentSymbol.SourceLineNumbers, componentSymbol.Id.Id, componentSymbol.ComponentId, type, path));
229 }
230 else
231 {
232 this.Messaging.Write(ErrorMessages.DuplicateComponentGuids(componentSymbol.SourceLineNumbers, componentSymbol.Id.Id, componentSymbol.ComponentId, type, path));
233 }
234 }
235 }
236 }
237
238 private Dictionary<string, IResolvedDirectory> ResolveDirectoryTargetPaths()
239 {
240 var directories = this.Section.Symbols.OfType<DirectorySymbol>().ToList();
241
242 var targetPathsByDirectoryId = new Dictionary<string, IResolvedDirectory>(directories.Count);
243
244 // Get the target paths for all directories.
245 foreach (var directory in directories)
246 {
247 // If the directory Id already exists, we will skip it here since
248 // checking for duplicate primary keys is done later when importing tables
249 // into database
250 if (targetPathsByDirectoryId.ContainsKey(directory.Id.Id))
251 {
252 continue;
253 }
254
255 var resolvedDirectory = this.BackendHelper.CreateResolvedDirectory(directory.ParentDirectoryRef, directory.Name);
256 targetPathsByDirectoryId.Add(directory.Id.Id, resolvedDirectory);
257 }
258
259 return targetPathsByDirectoryId;
260 }
261 }
262}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/ValidateComponentGuidsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/ValidateComponentGuidsCommand.cs
deleted file mode 100644
index 5cad9247..00000000
--- a/src/WixToolset.Core.WindowsInstaller/Bind/ValidateComponentGuidsCommand.cs
+++ /dev/null
@@ -1,63 +0,0 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Core.WindowsInstaller.Bind
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using WixToolset.Data;
9 using WixToolset.Data.Symbols;
10 using WixToolset.Extensibility.Services;
11
12 /// <summary>
13 /// Validate that there are no duplicate GUIDs in the output.
14 /// </summary>
15 /// <remarks>
16 /// Duplicate GUIDs without conditions are an error condition; with conditions, it's a
17 /// warning, as the conditions might be mutually exclusive.
18 /// </remarks>
19 internal class ValidateComponentGuidsCommand
20 {
21 internal ValidateComponentGuidsCommand(IMessaging messaging, IntermediateSection section)
22 {
23 this.Messaging = messaging;
24 this.Section = section;
25 }
26
27 private IMessaging Messaging { get; }
28
29 private IntermediateSection Section { get; }
30
31 public void Execute()
32 {
33 var componentGuidConditions = new Dictionary<string, bool>();
34
35 foreach (var componentSymbol in this.Section.Symbols.OfType<ComponentSymbol>())
36 {
37 // We don't care about unmanaged components and if there's a * GUID remaining,
38 // there's already an error that prevented it from being replaced with a real GUID.
39 if (!String.IsNullOrEmpty(componentSymbol.ComponentId) && "*" != componentSymbol.ComponentId)
40 {
41 var thisComponentHasCondition = !String.IsNullOrEmpty(componentSymbol.Condition);
42 var allComponentsHaveConditions = thisComponentHasCondition;
43
44 if (componentGuidConditions.TryGetValue(componentSymbol.ComponentId, out var alreadyCheckedCondition))
45 {
46 allComponentsHaveConditions = thisComponentHasCondition && alreadyCheckedCondition;
47
48 if (allComponentsHaveConditions)
49 {
50 this.Messaging.Write(WarningMessages.DuplicateComponentGuidsMustHaveMutuallyExclusiveConditions(componentSymbol.SourceLineNumbers, componentSymbol.Id.Id, componentSymbol.ComponentId));
51 }
52 else
53 {
54 this.Messaging.Write(ErrorMessages.DuplicateComponentGuids(componentSymbol.SourceLineNumbers, componentSymbol.Id.Id, componentSymbol.ComponentId));
55 }
56 }
57
58 componentGuidConditions[componentSymbol.ComponentId] = allComponentsHaveConditions;
59 }
60 }
61 }
62 }
63}
diff --git a/src/WixToolset.Core/Compiler.cs b/src/WixToolset.Core/Compiler.cs
index c5f3a763..c39bec70 100644
--- a/src/WixToolset.Core/Compiler.cs
+++ b/src/WixToolset.Core/Compiler.cs
@@ -2454,24 +2454,27 @@ namespace WixToolset.Core
2454 } 2454 }
2455 } 2455 }
2456 2456
2457 // check for conditions that exclude this component from using generated guids 2457 // Check for conditions that exclude this component from using implicit ids and/or generated guids.
2458 var isGeneratableGuidOk = "*" == guid; 2458 var allowImplicitIds = true;
2459 if (isGeneratableGuidOk) 2459 if (encounteredODBCDataSource || ComponentKeyPathType.Directory == keyPathType)
2460 { 2460 {
2461 if (encounteredODBCDataSource || ComponentKeyPathType.Directory == keyPathType) 2461 allowImplicitIds = false;
2462 if (guid == "*")
2462 { 2463 {
2463 this.Core.Write(ErrorMessages.IllegalComponentWithAutoGeneratedGuid(sourceLineNumbers)); 2464 this.Core.Write(ErrorMessages.IllegalComponentWithAutoGeneratedGuid(sourceLineNumbers));
2464 isGeneratableGuidOk = false;
2465 } 2465 }
2466 else if (0 < files && ComponentKeyPathType.Registry == keyPathType) 2466 }
2467 else if (0 < files && ComponentKeyPathType.Registry == keyPathType)
2468 {
2469 allowImplicitIds = false;
2470 if (guid == "*")
2467 { 2471 {
2468 this.Core.Write(ErrorMessages.IllegalComponentWithAutoGeneratedGuid(sourceLineNumbers, true)); 2472 this.Core.Write(ErrorMessages.IllegalComponentWithAutoGeneratedGuid(sourceLineNumbers, true));
2469 isGeneratableGuidOk = false;
2470 } 2473 }
2471 } 2474 }
2472 2475
2473 // check for implicit KeyPath which can easily be accidentally changed 2476 // Check for implicit KeyPath which can easily be accidentally changed
2474 if (this.ShowPedanticMessages && !keyFound && !isGeneratableGuidOk) 2477 if (this.ShowPedanticMessages && !keyFound && !allowImplicitIds)
2475 { 2478 {
2476 this.Core.Write(ErrorMessages.ImplicitComponentKeyPath(sourceLineNumbers, id.Id)); 2479 this.Core.Write(ErrorMessages.ImplicitComponentKeyPath(sourceLineNumbers, id.Id));
2477 } 2480 }
@@ -2481,7 +2484,7 @@ namespace WixToolset.Core
2481 // generatable guid must be met. 2484 // generatable guid must be met.
2482 if (componentIdPlaceholder == id.Id) 2485 if (componentIdPlaceholder == id.Id)
2483 { 2486 {
2484 if (isGeneratableGuidOk || keyFound && !String.IsNullOrEmpty(keyPath)) 2487 if (allowImplicitIds || keyFound && !String.IsNullOrEmpty(keyPath))
2485 { 2488 {
2486 this.componentIdPlaceholders.Add(componentIdPlaceholder, keyPath); 2489 this.componentIdPlaceholders.Add(componentIdPlaceholder, keyPath);
2487 2490
diff --git a/src/test/WixToolsetTest.CoreIntegration/ComponentFixture.cs b/src/test/WixToolsetTest.CoreIntegration/ComponentFixture.cs
new file mode 100644
index 00000000..d24ba08c
--- /dev/null
+++ b/src/test/WixToolsetTest.CoreIntegration/ComponentFixture.cs
@@ -0,0 +1,45 @@
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 Xunit;
12
13 public class ComponentFixture
14 {
15 [Fact]
16 public void CanDetectDuplicateComponentGuids()
17 {
18 var folder = TestData.Get(@"TestData");
19
20 using (var fs = new DisposableFileSystem())
21 {
22 var baseFolder = fs.GetFolder();
23 var intermediateFolder = Path.Combine(baseFolder, "obj");
24 var msiPath = Path.Combine(baseFolder, @"bin\test.msi");
25
26 var result = WixRunner.Execute(new[]
27 {
28 "build",
29 Path.Combine(folder, "Component", "GuidCollision.wxs"),
30 Path.Combine(folder, "ProductWithComponentGroupRef", "Product.wxs"),
31 "-bindpath", Path.Combine(folder, "SingleFile", "data"),
32 "-intermediateFolder", intermediateFolder,
33 "-o", msiPath
34 });
35
36 var errors = result.Messages.Where(m => m.Level == MessageLevel.Error);
37 Array.Equals(new[]
38 {
39 369,
40 369
41 }, errors.Select(e => e.Id).ToArray());
42 }
43 }
44 }
45}
diff --git a/src/test/WixToolsetTest.CoreIntegration/TestData/Component/GuidCollision.wxs b/src/test/WixToolsetTest.CoreIntegration/TestData/Component/GuidCollision.wxs
new file mode 100644
index 00000000..a0e921cb
--- /dev/null
+++ b/src/test/WixToolsetTest.CoreIntegration/TestData/Component/GuidCollision.wxs
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="utf-8"?>
2<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 <Fragment>
4 <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
5 <Component Guid="5917d193-7b5f-4d5d-bc2e-06aa210699cb">
6 <RegistryValue Root="HKLM" Key="SOFTWARE\WixToolset" Name="Test" Value="test value" />
7 </Component>
8
9 <Component Guid="5917d193-7b5f-4d5d-bc2e-06aa210699cb">
10 <File Source="test.txt" />
11 </Component>
12 </ComponentGroup>
13 </Fragment>
14</Wix>