// Remove mac code signature if applied before bundling static void RemoveCodesignIfNecessary(string bundlePath) { Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)); Debug.Assert(HostModelUtils.IsCodesignAvailable()); // `codesign -v` returns 0 if app is signed if (HostModelUtils.RunCodesign("-v", bundlePath).ExitCode == 0) { var(exitCode, stdErr) = HostModelUtils.RunCodesign("--remove-signature", bundlePath); if (exitCode != 0) { throw new InvalidOperationException($"Removing codesign from '{bundlePath}' failed: {stdErr}"); } } }
/// <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 published as output. /// </param> /// <returns> /// The full path 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); if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && HostModelUtils.IsCodesignAvailable()) { RemoveCodesignIfNecessary(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); // Sign the bundle if requested if (_macosCodesign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && HostModelUtils.IsCodesignAvailable()) { var(exitCode, stdErr) = HostModelUtils.RunCodesign("-s -", bundlePath); if (exitCode != 0) { throw new InvalidOperationException($"Failed to codesign '{bundlePath}': {stdErr}"); } } return(bundlePath);
/// <summary> /// Create an AppHost with embedded configuration of app binary location /// </summary> /// <param name="appHostSourceFilePath">The path of Apphost template, which has the place holder</param> /// <param name="appHostDestinationFilePath">The destination path for desired location to place, including the file name</param> /// <param name="appBinaryFilePath">Full path to app binary or relative path to the result apphost file</param> /// <param name="windowsGraphicalUserInterface">Specify whether to set the subsystem to GUI. Only valid for PE apphosts.</param> /// <param name="assemblyToCopyResourcesFrom">Path to the intermediate assembly, used for copying resources to PE apphosts.</param> /// <param name="enableMacOSCodeSign">Sign the app binary using codesign with an anonymous certificate.</param> public static void CreateAppHost( string appHostSourceFilePath, string appHostDestinationFilePath, string appBinaryFilePath, bool windowsGraphicalUserInterface = false, string assemblyToCopyResourcesFrom = null, bool enableMacOSCodeSign = false) { var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath); if (bytesToWrite.Length > 1024) { throw new AppNameTooLongException(appBinaryFilePath); } bool appHostIsPEImage = false; void RewriteAppHost(MemoryMappedViewAccessor accessor) { // Re-write the destination apphost with the proper contents. BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite); appHostIsPEImage = PEUtils.IsPEImage(accessor); if (windowsGraphicalUserInterface) { if (!appHostIsPEImage) { throw new AppHostNotPEFileException(); } PEUtils.SetWindowsGraphicalUserInterfaceBit(accessor); } } void UpdateResources() { if (assemblyToCopyResourcesFrom != null && appHostIsPEImage) { if (ResourceUpdater.IsSupportedOS()) { // Copy resources from managed dll to the apphost new ResourceUpdater(appHostDestinationFilePath) .AddResourcesFromPEImage(assemblyToCopyResourcesFrom) .Update(); } else { throw new AppHostCustomizationUnsupportedOSException(); } } } try { RetryUtil.RetryOnIOError(() => { FileStream appHostSourceStream = null; MemoryMappedFile memoryMappedFile = null; MemoryMappedViewAccessor memoryMappedViewAccessor = null; try { // Open the source host file. appHostSourceStream = new FileStream(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1); memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true); memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite); // Get the size of the source app host to ensure that we don't write extra data to the destination. // On Windows, the size of the view accessor is rounded up to the next page boundary. long sourceAppHostLength = appHostSourceStream.Length; // Transform the host file in-memory. RewriteAppHost(memoryMappedViewAccessor); // Save the transformed host. using (FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create)) { BinaryUtils.WriteToStream(memoryMappedViewAccessor, fileStream, sourceAppHostLength); // Remove the signature from MachO hosts. if (!appHostIsPEImage) { MachOUtils.RemoveSignature(fileStream); } } } finally { memoryMappedViewAccessor?.Dispose(); memoryMappedFile?.Dispose(); appHostSourceStream?.Dispose(); } }); RetryUtil.RetryOnWin32Error(UpdateResources); if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x const int EINTR = 4; int chmodReturnCode = 0; do { chmodReturnCode = chmod(appHostDestinationFilePath, filePermissionOctal); }while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR); if (chmodReturnCode == -1) { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {appHostDestinationFilePath}."); } if (enableMacOSCodeSign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && HostModelUtils.IsCodesignAvailable()) { (int exitCode, string stdErr) = HostModelUtils.RunCodesign("-s -", appHostDestinationFilePath); if (exitCode != 0) { throw new AppHostSigningException(exitCode, stdErr); } } } } catch (Exception ex) { // Delete the destination file so we don't leave an unmodified apphost try { File.Delete(appHostDestinationFilePath); } catch (Exception failedToDeleteEx) { throw new AggregateException(ex, failedToDeleteEx); } throw; } }