/// <summary> /// 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 publised as output. /// </param> /// <returns> /// The full path the the generated bundle file /// </returns> /// <exceptions> /// ArgumentException if input is invalid /// IOExceptions and ArgumentExceptions from callees flow to the caller. /// </exceptions> public string GenerateBundle(IReadOnlyList <FileSpec> fileSpecs) { 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"); } string bundlePath = Path.Combine(OutputDir, HostName); if (File.Exists(bundlePath)) { Tracer.Log($"Ovewriting existing File {bundlePath}"); } BinaryUtils.CopyFile(hostSource, bundlePath); // 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>(StringComparer.Ordinal); long headerOffset = 0; using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath))) { Stream bundle = writer.BaseStream; bundle.Position = bundle.Length; foreach (var fileSpec in fileSpecs) { string relativePath = fileSpec.BundleRelativePath; if (IsHost(relativePath)) { continue; } if (ShouldIgnore(relativePath)) { Tracer.Log($"Ignore: {relativePath}"); continue; } FileType type = InferType(fileSpec); if (ShouldExclude(type, relativePath)) { Tracer.Log($"Exclude [{type}]: {relativePath}"); fileSpec.Excluded = true; continue; } if (relativePathToSpec.TryGetValue(fileSpec.BundleRelativePath, out var existingFileSpec)) { if (!string.Equals(fileSpec.SourcePath, existingFileSpec.SourcePath, StringComparison.Ordinal)) { throw new ArgumentException($"Invalid input specification: Found entries '{fileSpec.SourcePath}' and '{existingFileSpec.SourcePath}' with the same BundleRelativePath '{fileSpec.BundleRelativePath}'"); } // Exact duplicate - intentionally skip and don't include a second copy in the bundle continue; } else { relativePathToSpec.Add(fileSpec.BundleRelativePath, fileSpec); } using (FileStream file = File.OpenRead(fileSpec.SourcePath)) { FileType targetType = Target.TargetSpecificFileType(type); (long startOffset, long compressedSize) = AddToBundle(bundle, file, targetType); FileEntry entry = BundleManifest.AddEntry(targetType, file, relativePath, startOffset, compressedSize, Target.BundleMajorVersion); Tracer.Log($"Embed: {entry}"); } } // Write the bundle manifest headerOffset = BundleManifest.Write(writer); Tracer.Log($"Header Offset={headerOffset}"); Tracer.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}"); Tracer.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}"); } HostWriter.SetAsBundle(bundlePath, headerOffset); return(bundlePath); }
/// <summary> /// Generate a bundle, given the specification of embedded files /// </summary> /// <param name="fileSpecs"> /// An enumeration FileSpecs for the files to be embedded. /// </param> /// <returns> /// The full path the the generated bundle file /// </returns> /// <exceptions> /// ArgumentException if input is invalid /// IOExceptions and ArgumentExceptions from callees flow to the caller. /// </exceptions> public string GenerateBundle(IReadOnlyList <FileSpec> fileSpecs) { trace.Log($"Bundler version {Version}"); 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"); } if (fileSpecs.GroupBy(file => file.BundleRelativePath).Where(g => g.Count() > 1).Any()) { throw new ArgumentException("Invalid input specification: Found multiple entries with the same BundleRelativePath"); } string bundlePath = Path.Combine(OutputDir, HostName); if (File.Exists(bundlePath)) { trace.Log($"Ovewriting existing File {bundlePath}"); } BinaryUtils.CopyFile(hostSource, bundlePath); long headerOffset = 0; using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath))) { Stream bundle = writer.BaseStream; bundle.Position = bundle.Length; // Write the files from the specification into the bundle // Alignment: // Assemblies are written aligned at "AssemblyAlignment" bytes to facilitate loading from bundle // Remaining files (native binaries and other files) are written without alignment. // // The unaligned files are written first, followed by the aligned files, // and finally the bundle manifest. // TODO: Order file writes to minimize file size. List <Tuple <FileSpec, FileType> > ailgnedFiles = new List <Tuple <FileSpec, FileType> >(); bool NeedsAlignment(FileType type) => type == FileType.IL || type == FileType.Ready2Run; foreach (var fileSpec in fileSpecs) { if (!ShouldEmbed(fileSpec.BundleRelativePath)) { trace.Log($"Skip: {fileSpec.BundleRelativePath}"); continue; } using (FileStream file = File.OpenRead(fileSpec.SourcePath)) { FileType type = InferType(fileSpec.BundleRelativePath, file); if (NeedsAlignment(type)) { ailgnedFiles.Add(Tuple.Create(fileSpec, type)); continue; } long startOffset = AddToBundle(bundle, file, shouldAlign: false); FileEntry entry = BundleManifest.AddEntry(type, fileSpec.BundleRelativePath, startOffset, file.Length); trace.Log($"Embed: {entry}"); } } foreach (var tuple in ailgnedFiles) { var fileSpec = tuple.Item1; var type = tuple.Item2; using (FileStream file = File.OpenRead(fileSpec.SourcePath)) { long startOffset = AddToBundle(bundle, file, shouldAlign: true); FileEntry entry = BundleManifest.AddEntry(type, fileSpec.BundleRelativePath, startOffset, file.Length); trace.Log($"Embed: {entry}"); } } // Write the bundle manifest headerOffset = BundleManifest.Write(writer); trace.Log($"Header Offset={headerOffset}"); trace.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}"); trace.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}"); } HostWriter.SetAsBundle(bundlePath, headerOffset); return(bundlePath); }
/// <summary> /// 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 publised as output. /// </param> /// <returns> /// The full path the the generated bundle file /// </returns> /// <exceptions> /// ArgumentException if input is invalid /// IOExceptions and ArgumentExceptions from callees flow to the caller. /// </exceptions> public string GenerateBundle(IReadOnlyList <FileSpec> fileSpecs) { Tracer.Log($"Bundler version: {Manifest.CurrentVersion}"); Tracer.Log($"Bundler Header: {BundleManifest.DesiredVersion}"); 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"); } if (fileSpecs.GroupBy(file => file.BundleRelativePath).Where(g => g.Count() > 1).Any()) { throw new ArgumentException("Invalid input specification: Found multiple entries with the same BundleRelativePath"); } string bundlePath = Path.Combine(OutputDir, HostName); if (File.Exists(bundlePath)) { Tracer.Log($"Ovewriting existing File {bundlePath}"); } BinaryUtils.CopyFile(hostSource, bundlePath); long headerOffset = 0; using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath))) { Stream bundle = writer.BaseStream; bundle.Position = bundle.Length; foreach (var fileSpec in fileSpecs) { string relativePath = fileSpec.BundleRelativePath; if (IsHost(relativePath)) { continue; } if (ShouldIgnore(relativePath)) { Tracer.Log($"Ignore: {relativePath}"); continue; } FileType type = InferType(fileSpec); if (ShouldExclude(type, relativePath)) { Tracer.Log($"Exclude [{type}]: {relativePath}"); fileSpec.Excluded = true; continue; } using (FileStream file = File.OpenRead(fileSpec.SourcePath)) { FileType targetType = Target.TargetSpecificFileType(type); long startOffset = AddToBundle(bundle, file, targetType); FileEntry entry = BundleManifest.AddEntry(targetType, relativePath, startOffset, file.Length); Tracer.Log($"Embed: {entry}"); } } // Write the bundle manifest headerOffset = BundleManifest.Write(writer); Tracer.Log($"Header Offset={headerOffset}"); Tracer.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}"); Tracer.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}"); } HostWriter.SetAsBundle(bundlePath, headerOffset); return(bundlePath); }
/// <summary> /// Generate a bundle, given the specification of embedded files /// </summary> /// <param name="fileSpecs"> /// An enumeration FileSpecs for the files to be embedded. /// </param> /// <returns> /// The full path the the generated bundle file /// </returns> /// <exceptions> /// ArgumentException if input is invalid /// IOExceptions and ArgumentExceptions from callees flow to the caller. /// </exceptions> public string GenerateBundle(IReadOnlyList <FileSpec> fileSpecs) { trace.Log($"Bundler version {Version}"); 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"); } if (fileSpecs.GroupBy(file => file.BundleRelativePath).Where(g => g.Count() > 1).Any()) { throw new ArgumentException("Invalid input specification: Found multiple entries with the same BundleRelativePath"); } string bundlePath = Path.Combine(OutputDir, HostName); if (File.Exists(bundlePath)) { trace.Log($"Ovewriting existing File {bundlePath}"); } BinaryUtils.CopyFile(hostSource, bundlePath); long headerOffset = 0; using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath))) { Stream bundle = writer.BaseStream; bundle.Position = bundle.Length; // Write the files from the specification into the bundle foreach (var fileSpec in fileSpecs) { if (!ShouldEmbed(fileSpec.BundleRelativePath)) { trace.Log($"Skip: {fileSpec.BundleRelativePath}"); continue; } using (FileStream file = File.OpenRead(fileSpec.SourcePath)) { FileType type = InferType(fileSpec.BundleRelativePath, file); long startOffset = AddToBundle(bundle, file, type); FileEntry entry = BundleManifest.AddEntry(type, fileSpec.BundleRelativePath, startOffset, file.Length); trace.Log($"Embed: {entry}"); } } // Write the bundle manifest headerOffset = BundleManifest.Write(writer); trace.Log($"Header Offset={headerOffset}"); trace.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}"); trace.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}"); } HostWriter.SetAsBundle(bundlePath, headerOffset); return(bundlePath); }