From b68261a092bffd11d037e3c1a99d9a97e16f1c43 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sat, 16 Feb 2019 19:03:56 -0600 Subject: [PATCH] Build script updates (#33) - Make generated NuGet packages deterministic. - Add a signing option to the build script. - Set versions for pre-release builds. - Publish packages to a MyGet feed. - Make Windows-produced artifacts match Linux-produced ones --- DotnetCliVersion.txt | 2 +- GitVersion.yml | 5 + appveyor.yml | 3 + build.ps1 | 18 +- build.sh | 40 ++- build/Build.cs | 691 +++++++++++++++++++++++++++------------ build/RepackNuget.cs | 136 ++++++++ build/_build.csproj | 8 +- src/LibHac/LibHac.csproj | 3 +- 9 files changed, 671 insertions(+), 235 deletions(-) create mode 100644 GitVersion.yml create mode 100644 build/RepackNuget.cs diff --git a/DotnetCliVersion.txt b/DotnetCliVersion.txt index 33ba87ed..c4d6f4ec 100644 --- a/DotnetCliVersion.txt +++ b/DotnetCliVersion.txt @@ -1 +1 @@ -2.1.500 \ No newline at end of file +2.2.103 \ No newline at end of file diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 00000000..2ad71d6b --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,5 @@ +mode: ContinuousDeployment +increment: Patch +branches: + master: + tag: alpha \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 7d189dee..6a211e2b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,8 @@ version: 0.1.3-{build} image: Visual Studio 2017 +environment: + myget_api_key: + secure: 0xJoYAtR6psXCRvk1qm5czDObkeRjHKPjfe5gIExXVFPwA0VVODYv/hBZYUtz2F3 build_script: - ps: .\build.ps1 test: off \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 304207eb..5cc0b61e 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,6 +1,7 @@ [CmdletBinding()] Param( #[switch]$CustomParam, + [switch]$BuildDotnetCoreOnly, [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)] [string[]]$BuildArguments ) @@ -43,14 +44,17 @@ try { if (Test-Path $DotNetGlobalFile) { $DotNetVersion = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json).sdk.version } + + $DotNetDirectory = "$TempDirectory\dotnet-win" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + # If dotnet is installed locally, and expected version is not set or installation matches the expected version if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` (!(Test-Path variable:DotNetVersion) -or $(& cmd.exe /c 'dotnet --version 2>&1') -eq $DotNetVersion)) { $env:DOTNET_EXE = (Get-Command "dotnet").Path } - else { - $DotNetDirectory = "$TempDirectory\dotnet-win" - $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + elseif ($null -eq (Get-Command $env:DOTNET_EXE -ErrorAction SilentlyContinue) -and ` + (!(Test-Path variable:DotNetVersion) -or $(& cmd.exe /c '$env:DOTNET_EXE --version 2>&1') -ne $DotNetVersion)) { # Download install script $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" @@ -68,8 +72,16 @@ try { Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" + if($BuildDotnetCoreOnly) { + $BuildArguments += "-DoCoreBuildOnly" + $BuildArguments += "true" + } + ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments } } +catch { + Write-Output $_.Exception.Message +} finally { if (Test-Path $DotNetGlobalFile) { Remove-Item $DotNetGlobalFile diff --git a/build.sh b/build.sh index 63edfcd9..4358a1f9 100644 --- a/build.sh +++ b/build.sh @@ -3,7 +3,7 @@ echo $(bash --version 2>&1 | head -n 1) #CUSTOMPARAM=0 -BUILD_ARGUMENTS=() +BUILD_ARGUMENTS=("-DoCoreBuildOnly") for i in "$@"; do case $(echo $1 | awk '{print tolower($0)}') in # -custom-param) CUSTOMPARAM=1;; @@ -37,37 +37,41 @@ export NUGET_XMLDOC_MODE="skip" function FirstJsonValue { perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2} } -trap "rm -f $DOTNET_GLOBAL_FILE" INT TERM EXIT +trap "rm -f \"$DOTNET_GLOBAL_FILE\"" INT TERM EXIT dotnetCliVersion=$(cat DotnetCliVersion.txt) json="{\"sdk\":{\"version\":\"$dotnetCliVersion\"}}" - echo "$json" > $DOTNET_GLOBAL_FILE + echo "$json" > "$DOTNET_GLOBAL_FILE" # If global.json exists, load expected version if [ -f "$DOTNET_GLOBAL_FILE" ]; then DOTNET_VERSION=$dotnetCliVersion fi +DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" +export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + # If dotnet is installed locally, and expected version is not set or installation matches the expected version if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") ]]; then export DOTNET_EXE="$(command -v dotnet)" -else - DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" - export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" +elif [[ ! (-x "$DOTNET_EXE" && (-z ${DOTNET_VERSION+x} || $($DOTNET_EXE --version) == "$DOTNET_VERSION")) ]]; then + + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + + if [ ! -x "$DOTNET_INSTALL_FILE" ]; then + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + fi - # Download install script - DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" - mkdir -p "$TEMP_DIRECTORY" - curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" - chmod +x "$DOTNET_INSTALL_FILE" - - # Install by channel or version - if [ -z ${DOTNET_VERSION+x} ]; then - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path - else - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path - fi + # Install by channel or version + if [ -z ${DOTNET_VERSION+x} ]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi fi echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" diff --git a/build/Build.cs b/build/Build.cs index 1cacbe25..01e00319 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -4,265 +4,538 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; using ICSharpCode.SharpZipLib.Zip; using ILRepacking; using Nuke.Common; +using Nuke.Common.BuildServers; using Nuke.Common.Git; using Nuke.Common.ProjectModel; using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.GitVersion; +using Nuke.Common.Tools.SignTool; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.Tools.DotNet.DotNetTasks; -class Build : NukeBuild +namespace LibHacBuild { - public static int Main() => Execute(x => x.Results); + partial class Build : NukeBuild + { + public static int Main() => Execute(x => x.Results); - [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] - readonly string Configuration = IsLocalBuild ? "Debug" : "Release"; + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly string Configuration = IsLocalBuild ? "Debug" : "Release"; - [Solution("LibHac.sln")] readonly Solution Solution; - [GitRepository] readonly GitRepository GitRepository; + [Parameter("Build only .NET Core targets if true. Default is false on Windows")] + readonly bool DoCoreBuildOnly; - AbsolutePath SourceDirectory => RootDirectory / "src"; - AbsolutePath TestsDirectory => RootDirectory / "tests"; - AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; - AbsolutePath TempDirectory => RootDirectory / ".tmp"; - AbsolutePath CliCoreDir => TempDirectory / "hactoolnet_netcoreapp2.1"; - AbsolutePath CliFrameworkDir => TempDirectory / "hactoolnet_net46"; - AbsolutePath CliFrameworkZip => ArtifactsDirectory / "hactoolnet.zip"; - AbsolutePath CliCoreZip => ArtifactsDirectory / "hactoolnet_netcore.zip"; + [Solution("LibHac.sln")] readonly Solution Solution; + [GitRepository] readonly GitRepository GitRepository; + [GitVersion] readonly GitVersion GitVersion; - AbsolutePath CliMergedExe => ArtifactsDirectory / "hactoolnet.exe"; + AbsolutePath SourceDirectory => RootDirectory / "src"; + AbsolutePath TestsDirectory => RootDirectory / "tests"; + AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; + AbsolutePath SignedArtifactsDirectory => ArtifactsDirectory / "signed"; + AbsolutePath TempDirectory => RootDirectory / ".tmp"; + AbsolutePath CliCoreDir => TempDirectory / "hactoolnet_netcoreapp2.1"; + AbsolutePath CliFrameworkDir => TempDirectory / "hactoolnet_net46"; + AbsolutePath CliFrameworkZip => ArtifactsDirectory / "hactoolnet.zip"; + AbsolutePath CliCoreZip => ArtifactsDirectory / "hactoolnet_netcore.zip"; - Project LibHacProject => Solution.GetProject("LibHac").NotNull(); - Project LibHacTestProject => Solution.GetProject("LibHac.Tests").NotNull(); - Project HactoolnetProject => Solution.GetProject("hactoolnet").NotNull(); + AbsolutePath CliMergedExe => ArtifactsDirectory / "hactoolnet.exe"; - Target Clean => _ => _ - .Executes(() => - { - DeleteDirectories(GlobDirectories(SourceDirectory, "**/bin", "**/obj")); - DeleteDirectories(GlobDirectories(TestsDirectory, "**/bin", "**/obj")); - EnsureCleanDirectory(ArtifactsDirectory); - EnsureCleanDirectory(CliCoreDir); - EnsureCleanDirectory(CliFrameworkDir); - }); + Project LibHacProject => Solution.GetProject("LibHac").NotNull(); + Project LibHacTestProject => Solution.GetProject("LibHac.Tests").NotNull(); + Project HactoolnetProject => Solution.GetProject("hactoolnet").NotNull(); - Target Restore => _ => _ - .DependsOn(Clean) - .Executes(() => - { - DotNetRestoreSettings settings = new DotNetRestoreSettings() - .SetProjectFile(Solution); + string AppVeyorVersion { get; set; } + Dictionary VersionProps { get; set; } = new Dictionary(); - if (EnvironmentInfo.IsUnix) settings = settings.RemoveRuntimes("net46"); + const string CertFileName = "cert.pfx"; - DotNetRestore(s => settings); - }); - - Target Compile => _ => _ - .DependsOn(Restore) - .Executes(() => - { - DotNetBuildSettings buildSettings = new DotNetBuildSettings() - .SetProjectFile(Solution) - .EnableNoRestore() - .SetConfiguration(Configuration); - - if (EnvironmentInfo.IsUnix) buildSettings = buildSettings.SetFramework("netcoreapp2.1"); - - DotNetBuild(s => buildSettings); - - DotNetPublishSettings publishSettings = new DotNetPublishSettings() - .EnableNoRestore() - .SetConfiguration(Configuration); - - DotNetPublish(s => publishSettings - .SetProject(HactoolnetProject) - .SetFramework("netcoreapp2.1") - .SetOutput(CliCoreDir)); - - if (EnvironmentInfo.IsWin) + Target SetVersion => _ => _ + .OnlyWhenStatic(() => GitRepository != null) + .Executes(() => { + AppVeyorVersion = $"{GitVersion.AssemblySemVer}-{GitVersion.PreReleaseTag}+{GitVersion.Sha.Substring(0, 8)}"; + + VersionProps = new Dictionary + { + ["VersionPrefix"] = GitVersion.AssemblySemVer, + ["VersionSuffix"] = GitVersion.PreReleaseTag + }; + + Console.WriteLine($"Building version {AppVeyorVersion}"); + + if (Host == HostType.AppVeyor) + { + SetAppVeyorVersion(AppVeyorVersion); + } + }); + + Target Clean => _ => _ + .Executes(() => + { + DeleteDirectories(GlobDirectories(SourceDirectory, "**/bin", "**/obj")); + DeleteDirectories(GlobDirectories(TestsDirectory, "**/bin", "**/obj")); + EnsureCleanDirectory(ArtifactsDirectory); + EnsureCleanDirectory(CliCoreDir); + EnsureCleanDirectory(CliFrameworkDir); + }); + + Target Restore => _ => _ + .DependsOn(Clean) + .Executes(() => + { + DotNetRestoreSettings settings = new DotNetRestoreSettings() + .SetProjectFile(Solution); + + DotNetRestore(s => settings); + }); + + Target Compile => _ => _ + .DependsOn(Restore, SetVersion) + .Executes(() => + { + DotNetBuildSettings buildSettings = new DotNetBuildSettings() + .SetProjectFile(Solution) + .EnableNoRestore() + .SetConfiguration(Configuration) + .SetProperties(VersionProps); + + if (DoCoreBuildOnly) buildSettings = buildSettings.SetFramework("netcoreapp2.1"); + + DotNetBuild(s => buildSettings); + + DotNetPublishSettings publishSettings = new DotNetPublishSettings() + .EnableNoRestore() + .SetConfiguration(Configuration); + DotNetPublish(s => publishSettings .SetProject(HactoolnetProject) - .SetFramework("net46") - .SetOutput(CliFrameworkDir)); - } + .SetFramework("netcoreapp2.1") + .SetOutput(CliCoreDir) + .SetProperties(VersionProps)); - // Hack around OS newline differences - if (EnvironmentInfo.IsUnix) - { - foreach (string filename in Directory.EnumerateFiles(CliCoreDir, "*.json")) + if (!DoCoreBuildOnly) { - ReplaceLineEndings(filename); + DotNetPublish(s => publishSettings + .SetProject(HactoolnetProject) + .SetFramework("net46") + .SetOutput(CliFrameworkDir) + .SetProperties(VersionProps)); } - } - }); - Target Pack => _ => _ - .DependsOn(Compile) - .Executes(() => - { - DotNetPackSettings settings = new DotNetPackSettings() - .SetProject(LibHacProject) - .EnableNoBuild() - .SetConfiguration(Configuration) - .EnableIncludeSymbols() - .SetOutputDirectory(ArtifactsDirectory); - - if (EnvironmentInfo.IsUnix) - settings = settings.SetProperties( - new Dictionary { ["TargetFrameworks"] = "netcoreapp2.1" }); - - DotNetPack(s => settings); - - if (Host != HostType.AppVeyor) return; - - foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory, "*.nupkg")) - { - PushArtifact(filename); - } - }); - - Target Merge => _ => _ - .DependsOn(Compile) - .OnlyWhen(() => EnvironmentInfo.IsWin) - .Executes(() => - { - string[] libraries = Directory.GetFiles(CliFrameworkDir, "*.dll"); - var cliList = new List { CliFrameworkDir / "hactoolnet.exe" }; - cliList.AddRange(libraries); - - var cliOptions = new RepackOptions - { - OutputFile = CliMergedExe, - InputAssemblies = cliList.ToArray(), - SearchDirectories = new[] { "." } - }; - - new ILRepack(cliOptions).Repack(); - - if (Host == HostType.AppVeyor) - { - PushArtifact(CliMergedExe); - } - }); - - Target Test => _ => _ - .DependsOn(Compile) - .Executes(() => - { - DotNetTestSettings settings = new DotNetTestSettings() - .SetProjectFile(LibHacTestProject) - .EnableNoBuild() - .SetConfiguration(Configuration); - - if (EnvironmentInfo.IsUnix) settings = settings.SetFramework("netcoreapp2.1"); - - DotNetTest(s => settings); - }); - - Target Zip => _ => _ - .DependsOn(Pack) - .Executes(() => - { - string[] namesFx = Directory.EnumerateFiles(CliFrameworkDir, "*.exe") - .Concat(Directory.EnumerateFiles(CliFrameworkDir, "*.dll")) - .ToArray(); - - string[] namesCore = Directory.EnumerateFiles(CliCoreDir, "*.json") - .Concat(Directory.EnumerateFiles(CliCoreDir, "*.dll")) - .ToArray(); - - if (EnvironmentInfo.IsWin) - { - ZipFiles(CliFrameworkZip, namesFx); - Console.WriteLine($"Created {CliFrameworkZip}"); - } - - ZipFiles(CliCoreZip, namesCore); - Console.WriteLine($"Created {CliCoreZip}"); - - if (Host == HostType.AppVeyor) - { - PushArtifact(CliFrameworkZip); - PushArtifact(CliCoreZip); - PushArtifact(CliMergedExe); - } - }); - - Target Results => _ => _ - .DependsOn(Test, Zip, Merge) - .Executes(() => - { - Console.WriteLine("SHA-1:"); - using (SHA1 sha = SHA1.Create()) - { - foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory)) + // Hack around OS newline differences + if (EnvironmentInfo.IsUnix) { - using (var stream = new FileStream(filename, FileMode.Open)) + foreach (string filename in Directory.EnumerateFiles(CliCoreDir, "*.json")) { - string hash = BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", ""); - Console.WriteLine($"{hash} - {Path.GetFileName(filename)}"); + ReplaceLineEndings(filename); + } + } + }); + + Target Pack => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetPackSettings settings = new DotNetPackSettings() + .SetProject(LibHacProject) + .EnableNoBuild() + .SetConfiguration(Configuration) + .EnableIncludeSymbols() + .SetSymbolPackageFormat(DotNetSymbolPackageFormat.snupkg) + .SetOutputDirectory(ArtifactsDirectory) + .SetProperties(VersionProps); + + if (DoCoreBuildOnly) + settings = settings.SetProperty("TargetFrameworks", "netcoreapp2.1"); + + DotNetPack(s => settings); + + foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory, "*.*nupkg")) + { + RepackNugetPackage(filename); + } + + if (Host != HostType.AppVeyor) return; + + foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory, "*.*nupkg")) + { + PushArtifact(filename); + } + }); + + Target Merge => _ => _ + .DependsOn(Compile) + .OnlyWhenStatic(() => !DoCoreBuildOnly) + .Executes(() => + { + string[] libraries = Directory.GetFiles(CliFrameworkDir, "*.dll"); + var cliList = new List { CliFrameworkDir / "hactoolnet.exe" }; + cliList.AddRange(libraries); + + var cliOptions = new RepackOptions + { + OutputFile = CliMergedExe, + InputAssemblies = cliList.ToArray(), + SearchDirectories = new[] { "." } + }; + + new ILRepack(cliOptions).Repack(); + + foreach (AbsolutePath file in ArtifactsDirectory.GlobFiles("*.exe.config")) + { + File.Delete(file); + } + + if (Host == HostType.AppVeyor) + { + PushArtifact(CliMergedExe); + } + }); + + Target Test => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetTestSettings settings = new DotNetTestSettings() + .SetProjectFile(LibHacTestProject) + .EnableNoBuild() + .SetConfiguration(Configuration); + + if (DoCoreBuildOnly) settings = settings.SetFramework("netcoreapp2.1"); + + DotNetTest(s => settings); + }); + + Target Zip => _ => _ + .DependsOn(Pack) + .Executes(() => + { + string[] namesFx = Directory.EnumerateFiles(CliFrameworkDir, "*.exe") + .Concat(Directory.EnumerateFiles(CliFrameworkDir, "*.dll")) + .ToArray(); + + string[] namesCore = Directory.EnumerateFiles(CliCoreDir, "*.json") + .Concat(Directory.EnumerateFiles(CliCoreDir, "*.dll")) + .ToArray(); + + if (!DoCoreBuildOnly) + { + ZipFiles(CliFrameworkZip, namesFx); + Console.WriteLine($"Created {CliFrameworkZip}"); + } + + ZipFiles(CliCoreZip, namesCore); + Console.WriteLine($"Created {CliCoreZip}"); + + if (Host == HostType.AppVeyor) + { + PushArtifact(CliFrameworkZip); + PushArtifact(CliCoreZip); + PushArtifact(CliMergedExe); + } + }); + + Target Publish => _ => _ + .DependsOn(Test) + .OnlyWhenStatic(() => Host == HostType.AppVeyor) + .OnlyWhenStatic(() => AppVeyor.Instance.PullRequestTitle == null) + .Executes(() => + { + AbsolutePath nupkgFile = ArtifactsDirectory.GlobFiles("*.nupkg").Single(); + AbsolutePath snupkgFile = ArtifactsDirectory.GlobFiles("*.snupkg").Single(); + + string apiKey = EnvironmentInfo.Variable("myget_api_key"); + DotNetNuGetPushSettings settings = new DotNetNuGetPushSettings() + .SetApiKey(apiKey) + .SetSymbolApiKey(apiKey) + .SetSource("https://www.myget.org/F/libhac/api/v2/package") + .SetSymbolSource("https://www.myget.org/F/libhac/symbols/api/v2/package"); + + DotNetNuGetPush(settings.SetTargetPath(nupkgFile)); + DotNetNuGetPush(settings.SetTargetPath(snupkgFile)); + }); + + Target Results => _ => _ + .DependsOn(Test, Zip, Merge, Sign, Publish) + .Executes(() => + { + Console.WriteLine("SHA-1:"); + using (SHA1 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("-", ""); + Console.WriteLine($"{hash} - {Path.GetFileName(filename)}"); + } + } + } + }); + + Target Sign => _ => _ + .DependsOn(Test, Zip, Merge) + .OnlyWhenStatic(() => !DoCoreBuildOnly) + .OnlyWhenStatic(() => File.Exists(CertFileName)) + .Executes(() => + { + string pwd = ReadPassword(); + + if (pwd == string.Empty) + { + Console.WriteLine("Skipping sign task"); + return; + } + + SignAndReZip(pwd); + }); + + public static void ZipFiles(string outFile, IEnumerable files) + { + 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 = DateTime.UnixEpoch; + + using (FileStream fs = File.OpenRead(file)) + { + entry.Size = fs.Length; + s.PutNextEntry(entry); + fs.CopyTo(s); } } } - }); + } - public static void ZipFiles(string outFile, string[] files) - { - using (var s = new ZipOutputStream(File.Create(outFile))) + public static void ZipDirectory(string outFile, string directory) { - s.SetLevel(9); - - foreach (string file in files) + using (var s = new ZipOutputStream(File.Create(outFile))) { - var entry = new ZipEntry(Path.GetFileName(file)); - entry.DateTime = DateTime.UnixEpoch; + s.SetLevel(9); - using (FileStream fs = File.OpenRead(file)) + foreach (string filePath in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)) { - entry.Size = fs.Length; - s.PutNextEntry(entry); - fs.CopyTo(s); + string relativePath = Path.GetRelativePath(directory, filePath); + + var entry = new ZipEntry(relativePath); + entry.DateTime = DateTime.UnixEpoch; + + using (FileStream fs = File.OpenRead(filePath)) + { + entry.Size = fs.Length; + s.PutNextEntry(entry); + fs.CopyTo(s); + } } } } - } - public static void PushArtifact(string path) - { - if (!File.Exists(path)) + public static void ZipDirectory(string outFile, string directory, IEnumerable files) { - Console.WriteLine($"Unable to add artifact {path}"); + 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 = DateTime.UnixEpoch; + + using (FileStream fs = File.OpenRead(absolutePath)) + { + entry.Size = fs.Length; + s.PutNextEntry(entry); + fs.CopyTo(s); + } + } + } } - var psi = new ProcessStartInfo + public static void UnzipFiles(string zipFile, string outDir) { - FileName = "appveyor", - Arguments = $"PushArtifact \"{path}\"", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true - }; + using (var s = new ZipInputStream(File.OpenRead(zipFile))) + { + ZipEntry entry; + while ((entry = s.GetNextEntry()) != null) + { + string outPath = Path.Combine(outDir, entry.Name); - var proc = new Process + 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 void PushArtifact(string path) { - StartInfo = psi - }; + if (!File.Exists(path)) + { + Console.WriteLine($"Unable to add artifact {path}"); + } - proc.Start(); + var psi = new ProcessStartInfo + { + FileName = "appveyor", + Arguments = $"PushArtifact \"{path}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; - proc.WaitForExit(); + var proc = new Process + { + StartInfo = psi + }; - Console.WriteLine($"Added AppVeyor artifact {path}"); - } + proc.Start(); - public static void ReplaceLineEndings(string filename) - { - string text = File.ReadAllText(filename); - File.WriteAllText(filename, text.Replace("\n", "\r\n")); + proc.WaitForExit(); + + Console.WriteLine($"Added AppVeyor artifact {path}"); + } + + public static void SetAppVeyorVersion(string version) + { + var psi = new ProcessStartInfo + { + FileName = "appveyor", + Arguments = $"UpdateBuild -Version \"{version}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + var proc = new Process + { + StartInfo = psi + }; + + proc.Start(); + + proc.WaitForExit(); + } + + 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 netFxDir = TempDirectory / ("sign_" + Path.GetFileName(CliFrameworkZip)); + AbsolutePath coreFxDir = TempDirectory / ("sign_" + Path.GetFileName(CliCoreZip)); + AbsolutePath signedMergedExe = SignedArtifactsDirectory / Path.GetFileName(CliMergedExe); + + try + { + UnzipFiles(CliFrameworkZip, netFxDir); + UnzipFiles(CliCoreZip, coreFxDir); + List pkgFileList = UnzipPackage(nupkgFile, nupkgDir); + + var toSign = new List(); + toSign.AddRange(nupkgDir.GlobFiles("**/LibHac.dll")); + toSign.Add(netFxDir / "hactoolnet.exe"); + toSign.Add(coreFxDir / "hactoolnet.dll"); + toSign.Add(signedMergedExe); + + Directory.CreateDirectory(SignedArtifactsDirectory); + File.Copy(CliMergedExe, signedMergedExe, true); + + SignAssemblies(password, toSign.Select(x => x.ToString()).ToArray()); + + // Avoid having multiple signed versions of the same file + File.Copy(nupkgDir / "lib" / "net46" / "LibHac.dll", netFxDir / "LibHac.dll", true); + File.Copy(nupkgDir / "lib" / "netcoreapp2.1" / "LibHac.dll", coreFxDir / "LibHac.dll", true); + + ZipDirectory(SignedArtifactsDirectory / Path.GetFileName(nupkgFile), nupkgDir, pkgFileList); + ZipDirectory(SignedArtifactsDirectory / Path.GetFileName(CliFrameworkZip), netFxDir); + ZipDirectory(SignedArtifactsDirectory / Path.GetFileName(CliCoreZip), coreFxDir); + + 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(netFxDir, 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(); + } } } diff --git a/build/RepackNuget.cs b/build/RepackNuget.cs new file mode 100644 index 00000000..942b3340 --- /dev/null +++ b/build/RepackNuget.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Linq; +using ICSharpCode.SharpZipLib.Zip; +using Nuke.Common.Tools.NuGet; +using static Nuke.Common.IO.FileSystemTasks; +using static Nuke.Common.IO.PathConstruction; + +namespace LibHacBuild +{ + public partial class Build + { + public void RepackNugetPackage(string path) + { + AbsolutePath tempDir = TempDirectory / Path.GetFileName(path); + AbsolutePath libDir = tempDir / "lib"; + AbsolutePath relsFile = tempDir / "_rels" / ".rels"; + + try + { + EnsureCleanDirectory(tempDir); + List fileList = UnzipPackage(path, tempDir); + + string newPsmdcpName = CalcPsmdcpName(libDir); + string newPsmdcpPath = RenamePsmdcp(tempDir, newPsmdcpName); + EditManifestRelationships(relsFile, newPsmdcpPath); + + int index = fileList.FindIndex(x => x.Contains(".psmdcp")); + fileList[index] = newPsmdcpPath; + + IEnumerable files = Directory.EnumerateFiles(tempDir, "*.json", SearchOption.AllDirectories) + .Concat(Directory.EnumerateFiles(tempDir, "*.xml", SearchOption.AllDirectories)) + .Concat(Directory.EnumerateFiles(tempDir, "*.rels", SearchOption.AllDirectories)) + .Concat(Directory.EnumerateFiles(tempDir, "*.psmdcp", SearchOption.AllDirectories)) + .Concat(Directory.EnumerateFiles(tempDir, "*.nuspec", SearchOption.AllDirectories)); + + foreach (string filename in files) + { + Console.WriteLine(filename); + ReplaceLineEndings(filename); + } + + ZipDirectory(path, tempDir, fileList); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + public List UnzipPackage(string package, string dest) + { + var fileList = new List(); + + UnzipFiles(package, dest); + + using (var s = new ZipInputStream(File.OpenRead(package))) + { + ZipEntry entry; + while ((entry = s.GetNextEntry()) != null) + { + fileList.Add(entry.Name); + } + } + + return fileList; + } + + public static string CalcPsmdcpName(string libDir) + { + using (SHA256 sha = SHA256.Create()) + { + foreach (string file in Directory.EnumerateFiles(libDir)) + { + byte[] data = File.ReadAllBytes(file); + sha.TransformBlock(data, 0, data.Length, data, 0); + } + + sha.TransformFinalBlock(new byte[0], 0, 0); + + return ToHexString(sha.Hash).ToLower().Substring(0, 32); + } + } + + public static string RenamePsmdcp(string packageDir, string name) + { + string fileName = Directory.EnumerateFiles(packageDir, "*.psmdcp", SearchOption.AllDirectories).Single(); + string newFileName = Path.Combine(Path.GetDirectoryName(fileName), name + ".psmdcp"); + Directory.Move(fileName, newFileName); + + return Path.GetRelativePath(packageDir, newFileName).Replace('\\', '/'); + } + + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public void EditManifestRelationships(string path, string psmdcpPath) + { + XDocument doc = XDocument.Load(path); + XNamespace ns = doc.Root.GetDefaultNamespace(); + + foreach (XElement rs in doc.Root.Elements(ns + "Relationship")) + { + using (SHA256 sha = SHA256.Create()) + { + if (rs.Attribute("Target").Value.Contains(".psmdcp")) + { + rs.Attribute("Target").Value = "/" + psmdcpPath; + } + + string s = "/" + psmdcpPath + rs.Attribute("Target").Value; + byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(s)); + string id = "R" + ToHexString(hash).Substring(0, 16); + rs.Attribute("Id").Value = id; + } + } + + doc.Save(path); + } + + public void SignNupkg(string pkgPath, string password) + { + NuGetTasks.NuGet( + $"sign \"{pkgPath}\" -CertificatePath cert.pfx -CertificatePassword {password} -Timestamper http://timestamp.digicert.com", + outputFilter: x => x.Replace(password, "hunter2")); + } + + public static string ToHexString(byte[] arr) + { + return BitConverter.ToString(arr).ToLower().Replace("-", ""); + } + } +} diff --git a/build/_build.csproj b/build/_build.csproj index 1a225dc8..e50e0e1a 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -4,15 +4,17 @@ Exe netcoreapp2.1 false - + LibHacBuild False CS0649;CS0169 - - + + + + diff --git a/src/LibHac/LibHac.csproj b/src/LibHac/LibHac.csproj index 3d2e1e92..8c6ad519 100644 --- a/src/LibHac/LibHac.csproj +++ b/src/LibHac/LibHac.csproj @@ -19,8 +19,9 @@ 0.2.0 $(MSBuildProjectDirectory)=C:/LibHac/ true + snupkg true - $(NoWarn);1591 + $(NoWarn);1591;NU5105 true