Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;

namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// Describes the contents of a bundle.
/// </summary>
public sealed class BundleContents
{
/// <summary>
/// The host binary that serves as the bundle container.
/// </summary>
public FileSpec Host { get; }

/// <summary>
/// Files that will be embedded in the bundle.
/// </summary>
public IReadOnlyList<FileSpec> IncludedFiles { get; }

/// <summary>
/// Files that are excluded from the bundle and should be published alongside the host.
/// </summary>
public IReadOnlyList<FileSpec> ExcludedFiles { get; }
Comment thread
elinor-fung marked this conversation as resolved.

internal (FileSpec Spec, FileType Type)[] TypedIncludedFiles { get; }

internal BundleContents(FileSpec host, (FileSpec Spec, FileType Type)[] includedFiles, FileSpec[] excludedFiles)
{
Host = host;
TypedIncludedFiles = includedFiles;
IncludedFiles = System.Array.ConvertAll(includedFiles, x => x.Spec);
ExcludedFiles = excludedFiles;
}
}
}
98 changes: 69 additions & 29 deletions src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,43 +248,52 @@ private FileType InferType(FileSpec fileSpec)
internal static ReadOnlySpan<byte> BundleHeaderSignature => BundleHeaderPlaceholder.Slice(8);

/// <summary>
/// Generate a bundle, given the specification of embedded files
/// Generate a bundle, given the specification of embedded files.
/// </summary>
/// <param name="fileSpecs">
/// An enumeration FileSpecs for the files to be embedded.
///
/// Files in fileSpecs that are not bundled within the single file bundle,
/// and should be published as separate files are marked as "IsExcluded" by this method.
/// This doesn't include unbundled files that should be dropped, and not published as output.
/// An enumeration of FileSpecs for the files to be embedded.
/// Files that are excluded from the bundle have their <see cref="FileSpec.Excluded"/> flag set by this method.
/// </param>
/// <returns>
/// The full path the generated bundle file
/// The full path of the generated bundle file.
/// </returns>
/// <exceptions>
/// ArgumentException if input is invalid
/// ArgumentException if input is invalid.
/// IOExceptions and ArgumentExceptions from callees flow to the caller.
/// </exceptions>
public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
{
return GenerateBundle(ComputeBundleContents(fileSpecs));
}

/// <summary>
/// Generate a bundle from the given <see cref="BundleContents"/>,
/// as computed by <see cref="ComputeBundleContents"/>.
/// </summary>
/// <param name="bundleContents">
/// The bundle contents from <see cref="ComputeBundleContents"/>.
/// </param>
/// <returns>
/// The full path of the generated bundle file.
/// </returns>
public string GenerateBundle(BundleContents bundleContents)
{
#if NET
ArgumentNullException.ThrowIfNull(bundleContents);
#else
if (bundleContents is null)
{
throw new ArgumentNullException(nameof(bundleContents));
}
#endif

_tracer.Log($"Bundler Version: {BundlerMajorVersion}.{BundlerMinorVersion}");
_tracer.Log($"Bundle Version: {BundleManifest.BundleVersion}");
_tracer.Log($"Target Runtime: {_target}");
_tracer.Log($"Bundler Options: {_options}");
if (fileSpecs.Any(x => !x.IsValid()))
{
throw new ArgumentException("Invalid input specification: Found entry with empty source-path or bundle-relative-path.");
}
string hostSource;
try
{
hostSource = fileSpecs.Where(x => x.BundleRelativePath.Equals(_hostName)).Single().SourcePath;
}
catch (InvalidOperationException)
{
throw new ArgumentException("Invalid input specification: Must specify the host binary");
}

(FileSpec Spec, FileType Type)[] relativePathToSpec = GetFilteredFileSpecs(fileSpecs);
string hostSource = bundleContents.Host.SourcePath;
(FileSpec Spec, FileType Type)[] relativePathToSpec = bundleContents.TypedIncludedFiles;
Comment thread
elinor-fung marked this conversation as resolved.
long bundledFilesSize = 0;
// Conservatively estimate the size of bundled files.
// Assume no compression and worst case alignment for assemblies.
Expand Down Expand Up @@ -445,21 +454,50 @@ void FindBundleHeader()
return headerOffset != 0;
}

private (FileSpec Spec, FileType Type)[] GetFilteredFileSpecs(IEnumerable<FileSpec> fileSpecs)
/// <summary>
/// Compute which files would be included in or excluded from the bundle
/// for the given set of input file specs and the bundler's current options.
/// </summary>
/// <param name="fileSpecs">
/// An enumeration of FileSpecs for the files to potentially be embedded.
/// Files that are excluded from the bundle have their <see cref="FileSpec.Excluded"/> flag set by this method.
/// </param>
/// <returns>
/// A <see cref="BundleContents"/> describing which files would be included in
/// and excluded from the bundle.
/// </returns>
public BundleContents ComputeBundleContents(IReadOnlyList<FileSpec> fileSpecs)
{
if (fileSpecs.Any(x => !x.IsValid()))
{
throw new ArgumentException("Invalid input specification: Found entry with empty source-path or bundle-relative-path.");
}

FileSpec[] hostSpecs = fileSpecs.Where(x => IsHost(x.BundleRelativePath)).ToArray();
if (hostSpecs.Length != 1)
{
throw new ArgumentException($"Invalid input specification: Must specify exactly one entry for the host binary '{_hostName}'");
}

FileSpec hostSpec = hostSpecs[0];
var (included, excluded) = GetFilteredFileSpecs(fileSpecs.Where(x => x != hostSpec));
return new BundleContents(
hostSpec,
included,
excluded);
}
Comment thread
elinor-fung marked this conversation as resolved.

private ((FileSpec Spec, FileType Type)[] Included, FileSpec[] Excluded) GetFilteredFileSpecs(IEnumerable<FileSpec> fileSpecs)
{
// Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app
// We can't really make assumptions about the file systems (even on Linux there can be case insensitive file systems
// and vice versa for Windows). So it's safer to do case sensitive comparison everywhere.
var relativePathToSpec = new Dictionary<string, (FileSpec Spec, FileType Type)>(StringComparer.Ordinal);
var excluded = new List<FileSpec>();
foreach (var fileSpec in fileSpecs)
{
string relativePath = fileSpec.BundleRelativePath;

if (IsHost(relativePath))
{
continue;
}

if (ShouldIgnore(relativePath))
{
_tracer.Log($"Ignore: {relativePath}");
Expand All @@ -472,6 +510,7 @@ void FindBundleHeader()
{
_tracer.Log($"Exclude [{type}]: {relativePath}");
fileSpec.Excluded = true;
excluded.Add(fileSpec);
continue;
}

Expand All @@ -490,7 +529,8 @@ void FindBundleHeader()
relativePathToSpec.Add(fileSpec.BundleRelativePath, (fileSpec, type));
}
}
return relativePathToSpec.Values.ToArray();

return (relativePathToSpec.Values.ToArray(), excluded.ToArray());
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,113 @@ public void BundleOptions_IncludedExcludedFiles(BundleOptions options)
bundler.BundleManifest.Contains(otherContentName).Should().Be(options.HasFlag(BundleOptions.BundleOtherFiles));
}

[InlineData(BundleOptions.None)]
[InlineData(BundleOptions.BundleNativeBinaries)]
[InlineData(BundleOptions.BundleOtherFiles)]
[InlineData(BundleOptions.BundleAllContent)]
[InlineData(BundleOptions.BundleSymbolFiles)]
[Theory]
public void ComputeBundleContents(BundleOptions options)
{
TestApp app = sharedTestState.App;
string devJsonName = Path.GetFileName(app.RuntimeDevConfigJson);
string appSymbolName = $"{app.Name}.pdb";
string otherContentName = "other.txt";
FileSpec[] fileSpecs = new FileSpec[]
{
new FileSpec(Binaries.AppHost.FilePath, BundlerHostName),
new FileSpec(app.AppDll, Path.GetRelativePath(app.Location, app.AppDll)),
new FileSpec(app.DepsJson, Path.GetRelativePath(app.Location, app.DepsJson)),
new FileSpec(app.RuntimeConfigJson, Path.GetRelativePath(app.Location, app.RuntimeConfigJson)),
new FileSpec(app.RuntimeConfigJson, devJsonName),
new FileSpec(Path.Combine(app.Location, appSymbolName), appSymbolName),
new FileSpec(Binaries.CoreClr.FilePath, Binaries.CoreClr.FileName),
new FileSpec(app.RuntimeConfigJson, otherContentName),
};

Bundler bundler = CreateBundlerInstance(options);
BundleContents contents = bundler.ComputeBundleContents(fileSpecs);

// App's dll, .deps.json, and .runtimeconfig.json should always be included
Assert.Contains(contents.IncludedFiles, f => f.BundleRelativePath == Path.GetFileName(app.AppDll));
Assert.Contains(contents.IncludedFiles, f => f.BundleRelativePath == Path.GetFileName(app.DepsJson));
Assert.Contains(contents.IncludedFiles, f => f.BundleRelativePath == Path.GetFileName(app.RuntimeConfigJson));

// Host should be identified separately
Assert.Equal(BundlerHostName, contents.Host.BundleRelativePath);
Assert.DoesNotContain(contents.IncludedFiles, f => f.BundleRelativePath == BundlerHostName);
Assert.DoesNotContain(contents.ExcludedFiles, f => f.BundleRelativePath == BundlerHostName);

// App's .runtimeconfig.dev.json is ignored (not in either list)
Assert.DoesNotContain(contents.IncludedFiles, f => f.BundleRelativePath == devJsonName);
Assert.DoesNotContain(contents.ExcludedFiles, f => f.BundleRelativePath == devJsonName);

// Symbols
if (options.HasFlag(BundleOptions.BundleSymbolFiles))
{
Assert.Contains(contents.IncludedFiles, f => f.BundleRelativePath == appSymbolName);
Assert.DoesNotContain(contents.ExcludedFiles, f => f.BundleRelativePath == appSymbolName);
}
else
{
Assert.Contains(contents.ExcludedFiles, f => f.BundleRelativePath == appSymbolName);
Assert.DoesNotContain(contents.IncludedFiles, f => f.BundleRelativePath == appSymbolName);
}

// Native libraries
if (options.HasFlag(BundleOptions.BundleNativeBinaries))
{
Assert.Contains(contents.IncludedFiles, f => f.BundleRelativePath == Binaries.CoreClr.FileName);
Assert.DoesNotContain(contents.ExcludedFiles, f => f.BundleRelativePath == Binaries.CoreClr.FileName);
}
else
{
Assert.Contains(contents.ExcludedFiles, f => f.BundleRelativePath == Binaries.CoreClr.FileName);
Assert.DoesNotContain(contents.IncludedFiles, f => f.BundleRelativePath == Binaries.CoreClr.FileName);
}

// Other files
if (options.HasFlag(BundleOptions.BundleOtherFiles))
{
Assert.Contains(contents.IncludedFiles, f => f.BundleRelativePath == otherContentName);
Assert.DoesNotContain(contents.ExcludedFiles, f => f.BundleRelativePath == otherContentName);
}
else
{
Assert.Contains(contents.ExcludedFiles, f => f.BundleRelativePath == otherContentName);
Assert.DoesNotContain(contents.IncludedFiles, f => f.BundleRelativePath == otherContentName);
}

}

[Fact]
public void ComputeBundleContents_GenerateBundle()
{
TestApp app = sharedTestState.App;
FileSpec[] fileSpecs = new FileSpec[]
{
new FileSpec(Binaries.AppHost.FilePath, BundlerHostName),
new FileSpec(app.AppDll, Path.GetRelativePath(app.Location, app.AppDll)),
new FileSpec(app.DepsJson, Path.GetRelativePath(app.Location, app.DepsJson)),
new FileSpec(app.RuntimeConfigJson, Path.GetRelativePath(app.Location, app.RuntimeConfigJson)),
new FileSpec(Binaries.CoreClr.FilePath, Binaries.CoreClr.FileName),
};

// Generate bundle directly from file specs
Bundler directBundler = CreateBundlerInstance();
directBundler.GenerateBundle(fileSpecs);

// Generate bundle via ComputeBundleContents + GenerateBundle(BundleContents)
Bundler computedBundler = CreateBundlerInstance();
BundleContents contents = computedBundler.ComputeBundleContents(fileSpecs);
computedBundler.GenerateBundle(contents);

// Both paths should produce bundles with the same manifest entries
var directEntries = directBundler.BundleManifest.Files.Select(f => (f.RelativePath, f.Size, f.CompressedSize, f.Type)).OrderBy(e => e.RelativePath);
var computedEntries = computedBundler.BundleManifest.Files.Select(f => (f.RelativePath, f.Size, f.CompressedSize, f.Type)).OrderBy(e => e.RelativePath);
Assert.Equal(directEntries, computedEntries);
}

[Fact]
public void FileSizes()
{
Expand Down