using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using ICSharpCode.SharpZipLib.Zip; using ICSharpCode.SharpZipLib.Zip.Compression; using LibHacBuild.CodeGen.Stage1; using Nuke.Common; using Nuke.Common.CI.AppVeyor; using Nuke.Common.Git; using Nuke.Common.IO; using Nuke.Common.ProjectModel; using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.GitVersion; using Nuke.Common.Tools.SignTool; using static Nuke.Common.Tools.DotNet.DotNetTasks; namespace LibHacBuild; partial class Build : NukeBuild { public static int Main() => Execute(x => x.Standard); [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] public readonly string Configuration = IsLocalBuild ? "Debug" : "Release"; [Parameter("Don't enable any size-reducing settings on native builds.")] public readonly bool Untrimmed; [Parameter("Disable reflection in native builds.")] public readonly bool NoReflection; [Solution("LibHac.sln")] readonly Solution _solution; AbsolutePath SourceDirectory => RootDirectory / "src"; AbsolutePath TestsDirectory => RootDirectory / "tests"; AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; AbsolutePath SignedArtifactsDirectory => ArtifactsDirectory / "signed"; AbsolutePath TempDirectory => RootDirectory / ".nuke" / "temp"; AbsolutePath CliCoreDir => TempDirectory / "hactoolnet_net7.0"; AbsolutePath CliNativeDir => TempDirectory / $"hactoolnet_{HostOsName}"; AbsolutePath CliNativeExe => CliNativeDir / $"hactoolnet{NativeProgramExtension}"; AbsolutePath CliCoreZip => ArtifactsDirectory / $"hactoolnet-{VersionString}-netcore.zip"; AbsolutePath CliNativeZip => ArtifactsDirectory / $"hactoolnet-{VersionString}-{HostOsName}.zip"; Project LibHacProject => _solution.GetProject("LibHac").NotNull(); Project LibHacTestProject => _solution.GetProject("LibHac.Tests").NotNull(); Project HactoolnetProject => _solution.GetProject("hactoolnet").NotNull(); Project CodeGenProject => _solution.GetProject("_buildCodeGen").NotNull(); private bool HasGitDir { get; set; } private string NativeRuntime { get; set; } private string HostOsName { get; set; } private string NativeProgramExtension { get; set; } private DateTimeOffset CommitTime { get; set; } = DateTimeOffset.Now; string VersionString { get; set; } Dictionary VersionProps { get; set; } = new Dictionary(); const string CertFileName = "cert.pfx"; public Build() { if (EnvironmentInfo.IsWin) { NativeRuntime = "win-x64"; NativeProgramExtension = ".exe"; HostOsName = "win"; } else if (EnvironmentInfo.IsLinux) { NativeRuntime = "linux-x64"; NativeProgramExtension = ""; HostOsName = "linux"; } else if (EnvironmentInfo.IsOsx) { NativeRuntime = "osx-x64"; NativeProgramExtension = ""; HostOsName = "macos"; } } Target SetVersion => _ => _ .Executes(() => { GitRepository gitRepository = null; GitVersion gitVersion = null; try { gitRepository = GitRepository.FromLocalDirectory(RootDirectory); gitVersion = GitVersionTasks.GitVersion(s => s .DisableProcessLogOutput()) .Result; } catch (Exception e) { if (!e.Message.Contains("not a git repository", StringComparison.OrdinalIgnoreCase)) { Serilog.Log.Error(e, e.Message); } } if (gitRepository == null || gitVersion == null) { Serilog.Log.Debug("Unable to read Git version."); VersionString = GetCsprojVersion(); Serilog.Log.Debug($"Using version from .csproj: {VersionString}"); return; } HasGitDir = true; if (DateTimeOffset.TryParseExact(gitVersion.CommitDate, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.AssumeUniversal, out DateTimeOffset commitTime)) { CommitTime = commitTime.LocalDateTime; } VersionString = $"{gitVersion.MajorMinorPatch}"; if (!string.IsNullOrWhiteSpace(gitVersion.PreReleaseTag)) { VersionString += $"-{gitVersion.PreReleaseTag}+{gitVersion.Sha.Substring(0, 8)}"; } string suffix = gitVersion.PreReleaseTag; if (!string.IsNullOrWhiteSpace(suffix)) { if (!gitRepository.IsOnMasterBranch()) { suffix = $"-{suffix}"; } suffix += $"+{gitVersion.Sha.Substring(0, 8)}"; } if (Host is AppVeyor appVeyor) { // Workaround GitVersion issue by getting PR info manually https://github.com/GitTools/GitVersion/issues/1927 int? prNumber = appVeyor.PullRequestNumber; string branchName = Environment.GetEnvironmentVariable("APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH"); if (prNumber != null && branchName != null) { string prString = $"PullRequest{prNumber:D4}"; VersionString = VersionString.Replace(gitVersion.EscapedBranchName, prString); suffix = suffix?.Replace(gitVersion.EscapedBranchName, prString) ?? ""; } appVeyor.UpdateBuildVersion(VersionString); } VersionProps = new Dictionary { ["VersionPrefix"] = gitVersion.AssemblySemVer, ["VersionSuffix"] = suffix }; Serilog.Log.Debug($"Building version {VersionString}"); }); Target Clean => _ => _ .Executes(() => { List toDelete = SourceDirectory.GlobDirectories("**/bin", "**/obj") .Concat(TestsDirectory.GlobDirectories("**/bin", "**/obj")).ToList(); foreach (AbsolutePath dir in toDelete) { dir.DeleteDirectory(); } ArtifactsDirectory.CreateOrCleanDirectory(); CliCoreDir.CreateOrCleanDirectory(); CliNativeDir.CreateOrCleanDirectory(); }); Target Restore => _ => _ .DependsOn(Clean) .Executes(() => { DotNetRestoreSettings settings = new DotNetRestoreSettings() .SetProjectFile(_solution); DotNetRestore(s => settings); }); Target Codegen => _ => _ .Executes(() => { ResultCodeGen.Run(); RunCodegenStage2(); }); Target Compile => _ => _ .DependsOn(Restore, SetVersion, Codegen) .Executes(() => { DotNetBuildSettings buildSettings = new DotNetBuildSettings() .SetProjectFile(_solution) .EnableNoRestore() .SetConfiguration(Configuration) .SetProperties(VersionProps) .SetProperty("BuildType", "Release") .SetProperty("HasGitDir", HasGitDir); DotNetBuild(s => buildSettings); DotNetPublishSettings publishSettings = new DotNetPublishSettings() .EnableNoRestore() .SetConfiguration(Configuration); DotNetPublish(s => publishSettings .SetProject(HactoolnetProject) .SetFramework("net7.0") .SetOutput(CliCoreDir) .SetNoBuild(true) .SetProperties(VersionProps)); // Hack around OS newline differences if (EnvironmentInfo.IsUnix) { foreach (string filename in Directory.EnumerateFiles(CliCoreDir, "*.json")) { ReplaceLineEndings(filename); } } }); Target Pack => _ => _ .DependsOn(Compile) .Executes(() => { DotNetPackSettings settings = new DotNetPackSettings() .SetProject(LibHacProject) .EnableNoBuild() .SetConfiguration(Configuration) .EnableIncludeSymbols() .SetSymbolPackageFormat(DotNetSymbolPackageFormat.snupkg) .SetOutputDirectory(ArtifactsDirectory) .SetProperties(VersionProps); DotNetPack(s => settings); foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory, "*.*nupkg")) { RepackNugetPackage(filename); } foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory, "*.*nupkg")) { PushArtifact(filename); } }); Target Test => _ => _ .DependsOn(Compile) .Executes(() => { DotNetTestSettings settings = new DotNetTestSettings() .SetProjectFile(LibHacTestProject) .EnableNoBuild() .SetConfiguration(Configuration); if (EnvironmentInfo.IsUnix) settings = settings.SetProperty("TargetFramework", "net7.0"); DotNetTest(s => settings); }); Target Zip => _ => _ .DependsOn(Pack) .After(Native) .Executes(() => { string[] namesCore = Directory.EnumerateFiles(CliCoreDir, "*.json") .Concat(Directory.EnumerateFiles(CliCoreDir, "*.dll")) .ToArray(); ArtifactsDirectory.CreateDirectory(); ZipFiles(CliCoreZip, namesCore, CommitTime); Serilog.Log.Debug($"Created {CliCoreZip}"); PushArtifact(CliCoreZip); }); Target Publish => _ => _ .DependsOn(Test, Pack) .OnlyWhenStatic(() => AppVeyor.Instance != null && AppVeyor.Instance.PullRequestTitle == null) .Executes(() => { AbsolutePath nupkgFile = ArtifactsDirectory.GlobFiles("*.nupkg").Single(); string apiKey = EnvironmentInfo.GetVariable("myget_api_key"); DotNetNuGetPushSettings settings = new DotNetNuGetPushSettings() .SetApiKey(apiKey) .SetSource("https://www.myget.org/F/libhac/api/v3/index.json"); DotNetNuGetPush(settings.SetTargetPath(nupkgFile)); }); Target Sign => _ => _ .DependsOn(Test, Zip) .OnlyWhenStatic(() => File.Exists(CertFileName)) .OnlyWhenStatic(() => EnvironmentInfo.IsWin) .WhenSkipped(DependencyBehavior.Execute) .Executes(() => { string pwd = ReadPassword(); if (pwd == string.Empty) { Serilog.Log.Debug("Skipping sign task"); return; } SignAndReZip(pwd); }); Target Native => _ => _ .DependsOn(SetVersion) .After(Compile) .Executes(BuildNative); // ReSharper disable once UnusedMember.Local Target AppVeyorBuild => _ => _ .DependsOn(Zip, Native, Test, Publish) .Unlisted() .Executes(PrintResults); Target Standard => _ => _ .DependsOn(Test, Zip) .Executes(PrintResults); // ReSharper disable once UnusedMember.Local Target Full => _ => _ .DependsOn(Sign, Native) .Executes(PrintResults); public void PrintResults() { Serilog.Log.Debug("SHA-1:"); using (var sha = SHA1.Create()) { foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory)) { using (var stream = new FileStream(filename, FileMode.Open)) { string hash = BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", ""); Serilog.Log.Debug($"{hash} - {Path.GetFileName(filename)}"); } } } } public void BuildNative() { string buildType = Untrimmed ? "native-untrimmed" : "native"; if (NoReflection) { buildType = "native-noreflection"; } DotNetPublishSettings publishSettings = new DotNetPublishSettings() .SetConfiguration(Configuration) .SetProject(HactoolnetProject) .SetRuntime(NativeRuntime) .SetSelfContained(true) .SetOutput(CliNativeDir) .SetProperties(VersionProps) .AddProperty("BuildType", buildType); DotNetPublish(publishSettings); if (EnvironmentInfo.IsUnix && !Untrimmed) { File.Copy(CliNativeExe, CliNativeExe + "_unstripped", true); ProcessTasks.StartProcess("strip", CliNativeExe).AssertZeroExitCode(); } ArtifactsDirectory.CreateOrCleanDirectory(); ZipFile(CliNativeZip, CliNativeExe, $"hactoolnet{NativeProgramExtension}", CommitTime); Serilog.Log.Debug($"Created {CliNativeZip}"); Serilog.Log.Debug($"Created {CliNativeZip}"); PushArtifact(CliNativeZip); } public static void ZipFiles(string outFile, IEnumerable files, DateTimeOffset fileDateTime) { using (var s = new ZipOutputStream(File.Create(outFile))) { s.SetLevel(9); foreach (string file in files) { var entry = new ZipEntry(Path.GetFileName(file)); entry.DateTime = fileDateTime.DateTime; using (FileStream fs = File.OpenRead(file)) { entry.Size = fs.Length; s.PutNextEntry(entry); fs.CopyTo(s); } } } } public static void ZipFile(string outFile, string file, string nameInsideZip, DateTimeOffset fileDateTime) { using (var s = new ZipOutputStream(File.Create(outFile))) { s.SetLevel(9); var entry = new ZipEntry(nameInsideZip); entry.DateTime = fileDateTime.DateTime; using (FileStream fs = File.OpenRead(file)) { entry.Size = fs.Length; s.PutNextEntry(entry); fs.CopyTo(s); } } } public static void ZipDirectory(string outFile, string directory, DateTimeOffset fileDateTime) { using (var s = new ZipOutputStream(File.Create(outFile))) { s.SetLevel(9); foreach (string filePath in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)) { string relativePath = Path.GetRelativePath(directory, filePath); var entry = new ZipEntry(relativePath); entry.DateTime = fileDateTime.DateTime; using (FileStream fs = File.OpenRead(filePath)) { entry.Size = fs.Length; s.PutNextEntry(entry); fs.CopyTo(s); } } } } public static void ZipDirectory(string outFile, string directory, IEnumerable files, DateTimeOffset fileDateTime) { using (var s = new ZipOutputStream(File.Create(outFile))) { s.SetLevel(9); foreach (string filePath in files) { string absolutePath = Path.Combine(directory, filePath); var entry = new ZipEntry(filePath); entry.DateTime = fileDateTime.DateTime; using (FileStream fs = File.OpenRead(absolutePath)) { entry.Size = fs.Length; s.PutNextEntry(entry); fs.CopyTo(s); } } } } public static void UnzipFiles(string zipFile, string outDir) { using (var s = new ZipInputStream(File.OpenRead(zipFile))) { ZipEntry entry; while ((entry = s.GetNextEntry()) != null) { string outPath = Path.Combine(outDir, entry.Name); string directoryName = Path.GetDirectoryName(outPath); string fileName = Path.GetFileName(outPath); if (!string.IsNullOrWhiteSpace(directoryName)) { Directory.CreateDirectory(directoryName); } if (!string.IsNullOrWhiteSpace(fileName)) { using (FileStream outFile = File.Create(outPath)) { s.CopyTo(outFile); } } } } } public static byte[] DeflateBytes(byte[] data) { var s = new Deflater(9, true); s.SetInput(data); s.Finish(); byte[] buffer = new byte[data.Length]; s.Deflate(buffer); Debug.Assert(s.IsFinished); byte[] compressed = new byte[s.TotalOut]; Array.Copy(buffer, compressed, compressed.Length); return compressed; } public static void PushArtifact(string path, string name = null) { if (Host is not AppVeyor appVeyor) return; if (!File.Exists(path)) { Serilog.Log.Warning($"Unable to add artifact {path}"); } appVeyor.PushArtifact(path, name); Serilog.Log.Debug($"Added AppVeyor artifact {path}"); } public static void ReplaceLineEndings(string filename) { string text = File.ReadAllText(filename); File.WriteAllText(filename, Regex.Replace(text, @"\r\n|\n\r|\n|\r", "\r\n")); } public static void SignAssemblies(string password, params string[] fileNames) { SignToolSettings settings = new SignToolSettings() .SetFileDigestAlgorithm("SHA256") .SetFile(CertFileName) .SetFiles(fileNames) .SetPassword(password) .SetTimestampServerDigestAlgorithm("SHA256") .SetRfc3161TimestampServerUrl("http://timestamp.digicert.com"); SignToolTasks.SignTool(settings); } public void SignAndReZip(string password) { AbsolutePath nupkgFile = ArtifactsDirectory.GlobFiles("*.nupkg").Single(); AbsolutePath snupkgFile = ArtifactsDirectory.GlobFiles("*.snupkg").Single(); AbsolutePath nupkgDir = TempDirectory / ("sign_" + Path.GetFileName(nupkgFile)); AbsolutePath coreFxDir = TempDirectory / ("sign_" + Path.GetFileName(CliCoreZip)); AbsolutePath nativeZipDir = TempDirectory / ("sign_" + Path.GetFileName(CliNativeZip)); bool signNative = CliNativeExe.FileExists(); try { UnzipFiles(CliCoreZip, coreFxDir); List pkgFileList = UnzipPackage(nupkgFile, nupkgDir); var toSign = new List(); toSign.AddRange(nupkgDir.GlobFiles("**/LibHac.dll")); toSign.Add(coreFxDir / "hactoolnet.dll"); if (signNative) { UnzipFiles(CliNativeZip, nativeZipDir); toSign.Add(nativeZipDir / "hactoolnet.exe"); } Directory.CreateDirectory(SignedArtifactsDirectory); SignAssemblies(password, toSign.Select(x => x.ToString()).ToArray()); // Avoid having multiple signed versions of the same file File.Copy(nupkgDir / "lib" / "net7.0" / "LibHac.dll", coreFxDir / "LibHac.dll", true); ZipDirectory(SignedArtifactsDirectory / Path.GetFileName(nupkgFile), nupkgDir, pkgFileList, CommitTime); ZipDirectory(SignedArtifactsDirectory / Path.GetFileName(CliCoreZip), coreFxDir, CommitTime); if (signNative) { ZipDirectory(SignedArtifactsDirectory / Path.GetFileName(CliNativeZip), nativeZipDir, CommitTime); } File.Copy(snupkgFile, SignedArtifactsDirectory / Path.GetFileName(snupkgFile)); SignNupkg(SignedArtifactsDirectory / Path.GetFileName(nupkgFile), password); SignNupkg(SignedArtifactsDirectory / Path.GetFileName(snupkgFile), password); } catch (Exception) { Directory.Delete(SignedArtifactsDirectory, true); throw; } finally { Directory.Delete(nupkgDir, true); Directory.Delete(coreFxDir, true); } } public static string ReadPassword() { var pwd = new StringBuilder(); ConsoleKeyInfo key; Console.Write("Enter certificate password (Empty password to skip): "); do { key = Console.ReadKey(true); // Ignore any key out of range. if (((int)key.Key) >= '!' && ((int)key.Key <= '~')) { // Append the character to the password. pwd.Append(key.KeyChar); Console.Write("*"); } // Exit if Enter key is pressed. } while (key.Key != ConsoleKey.Enter); Console.WriteLine(); return pwd.ToString(); } public string GetCsprojVersion() { return XmlTasks.XmlPeekSingle(LibHacProject.Path, "/Project/PropertyGroup/VersionPrefix", null); } public void RunCodegenStage2() { Serilog.Log.Debug("\nBuilding stage 2 codegen project."); DotNetRunSettings settings = new DotNetRunSettings() .SetProjectFile(CodeGenProject.Path); // .SetLogOutput(false); try { DotNetRun(settings); Serilog.Log.Debug(""); } catch (ProcessException) { Serilog.Log.Error("\nError running stage 2 codegen. Skipping...\n"); } } }