Skip to content

Commit 8445e4e

Browse files
authored
Allow exact duplicate input entries when bundling single file (#50476)
SDK can (and will) produce inputs for bundling which contain exact duplicate entries (same source path and same target relative path). Currently publishing such app as non-single-file works just fine (files are overwritten), but publishing it as single-file fails. Fixing this in the SDK seems like a rather complex problem - see dotnet/sdk#16576 for more details. There's no harm in allowing exact duplicates and ignoring them when bundling (only one copy of the file is bundled) as that will be the same behavior as non-single-file publish. If the duplicates are not exact (different source path) then still reject those. SDK currently allows that, but it's very problematic (effectively random output).
1 parent 7fb3997 commit 8445e4e

File tree

2 files changed

+100
-7
lines changed

2 files changed

+100
-7
lines changed

src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,6 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
260260
throw new ArgumentException("Invalid input specification: Must specify the host binary");
261261
}
262262

263-
var bundleRelativePathCollision = fileSpecs.GroupBy(file => file.BundleRelativePath).FirstOrDefault(g => g.Count() > 1);
264-
if (bundleRelativePathCollision != null)
265-
{
266-
string fileSpecPaths = string.Join(", ", bundleRelativePathCollision.Select(file => "'" + file.SourcePath + "'"));
267-
throw new ArgumentException($"Invalid input specification: Found entries {fileSpecPaths} with the same BundleRelativePath '{bundleRelativePathCollision.Key}'");
268-
}
269-
270263
string bundlePath = Path.Combine(OutputDir, HostName);
271264
if (File.Exists(bundlePath))
272265
{
@@ -275,6 +268,11 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
275268

276269
BinaryUtils.CopyFile(hostSource, bundlePath);
277270

271+
// Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app
272+
// We can't really make assumptions about the file systems (even on Linux there can be case insensitive file systems
273+
// and vice versa for Windows). So it's safer to do case sensitive comparison everywhere.
274+
var relativePathToSpec = new Dictionary<string, FileSpec>(StringComparer.Ordinal);
275+
278276
long headerOffset = 0;
279277
using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath)))
280278
{
@@ -305,6 +303,21 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
305303
continue;
306304
}
307305

306+
if (relativePathToSpec.TryGetValue(fileSpec.BundleRelativePath, out var existingFileSpec))
307+
{
308+
if (!string.Equals(fileSpec.SourcePath, existingFileSpec.SourcePath, StringComparison.Ordinal))
309+
{
310+
throw new ArgumentException($"Invalid input specification: Found entries '{fileSpec.SourcePath}' and '{existingFileSpec.SourcePath}' with the same BundleRelativePath '{fileSpec.BundleRelativePath}'");
311+
}
312+
313+
// Exact duplicate - intentionally skip and don't include a second copy in the bundle
314+
continue;
315+
}
316+
else
317+
{
318+
relativePathToSpec.Add(fileSpec.BundleRelativePath, fileSpec);
319+
}
320+
308321
using (FileStream file = File.OpenRead(fileSpec.SourcePath))
309322
{
310323
FileType targetType = Target.TargetSpecificFileType(type);

src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Bundle.Tests/BundlerConsistencyTests.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,34 @@ public void TestWithoutSpecifyingHostFails()
7070
Assert.Throws<ArgumentException>(() => bundler.GenerateBundle(fileSpecs));
7171
}
7272

73+
[Fact]
74+
public void TestWithExactDuplicateEntriesPasses()
75+
{
76+
var fixture = sharedTestState.TestFixture.Copy();
77+
78+
var hostName = BundleHelper.GetHostName(fixture);
79+
var bundleDir = BundleHelper.GetBundleDir(fixture);
80+
var targetOS = BundleHelper.GetTargetOS(fixture.CurrentRid);
81+
var targetArch = BundleHelper.GetTargetArch(fixture.CurrentRid);
82+
83+
// Generate a file specification with duplicate entries
84+
var fileSpecs = new List<FileSpec>();
85+
fileSpecs.Add(new FileSpec(BundleHelper.GetHostPath(fixture), BundleHelper.GetHostName(fixture)));
86+
string appPath = BundleHelper.GetAppPath(fixture);
87+
fileSpecs.Add(new FileSpec(appPath, "rel/app.repeat.dll"));
88+
fileSpecs.Add(new FileSpec(appPath, "rel/app.repeat.dll"));
89+
string systemLibPath = Path.Join(BundleHelper.GetPublishPath(fixture), "System.dll");
90+
fileSpecs.Add(new FileSpec(systemLibPath, "rel/system.repeat.dll"));
91+
fileSpecs.Add(new FileSpec(systemLibPath, "rel/system.repeat.dll"));
92+
93+
Bundler bundler = new Bundler(hostName, bundleDir.FullName, targetOS: targetOS, targetArch: targetArch);
94+
bundler.GenerateBundle(fileSpecs);
95+
96+
// Exact duplicates are not duplicated in the bundle
97+
bundler.BundleManifest.Files.Where(entry => entry.RelativePath.Equals("rel/app.repeat.dll")).Single().Type.Should().Be(FileType.Assembly);
98+
bundler.BundleManifest.Files.Where(entry => entry.RelativePath.Equals("rel/system.repeat.dll")).Single().Type.Should().Be(FileType.Assembly);
99+
}
100+
73101
[Fact]
74102
public void TestWithDuplicateEntriesFails()
75103
{
@@ -93,6 +121,58 @@ public void TestWithDuplicateEntriesFails()
93121
.And.Contain(BundleHelper.GetAppPath(fixture));
94122
}
95123

124+
[Fact]
125+
public void TestWithCaseSensitiveDuplicateEntriesPasses()
126+
{
127+
var fixture = sharedTestState.TestFixture.Copy();
128+
129+
var hostName = BundleHelper.GetHostName(fixture);
130+
var bundleDir = BundleHelper.GetBundleDir(fixture);
131+
var targetOS = BundleHelper.GetTargetOS(fixture.CurrentRid);
132+
var targetArch = BundleHelper.GetTargetArch(fixture.CurrentRid);
133+
134+
// Generate a file specification with duplicate entries
135+
var fileSpecs = new List<FileSpec>();
136+
fileSpecs.Add(new FileSpec(BundleHelper.GetHostPath(fixture), BundleHelper.GetHostName(fixture)));
137+
fileSpecs.Add(new FileSpec(BundleHelper.GetAppPath(fixture), "rel/app.repeat.dll"));
138+
fileSpecs.Add(new FileSpec(Path.Join(BundleHelper.GetPublishPath(fixture), "System.dll"), "rel/app.Repeat.dll"));
139+
140+
Bundler bundler = new Bundler(hostName, bundleDir.FullName, targetOS: targetOS, targetArch: targetArch);
141+
bundler.GenerateBundle(fileSpecs);
142+
143+
bundler.BundleManifest.Files.Where(entry => entry.RelativePath.Equals("rel/app.repeat.dll")).Single().Type.Should().Be(FileType.Assembly);
144+
bundler.BundleManifest.Files.Where(entry => entry.RelativePath.Equals("rel/app.Repeat.dll")).Single().Type.Should().Be(FileType.Assembly);
145+
}
146+
147+
[Fact]
148+
public void TestWithMultipleDuplicateEntriesFails()
149+
{
150+
var fixture = sharedTestState.TestFixture.Copy();
151+
152+
var hostName = BundleHelper.GetHostName(fixture);
153+
var bundleDir = BundleHelper.GetBundleDir(fixture);
154+
var targetOS = BundleHelper.GetTargetOS(fixture.CurrentRid);
155+
var targetArch = BundleHelper.GetTargetArch(fixture.CurrentRid);
156+
157+
// Generate a file specification with duplicate entries
158+
var fileSpecs = new List<FileSpec>();
159+
fileSpecs.Add(new FileSpec(BundleHelper.GetHostPath(fixture), BundleHelper.GetHostName(fixture)));
160+
string appPath = BundleHelper.GetAppPath(fixture);
161+
fileSpecs.Add(new FileSpec(appPath, "rel/app.repeat.dll"));
162+
fileSpecs.Add(new FileSpec(appPath, "rel/app.repeat.dll"));
163+
string systemLibPath = Path.Join(BundleHelper.GetPublishPath(fixture), "System.dll");
164+
fileSpecs.Add(new FileSpec(appPath, "rel/system.repeat.dll"));
165+
fileSpecs.Add(new FileSpec(systemLibPath, "rel/system.repeat.dll"));
166+
167+
Bundler bundler = new Bundler(hostName, bundleDir.FullName, targetOS: targetOS, targetArch: targetArch);
168+
Assert.Throws<ArgumentException>(() => bundler.GenerateBundle(fileSpecs))
169+
.Message
170+
.Should().Contain("rel/system.repeat.dll")
171+
.And.NotContain("rel/app.repeat.dll")
172+
.And.Contain(appPath)
173+
.And.Contain(systemLibPath);
174+
}
175+
96176
[Fact]
97177
public void TestBaseNameComputation()
98178
{

0 commit comments

Comments
 (0)