diff --git a/src/wix/WixToolset.Core/Compiler.cs b/src/wix/WixToolset.Core/Compiler.cs
index 3b3c62d4f..e359f3c14 100644
--- a/src/wix/WixToolset.Core/Compiler.cs
+++ b/src/wix/WixToolset.Core/Compiler.cs
@@ -5070,6 +5070,75 @@ private void ParseExtensionElement(XElement node, string componentId, YesNoType
}
}
+ private void ParseFileNamingAttributes(XElement node, string sourcePath, string directoryId, bool isNakedFile, out Identifier id, out string name, out string shortName, out string source)
+ {
+ var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node);
+ name = null;
+ id = null;
+ shortName = null;
+ source = sourcePath; // assume we'll use the parents as the source for this file
+ var sourceSet = false;
+
+ foreach (var attrib in node.Attributes())
+ {
+ if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || CompilerCore.WixNamespace == attrib.Name.Namespace)
+ {
+ switch (attrib.Name.LocalName)
+ {
+ case "Id":
+ id = this.Core.GetAttributeIdentifier(sourceLineNumbers, attrib);
+ break;
+ case "Name":
+ name = this.Core.GetAttributeLongFilename(sourceLineNumbers, attrib, false);
+ break;
+ case "ShortName":
+ shortName = this.Core.GetAttributeShortFilename(sourceLineNumbers, attrib, false);
+ break;
+ case "Source":
+ source = this.Core.GetAttributeValue(sourceLineNumbers, attrib);
+ sourceSet = true;
+ break;
+ }
+ }
+ }
+
+ if (sourceSet && !source.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) && null == name)
+ {
+ name = Path.GetFileName(source);
+ if (!this.Core.IsValidLongFilename(name, false))
+ {
+ this.Core.Write(ErrorMessages.IllegalLongFilename(sourceLineNumbers, node.Name.LocalName, "Source", name));
+ }
+ }
+
+ if (name == null)
+ {
+ if (shortName == null)
+ {
+ this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Name"));
+ }
+ else
+ {
+ name = shortName;
+ shortName = null;
+ }
+ }
+
+ if (String.IsNullOrEmpty(source))
+ {
+ source = name;
+ }
+ else if (source.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)) // if source relies on parent directories, append the file name
+ {
+ source = Path.Combine(source, name);
+ }
+
+ if (null == id && !isNakedFile)
+ {
+ id = this.Core.CreateIdentifier("fil", directoryId, name);
+ }
+ }
+
///
/// Parses a File element's attributes.
///
@@ -5077,6 +5146,9 @@ private void ParseExtensionElement(XElement node, string componentId, YesNoType
/// Parent's component id.
/// Ancestor's directory id.
/// Disk id inherited from parent component.
+ /// Already-parsed or defaulted id.
+ /// Already-parsed or defaulted name.
+ /// Already-parsed short name.
/// Default source path of parent directory.
/// This will be set with the possible keyPath for the parent component.
/// Component GUID (including `*`).
@@ -5084,16 +5156,14 @@ private void ParseExtensionElement(XElement node, string componentId, YesNoType
/// Outgoing file symbol containing parsed attributes.
/// Outgoing assembly symbol containing parsed attributes.
/// Yes if this element was marked as the parent component's key path, No if explicitly marked as not being a key path, or NotSet otherwise.
- private YesNoType ParseFileElementAttributes(XElement node, string componentId, string directoryId, int diskId, string sourcePath, out Identifier possibleKeyPath, string componentGuid, bool isNakedFile, out FileSymbol fileSymbol, out AssemblySymbol assemblySymbol)
+ private YesNoType ParseFileElementOtherAttributes(XElement node, string componentId, string directoryId, int diskId, Identifier id, string name, string shortName, string sourcePath, out Identifier possibleKeyPath, string componentGuid, bool isNakedFile, out FileSymbol fileSymbol, out AssemblySymbol assemblySymbol)
{
var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node);
- Identifier id = null;
var assemblyType = AssemblyType.NotAnAssembly;
string assemblyApplication = null;
string assemblyManifest = null;
string bindPath = null;
- //int bits = MsiInterop.MsidbFileAttributesVital;
var readOnly = false;
var checksum = false;
bool? compressed = null;
@@ -5107,7 +5177,6 @@ private YesNoType ParseFileElementAttributes(XElement node, string componentId,
string defaultVersion = null;
string fontTitle = null;
var keyPath = YesNoType.NotSet;
- string name = null;
var patchGroup = CompilerConstants.IntegerNotSet;
var patchIgnore = false;
var patchIncludeWholeFile = false;
@@ -5121,9 +5190,6 @@ private YesNoType ParseFileElementAttributes(XElement node, string componentId,
string procArch = null;
int? selfRegCost = null;
- string shortName = null;
- var source = sourcePath; // assume we'll use the parents as the source for this file
- var sourceSet = false;
fileSymbol = null;
assemblySymbol = null;
@@ -5134,6 +5200,13 @@ private YesNoType ParseFileElementAttributes(XElement node, string componentId,
{
switch (attrib.Name.LocalName)
{
+ case "Id":
+ case "Name":
+ case "ShortName":
+ case "Source":
+ // Handled in ParseFileNamingAttributes
+ break;
+
case "Bitness":
case "Condition":
case "Directory":
@@ -5144,9 +5217,6 @@ private YesNoType ParseFileElementAttributes(XElement node, string componentId,
this.Messaging.Write(ErrorMessages.IllegalAttributeWhenNested(sourceLineNumbers, attrib.Name.LocalName));
}
break;
- case "Id":
- id = this.Core.GetAttributeIdentifier(sourceLineNumbers, attrib);
- break;
case "Assembly":
var assemblyValue = this.Core.GetAttributeValue(sourceLineNumbers, attrib);
switch (assemblyValue)
@@ -5225,9 +5295,6 @@ private YesNoType ParseFileElementAttributes(XElement node, string componentId,
case "KeyPath":
keyPath = this.Core.GetAttributeYesNoValue(sourceLineNumbers, attrib);
break;
- case "Name":
- name = this.Core.GetAttributeLongFilename(sourceLineNumbers, attrib, false);
- break;
case "PatchGroup":
patchGroup = this.Core.GetAttributeIntegerValue(sourceLineNumbers, attrib, 1, Int32.MaxValue);
break;
@@ -5273,13 +5340,6 @@ private YesNoType ParseFileElementAttributes(XElement node, string componentId,
case "SelfRegCost":
selfRegCost = this.Core.GetAttributeIntegerValue(sourceLineNumbers, attrib, 0, Int16.MaxValue);
break;
- case "ShortName":
- shortName = this.Core.GetAttributeShortFilename(sourceLineNumbers, attrib, false);
- break;
- case "Source":
- source = this.Core.GetAttributeValue(sourceLineNumbers, attrib);
- sourceSet = true;
- break;
case "System":
if (YesNoType.Yes == this.Core.GetAttributeYesNoValue(sourceLineNumbers, attrib))
{
@@ -5326,33 +5386,6 @@ private YesNoType ParseFileElementAttributes(XElement node, string componentId,
}
}
- if (sourceSet && !source.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) && null == name)
- {
- name = Path.GetFileName(source);
- if (!this.Core.IsValidLongFilename(name, false))
- {
- this.Core.Write(ErrorMessages.IllegalLongFilename(sourceLineNumbers, node.Name.LocalName, "Source", name));
- }
- }
-
- if (name == null)
- {
- if (shortName == null)
- {
- this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Name"));
- }
- else
- {
- name = shortName;
- shortName = null;
- }
- }
-
- if (null == id)
- {
- id = this.Core.CreateIdentifier("fil", directoryId, name);
- }
-
if (null != defaultVersion && null != companionFile)
{
this.Core.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(sourceLineNumbers, node.Name.LocalName, "DefaultVersion", "CompanionFile", companionFile));
@@ -5400,15 +5433,6 @@ private YesNoType ParseFileElementAttributes(XElement node, string componentId,
patchAttributes |= PatchAttributeType.AllowIgnoreOnError;
}
- if (String.IsNullOrEmpty(source))
- {
- source = name;
- }
- else if (source.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)) // if source relies on parent directories, append the file name
- {
- source = Path.Combine(source, name);
- }
-
var attributes = FileSymbolAttributes.None;
attributes |= readOnly ? FileSymbolAttributes.ReadOnly : 0;
attributes |= hidden ? FileSymbolAttributes.Hidden : 0;
@@ -5430,7 +5454,7 @@ private YesNoType ParseFileElementAttributes(XElement node, string componentId,
DirectoryRef = directoryId,
DiskId = (CompilerConstants.IntegerNotSet == diskId) ? null : (int?)diskId,
- Source = new IntermediateFieldPathValue { Path = source },
+ Source = new IntermediateFieldPathValue { Path = sourcePath },
FontTitle = fontTitle,
SelfRegCost = selfRegCost,
@@ -5577,7 +5601,9 @@ private void ParseFileElementChildren(XElement node, FileSymbol fileSymbol, YesN
/// Yes if this element was marked as the parent component's key path, No if explicitly marked as not being a key path, or NotSet otherwise.
private YesNoType ParseFileElement(XElement node, string componentId, string directoryId, int diskId, string sourcePath, out Identifier possibleKeyPath, bool win64Component, string componentGuid)
{
- var keyPath = this.ParseFileElementAttributes(node, componentId, directoryId, diskId, sourcePath, out possibleKeyPath, componentGuid, isNakedFile: false, out var fileSymbol, out var assemblySymbol);
+ this.ParseFileNamingAttributes(node, sourcePath, directoryId, isNakedFile: false, out var id, out var name, out var shortName, out var source);
+
+ var keyPath = this.ParseFileElementOtherAttributes(node, componentId, directoryId, diskId, id, name, shortName, source, out possibleKeyPath, componentGuid, isNakedFile: false, out var fileSymbol, out var assemblySymbol);
if (!this.Core.EncounteredError)
{
@@ -5610,8 +5636,6 @@ private void ParseNakedFileElement(XElement node, ComplexReferenceParentType par
string condition = null;
string subdirectory = null;
- var keyPath = this.ParseFileElementAttributes(node, "@WixTemporaryComponentId", directoryId, diskId: CompilerConstants.IntegerNotSet, sourcePath, out var _, componentGuid: "*", isNakedFile: true, out var fileSymbol, out var assemblySymbol);
-
if (!this.Core.EncounteredError)
{
// Naked files have additional attributes to handle common component attributes.
@@ -5661,13 +5685,23 @@ private void ParseNakedFileElement(XElement node, ComplexReferenceParentType par
directoryId = this.HandleSubdirectory(sourceLineNumbers, node, directoryId, subdirectory, "Directory", "Subdirectory");
- this.Core.AddSymbol(new ComponentSymbol(sourceLineNumbers, fileSymbol.Id)
+ this.ParseFileNamingAttributes(node, sourcePath, directoryId, isNakedFile: true, out var id, out var name, out var shortName, out var source);
+
+ // Now that we have all the data we need to generate a good id, do
+ // so and create a file and component symbol with the right data.
+ id = id ?? this.Core.CreateIdentifier("nkf", directoryId, name, condition, win64.ToString());
+
+ var keyPath = this.ParseFileElementOtherAttributes(node, id.Id, directoryId, diskId: CompilerConstants.IntegerNotSet, id, name, shortName, source, out var _, componentGuid: "*", isNakedFile: true, fileSymbol: out var fileSymbol, assemblySymbol: out var assemblySymbol);
+
+ this.Core.AddSymbol(fileSymbol);
+
+ this.Core.AddSymbol(new ComponentSymbol(sourceLineNumbers, id)
{
ComponentId = "*",
DirectoryRef = directoryId,
Location = ComponentLocation.LocalOnly,
Condition = condition,
- KeyPath = fileSymbol.Id.Id,
+ KeyPath = id.Id,
KeyPathType = ComponentKeyPathType.File,
DisableRegistryReflection = false,
NeverOverwrite = false,
@@ -5679,9 +5713,6 @@ private void ParseNakedFileElement(XElement node, ComplexReferenceParentType par
Win64 = win64,
});
- fileSymbol.ComponentRef = fileSymbol.Id.Id;
- this.Core.AddSymbol(fileSymbol);
-
if (assemblySymbol != null)
{
this.Core.AddSymbol(assemblySymbol);
@@ -5697,7 +5728,7 @@ private void ParseNakedFileElement(XElement node, ComplexReferenceParentType par
else if (ComplexReferenceParentType.Unknown != parentType && null != parentId) // if parent was provided, add a complex reference to that.
{
// If the naked file's component is defined directly under a feature, then mark the complex reference primary.
- this.Core.CreateComplexReference(sourceLineNumbers, parentType, parentId, null, ComplexReferenceChildType.Component, fileSymbol.Id.Id, ComplexReferenceParentType.Feature == parentType);
+ this.Core.CreateComplexReference(sourceLineNumbers, parentType, parentId, null, ComplexReferenceChildType.Component, id.Id, ComplexReferenceParentType.Feature == parentType);
}
}
}
diff --git a/src/wix/WixToolset.Core/Linker.cs b/src/wix/WixToolset.Core/Linker.cs
index 43a41eac9..b10a12d84 100644
--- a/src/wix/WixToolset.Core/Linker.cs
+++ b/src/wix/WixToolset.Core/Linker.cs
@@ -716,7 +716,7 @@ private void FlattenGroup(string parentTypeAndId, Stack loopDetector, Di
var childTypeAndId = this.CombineTypeAndId(wixComplexReferenceRow.ChildType, wixComplexReferenceRow.Child);
if (loopDetector.Contains(childTypeAndId))
{
- // Create a comma delimited list of the references that participate in the
+ // Create an arrow-delimited list of the references that participate in the
// loop for the error message. Start at the bottom of the stack and work the
// way up to present the loop as a directed graph.
var loop = String.Join(" -> ", loopDetector);
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs
index 8e4964ded..a8af2b4d3 100644
--- a/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs
@@ -355,6 +355,41 @@ public void CanGetVerboseHarvestingDetails()
}, additionalCommandLineArguments: "-v");
}
+ [Fact]
+ public void CanHarvestFilesWithDuplicateNames()
+ {
+ var expectedFiles = new[]
+ {
+ "File:fls47G631z.20kTPzBwIPjGuWifsVo\tfls47G631z.20kTPzBwIPjGuWifsVo\tfile.x\t0\t\t\t512\t2",
+ "File:flshOXYGoD0VHcQhrkIILRPNOVbYuM\tflshOXYGoD0VHcQhrkIILRPNOVbYuM\tfile.x\t0\t\t\t512\t1",
+ "File:flsp2QdFvBeSwll1i5tN0jM72w3Hu4\tflsp2QdFvBeSwll1i5tN0jM72w3Hu4\tfile.x\t0\t\t\t512\t3",
+ "File:flsqopW5ihQpwCTF7t_51GkHd4Hf6s\tflsqopW5ihQpwCTF7t_51GkHd4Hf6s\tfile.x\t0\t\t\t512\t4",
+ };
+
+ var expectedFilesAndDirectories = new[]
+ {
+ @"fls47G631z.20kTPzBwIPjGuWifsVo=PFiles\Example Corporation MsiPackage\b\file.x",
+ @"flshOXYGoD0VHcQhrkIILRPNOVbYuM=PFiles\Example Corporation MsiPackage\a\file.x",
+ @"flsp2QdFvBeSwll1i5tN0jM72w3Hu4=PFiles\Example Corporation MsiPackage\c\file.x",
+ @"flsqopW5ihQpwCTF7t_51GkHd4Hf6s=PFiles\Example Corporation MsiPackage\d\file.x",
+ };
+
+ Build("DuplicateNames.wxs", (msiPath, result) => AssertFileAndComponentIdsAndTargetPaths(msiPath, result, expectedFiles, expectedFilesAndDirectories));
+ }
+
+ private static void AssertFileAndComponentIdsAndTargetPaths(string msiPath, WixRunnerResult result, string[] expectedFiles, string[] expectedFilesAndDirectories)
+ {
+ result.AssertSuccess();
+
+ var files = Query.QueryDatabase(msiPath, new[] { "File" })
+ .OrderBy(s => s)
+ .ToArray();
+
+ Assert.Equal(expectedFiles, files);
+
+ AssertFileIdsAndTargetPaths(msiPath, expectedFilesAndDirectories);
+ }
+
private static void AssertFileIdsAndTargetPaths(string msiPath, string[] expected)
{
var pkg = new WixToolset.Dtf.WindowsInstaller.Package.InstallPackage(msiPath,
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/NakedFileFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/NakedFileFixture.cs
index ae13421de..3142b9da4 100644
--- a/src/wix/test/WixToolsetTest.CoreIntegration/NakedFileFixture.cs
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/NakedFileFixture.cs
@@ -114,6 +114,13 @@ public void CanBuildNakedFilesUnderPackage()
AssertFileComponentIds(4, rows);
}
+ [Fact]
+ public void CanBuildNakedFilesUnderPackageWithDuplicateNames()
+ {
+ var rows = BuildAndQueryComponentAndFileTables("DuplicateNames.wxs");
+ AssertFileComponentIds(5, rows);
+ }
+
[Fact]
public void CanBuildNakedFilesUnderPackageWithDefaultInstallFolder()
{
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DuplicateNames.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DuplicateNames.wxs
new file mode 100644
index 000000000..c3b08b69e
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DuplicateNames.wxs
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/dupes/a/file.x b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/dupes/a/file.x
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/dupes/b/file.x b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/dupes/b/file.x
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/dupes/c/file.x b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/dupes/c/file.x
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/dupes/d/file.x b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/dupes/d/file.x
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/DuplicateNames.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/DuplicateNames.wxs
new file mode 100644
index 000000000..edccc5044
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/DuplicateNames.wxs
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/Package.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/Package.wxs
index e5dd94e02..9ddd4787b 100644
--- a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/Package.wxs
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/Package.wxs
@@ -1,10 +1,8 @@
-
-
-
-
-
-
+
+
+
+
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/PackageWithDefaultInstallFolder.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/PackageWithDefaultInstallFolder.wxs
index 824f35014..01ca77350 100644
--- a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/PackageWithDefaultInstallFolder.wxs
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/NakedFile/PackageWithDefaultInstallFolder.wxs
@@ -1,10 +1,8 @@
-
-
-
-
-
-
+
+
+
+