using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Xml.Linq; using AsmResolver.PE; using AsmResolver.PE.File; using AsmResolver.PE.File.Headers; using AsmResolver.PE.Win32Resources.Builder; namespace DeployAll; public static class DotnetCmd { private const string DotnetAppName = "dotnet"; private const string DesiredRuntimeVersion = "8.0.0"; public static void Publish(string projPath, string configuration, string runtime, string resultPath) { ProcessStartInfo psi = new ProcessStartInfo { FileName = DotnetAppName, ArgumentList = { "publish", projPath, "-c", configuration, "-clp:ErrorsOnly;Summary", "--self-contained", "-r", runtime, "/p:Platform=x64", "/p:ErrorOnDuplicatePublishOutputFiles=false", //TODO: fix our duplicate files "/p:RollForward=Disable", $"/p:RuntimeFrameworkVersion={DesiredRuntimeVersion}", "-o", resultPath }, RedirectStandardOutput = true, RedirectStandardError = true }; var process = Util.StartProcess(psi); process.WaitForExit(); string stdout = process.StandardOutput.ReadToEnd(); string stderr = process.StandardError.ReadToEnd(); string errorLine = $"{stdout}\n{stderr}".Split('\n') .First(ln => ln.Contains("Error(s)", StringComparison.OrdinalIgnoreCase)) .Trim(); if (!errorLine.StartsWith("0 ", StringComparison.OrdinalIgnoreCase)) { throw new Exception($"Failed to build {projPath}, {errorLine}"); } Console.WriteLine($" - Published \"{projPath}\" to \"{resultPath}\", {errorLine}"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !runtime.StartsWith("win")) { return; } // You may be wondering, what is this crap? // Cross-compiling is something that should work perfectly, because it's super convenient. // However, thanks to the way .NET works, cross-compiling to Windows from *nix platforms // results in an executable with basically no metadata, and the wrong subsystem! // (see https://github.com/dotnet/sdk/blob/375955d3a9de213a01d70eb6180298000dee30ee/src/Tasks/Microsoft.NET.Build.Tasks/GenerateShims.cs#L127-L132) // Does it look like we're about to modify the SDK itself to solve this problem? Yeah right. // Instead let's just take the shim generated by the SDK and fix it ourselves. XElement firstPropertyGroup = XDocument.Load(projPath) .Root? .Element("PropertyGroup") ?? throw new Exception("PropertyGroup not found"); string assemblyName = firstPropertyGroup.Element("AssemblyName")?.Value ?? throw new Exception("AssemblyName not found"); // This is the shim that doesn't have the stuff we want. var fileToChange = PEFile.FromFile(Path.Combine(resultPath, $"{assemblyName}.exe")); // Luckily, the SDK does embed all of that data in the assembly with all of the IL! // We can just yoink it from here. var managedAssembly = PEImage.FromFile(Path.Combine(resultPath, $"{assemblyName}.dll")); // Here's a whole lot of magic to set up the resources section of the executable var resourceSection = new PESection(".rsrc", SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); var resourceDirectoryBuffer = new ResourceDirectoryBuffer(); resourceDirectoryBuffer.AddDirectory(managedAssembly.Resources ?? throw new Exception($"{assemblyName}.dll has no resources")); resourceSection.Contents = resourceDirectoryBuffer; fileToChange.Sections.Add(resourceSection); fileToChange.AlignSections(); var dataDirectories = fileToChange.OptionalHeader.DataDirectories; dataDirectories[2] = new DataDirectory(resourceDirectoryBuffer.Rva, resourceDirectoryBuffer.GetPhysicalSize()); // And here's something a little less magical that fixes the subsystem fileToChange.OptionalHeader.SubSystem = firstPropertyGroup.Element("OutputType")?.Value == "WinExe" ? SubSystem.WindowsGui : SubSystem.WindowsCui; using var writeStream = File.Open(Path.Combine(resultPath, $"{assemblyName}.exe"), FileMode.Create); fileToChange.Write(writeStream); } public static Version GetSdkVersion() { ProcessStartInfo psi = new ProcessStartInfo { FileName = DotnetAppName, ArgumentList = { "--version" }, RedirectStandardOutput = true, RedirectStandardError = true }; var process = Util.StartProcess(psi); process.WaitForExit(); string stdout = process.StandardOutput.ReadToEnd(); return Version.Parse(stdout.Trim()); } }