diff --git a/Squirrel.sln b/Squirrel.sln index 2f1ce143..6d9fda45 100644 --- a/Squirrel.sln +++ b/Squirrel.sln @@ -11,8 +11,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Setup", "src\Setup\Setup.vc EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Update", "src\Update\Update.csproj", "{1EEBACBC-6982-4696-BD4E-899ED0AC6CD2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SyncReleases", "src\SyncReleases\SyncReleases.csproj", "{EB521191-1EBF-4D06-8541-ED192E2EE378}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionLevel", "SolutionLevel", "{ED657D2C-F8A0-4012-A64F-7367D41BE4D2}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig @@ -26,6 +24,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WriteZipToSetup", "src\Writ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StubExecutable", "src\StubExecutable\StubExecutable.vcxproj", "{C028DB2A-E7C5-4232-8C22-D5FBA2176136}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SquirrelCli", "src\SquirrelCli\SquirrelCli.csproj", "{19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CIBuild|Any CPU = CIBuild|Any CPU @@ -244,54 +244,6 @@ Global {1EEBACBC-6982-4696-BD4E-899ED0AC6CD2}.Release|x64.Build.0 = Release|Any CPU {1EEBACBC-6982-4696-BD4E-899ED0AC6CD2}.Release|x86.ActiveCfg = Release|Any CPU {1EEBACBC-6982-4696-BD4E-899ED0AC6CD2}.Release|x86.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.CIBuild|Any CPU.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.CIBuild|Any CPU.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.CIBuild|Mixed Platforms.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.CIBuild|Mixed Platforms.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.CIBuild|x64.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.CIBuild|x64.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.CIBuild|x86.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.CIBuild|x86.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Coverage|Any CPU.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Coverage|Any CPU.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Coverage|Mixed Platforms.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Coverage|Mixed Platforms.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Coverage|x64.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Coverage|x64.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Coverage|x86.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Coverage|x86.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Debug|x64.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Debug|x64.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Debug|x86.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Debug|x86.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Debug|Any CPU.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Debug|x64.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Debug|x64.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Debug|x86.ActiveCfg = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Debug|x86.Build.0 = Debug|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Release|Any CPU.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Release|Any CPU.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Release|Mixed Platforms.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Release|x64.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Release|x64.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Release|x86.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Mono Release|x86.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Release|Any CPU.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Release|x64.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Release|x64.Build.0 = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Release|x86.ActiveCfg = Release|Any CPU - {EB521191-1EBF-4D06-8541-ED192E2EE378}.Release|x86.Build.0 = Release|Any CPU {4D3C8B70-075D-48A5-9FF3-EDB87347B136}.CIBuild|Any CPU.ActiveCfg = Debug|Win32 {4D3C8B70-075D-48A5-9FF3-EDB87347B136}.CIBuild|Any CPU.Build.0 = Debug|Win32 {4D3C8B70-075D-48A5-9FF3-EDB87347B136}.CIBuild|Mixed Platforms.ActiveCfg = Release|Win32 @@ -386,6 +338,54 @@ Global {C028DB2A-E7C5-4232-8C22-D5FBA2176136}.Release|x64.Build.0 = Release|x64 {C028DB2A-E7C5-4232-8C22-D5FBA2176136}.Release|x86.ActiveCfg = Release|Win32 {C028DB2A-E7C5-4232-8C22-D5FBA2176136}.Release|x86.Build.0 = Release|Win32 + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.CIBuild|Any CPU.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.CIBuild|Any CPU.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.CIBuild|Mixed Platforms.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.CIBuild|Mixed Platforms.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.CIBuild|x64.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.CIBuild|x64.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.CIBuild|x86.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.CIBuild|x86.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Coverage|Any CPU.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Coverage|Any CPU.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Coverage|Mixed Platforms.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Coverage|Mixed Platforms.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Coverage|x64.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Coverage|x64.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Coverage|x86.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Coverage|x86.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Debug|x64.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Debug|x64.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Debug|x86.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Debug|x86.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Debug|Any CPU.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Debug|x64.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Debug|x64.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Debug|x86.ActiveCfg = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Debug|x86.Build.0 = Debug|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Release|Any CPU.ActiveCfg = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Release|Any CPU.Build.0 = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Release|Mixed Platforms.Build.0 = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Release|x64.ActiveCfg = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Release|x64.Build.0 = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Release|x86.ActiveCfg = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Mono Release|x86.Build.0 = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Release|Any CPU.Build.0 = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Release|x64.ActiveCfg = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Release|x64.Build.0 = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Release|x86.ActiveCfg = Release|Any CPU + {19E8EBF5-0277-422F-BF49-C66D9DBA5AA4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build.ps1 b/build.ps1 index da27f6f3..364b50c7 100644 --- a/build.ps1 +++ b/build.ps1 @@ -20,11 +20,11 @@ foreach ($Folder in $Folders) { # Build single-exe packaged projects dotnet publish -v minimal -c Release "$PSScriptRoot\src\Update\Update.csproj" -o "$Out" -dotnet publish -v minimal -c Release "$PSScriptRoot\src\SyncReleases\SyncReleases.csproj" -o "$Out" +dotnet publish -v minimal -c Release "$PSScriptRoot\src\SquirrelCli\SquirrelCli.csproj" -o "$Out" # Copy over all files we need -Move-Item "$Out\Update.exe" -Destination "$Out\Squirrel.exe" -Move-Item "$Out\Update.com" -Destination "$Out\Squirrel.com" +# Move-Item "$Out\Update.exe" -Destination "$Out\Squirrel.exe" +# Move-Item "$Out\Update.com" -Destination "$Out\Squirrel.com" # Move-Item "$Out\Update.pdb" -Destination "$Out\Squirrel.pdb" # New-Item -Path "$Out\lib" -ItemType "directory" | Out-Null @@ -38,7 +38,9 @@ Copy-Item "$In\Win32\WriteZipToSetup.pdb" -Destination "$Out" Copy-Item -Path "$PSScriptRoot\vendor\7zip\*" -Destination "$Out" -Recurse Copy-Item -Path "$PSScriptRoot\vendor\wix\*" -Destination "$Out" -Recurse -Copy-Item "$PSScriptRoot\.nuget\NuGet.exe" -Destination "$Out" +Copy-Item "$PSScriptRoot\vendor\NuGet.exe" -Destination "$Out" +Copy-Item "$PSScriptRoot\vendor\rcedit.exe" -Destination "$Out" +Copy-Item "$PSScriptRoot\vendor\signtool.exe" -Destination "$Out" Remove-Item "$Out\*.pdb" diff --git a/src/Update/AuthenticodeTools.cs b/src/Squirrel/Lib/AuthenticodeTools.cs similarity index 99% rename from src/Update/AuthenticodeTools.cs rename to src/Squirrel/Lib/AuthenticodeTools.cs index f61af622..7cbb4416 100644 --- a/src/Update/AuthenticodeTools.cs +++ b/src/Squirrel/Lib/AuthenticodeTools.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace Squirrel.Update +namespace Squirrel.Lib { internal static class AuthenticodeTools { diff --git a/src/Squirrel/Lib/ValidatedOptionSet.cs b/src/Squirrel/Lib/ValidatedOptionSet.cs new file mode 100644 index 00000000..dea6a4e9 --- /dev/null +++ b/src/Squirrel/Lib/ValidatedOptionSet.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Mono.Options; + +namespace Squirrel.Lib +{ + internal class OptionValidationException : Exception + { + public OptionValidationException(string message) : base(message) + { + } + + public OptionValidationException(string propertyName, string message) : base($"Argument '{propertyName}': {message}") + { + + } + } + + internal abstract class ValidatedOptionSet : OptionSet + { + protected virtual bool IsNullOrDefault(string propertyName) + { + var p = this.GetType().GetProperty(propertyName); + object argument = p.GetValue(this, null); + + // deal with normal scenarios + if (argument == null) return true; + + // deal with non-null nullables + Type methodType = argument.GetType(); + if (Nullable.GetUnderlyingType(methodType) != null) return false; + + // deal with boxed value types + Type argumentType = argument.GetType(); + if (argumentType.IsValueType && argumentType != methodType) { + object obj = Activator.CreateInstance(argument.GetType()); + return obj.Equals(argument); + } + + return false; + } + + protected virtual void IsRequired(params string[] propertyNames) + { + foreach (var property in propertyNames) { + IsRequired(property); + } + } + + protected virtual void IsRequired(string propertyName) + { + if (IsNullOrDefault(propertyName)) + throw new OptionValidationException($"Argument '{propertyName}' is required"); + } + + protected virtual void IsValidFile(string propertyName) + { + var p = this.GetType().GetProperty(propertyName); + var path = p.GetValue(this, null) as string; + if (path != null) + if (!File.Exists(path)) + throw new OptionValidationException($"Argument '{propertyName}': Expected file to exist at this location but no file was found"); + } + + protected virtual void IsValidUrl(string propertyName) + { + var p = this.GetType().GetProperty(propertyName); + var val = p.GetValue(this, null) as string; + if (val != null) + if (!Utility.IsHttpUrl(val)) + throw new OptionValidationException(propertyName, "Must start with http or https and be a valid URI."); + + } + + public abstract void Validate(); + + public virtual void WriteOptionDescriptions() + { + WriteOptionDescriptions(Console.Out); + } + } + + internal abstract class CommandAction + { + public string Command { get; protected set; } + public string Description { get; protected set; } + public abstract void Execute(IEnumerable args); + public abstract void PrintHelp(); + } + + internal class CommandAction : CommandAction where T : ValidatedOptionSet, new() + { + public T Options { get; } + public Action Action { get; } + + public CommandAction(string command, string description, T options, Action action) + { + Command = command; + Description = description; + Options = options; + Action = action; + } + + public override void Execute(IEnumerable args) + { + Options.Parse(args); + Options.Validate(); + Action(Options); + } + + public override void PrintHelp() + { + Options.WriteOptionDescriptions(); + } + } + + internal class CommandSet : List + { + //public CommandSet() : base(StringComparer.InvariantCultureIgnoreCase) { } + + public void Add(string command, string description, T options, Action action) where T : ValidatedOptionSet, new() + { + this.Add(new CommandAction(command, description, options, action)); + } + + public virtual void Execute(string[] args) + { + if (args.Length == 0) + throw new OptionValidationException("Must specify a command to execute."); + + var combined = String.Join(" ", args); + CommandAction cmd = null; + + foreach (var k in this.OrderByDescending(k => k.Command.Length)) { + if (combined.StartsWith(k.Command, StringComparison.InvariantCultureIgnoreCase)) { + cmd = k; + break; + } + } + + if (cmd == null) + throw new OptionValidationException($"Command was not specified or does not exist."); + + cmd.Execute(combined.Substring(cmd.Command.Length).Split(' ')); + } + + public virtual void WriteHelp() + { + var exeName = Path.GetFileName(AssemblyRuntimeInfo.EntryExePath); + Console.WriteLine($"Usage: {exeName} [command] [options]"); + Console.WriteLine(); + Console.WriteLine("Commands:"); + + var array = this.ToArray(); + for (var i = 0; i < array.Length; i++) { + var c = array[i]; + + // print command name + desc + Console.WriteLine(); + Utility.ConsoleWriteWithColor(c.Command, ConsoleColor.Blue); + if (!String.IsNullOrWhiteSpace(c.Description)) + Console.Write(": " + c.Description); + + + //Console.Write(c.Command); + //if(String.IsNullOrWhiteSpace(c.Description)) + // Console.WriteLine(); + //else + // Console.WriteLine(": " + c.Description); + + + //Console.Write(c.); + + // group similar command parameters together + if (i + 1 < array.Length) { + if (c.GetType() == array[i + 1].GetType()) { + continue; + } + + } + + Console.WriteLine(); + c.PrintHelp(); + + //Console.WriteLine(); + //c.Value.WriteOptionDescriptions(); + //Console.WriteLine(); + } + } + } +} diff --git a/src/Squirrel/Properties/AssemblyInfo.cs b/src/Squirrel/Properties/AssemblyInfo.cs index c1bbd998..434a93a2 100644 --- a/src/Squirrel/Properties/AssemblyInfo.cs +++ b/src/Squirrel/Properties/AssemblyInfo.cs @@ -6,3 +6,5 @@ using System.Runtime.InteropServices; [assembly: InternalsVisibleTo("Squirrel.Tests")] [assembly: InternalsVisibleTo("Update")] [assembly: InternalsVisibleTo("SyncReleases")] +[assembly: InternalsVisibleTo("SquirrelCli")] +[assembly: InternalsVisibleTo("Squirrel")] diff --git a/src/Squirrel/Squirrel.csproj b/src/Squirrel/Squirrel.csproj index 27da4818..620bdb9c 100644 --- a/src/Squirrel/Squirrel.csproj +++ b/src/Squirrel/Squirrel.csproj @@ -8,6 +8,7 @@ Squirrel true 9 + Squirrel.Lib diff --git a/src/Squirrel/Utility.cs b/src/Squirrel/Utility.cs index 57c688ef..e121b0ea 100644 --- a/src/Squirrel/Utility.cs +++ b/src/Squirrel/Utility.cs @@ -341,16 +341,24 @@ namespace Squirrel } } - public static string FindHelperExecutable(string toFind, IEnumerable additionalDirs = null) + public static string FindHelperExecutable(string toFind, IEnumerable additionalDirs = null, bool throwWhenNotFound = false) { + if (File.Exists(toFind)) + return Path.GetFullPath(toFind); + additionalDirs = additionalDirs ?? Enumerable.Empty(); var dirs = (new[] { AppContext.BaseDirectory, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) }) - .Concat(additionalDirs ?? Enumerable.Empty()); + .Concat(additionalDirs ?? Enumerable.Empty()).Select(Path.GetFullPath); var exe = @".\" + toFind; - return dirs + var result = dirs .Select(x => Path.Combine(x, toFind)) - .FirstOrDefault(x => File.Exists(x)) ?? exe; + .FirstOrDefault(x => File.Exists(x)); + + if (result == null && throwWhenNotFound) + throw new Exception($"Could not find helper '{exe}'."); + + return result ?? exe; } static string find7Zip() @@ -668,6 +676,47 @@ namespace Squirrel return This.Log().LogIfThrows(LogLevel.Error, message, block); } + public static void WarnIfThrows(this IFullLogger This, Action block, string message = null) + { + This.LogIfThrows(LogLevel.Warn, message, block); + } + + public static Task WarnIfThrows(this IFullLogger This, Func block, string message = null) + { + return This.LogIfThrows(LogLevel.Warn, message, block); + } + + public static Task WarnIfThrows(this IFullLogger This, Func> block, string message = null) + { + return This.LogIfThrows(LogLevel.Warn, message, block); + } + + public static void ErrorIfThrows(this IFullLogger This, Action block, string message = null) + { + This.LogIfThrows(LogLevel.Error, message, block); + } + + public static Task ErrorIfThrows(this IFullLogger This, Func block, string message = null) + { + return This.LogIfThrows(LogLevel.Error, message, block); + } + + public static Task ErrorIfThrows(this IFullLogger This, Func> block, string message = null) + { + return This.LogIfThrows(LogLevel.Error, message, block); + } + + public static void ConsoleWriteWithColor(string text, ConsoleColor color) + { + var fc = Console.ForegroundColor; + var bc = Console.BackgroundColor; + Console.ForegroundColor = color; + Console.BackgroundColor = ConsoleColor.Black; + Console.Write(text); + Console.ForegroundColor = fc; + Console.BackgroundColor = bc; + } + static IFullLogger logger; static IFullLogger Log() { diff --git a/src/SquirrelCli/Options.cs b/src/SquirrelCli/Options.cs new file mode 100644 index 00000000..c693f1ab --- /dev/null +++ b/src/SquirrelCli/Options.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Squirrel.Lib; + +namespace SquirrelCli +{ + internal abstract class BaseOptions : ValidatedOptionSet + { + public string releaseDir { get; private set; } = ".\\Releases"; + public BaseOptions() + { + Add("r=|releaseDir=", "Release directory containing releasified packages", v => releaseDir = v); + } + } + + internal class ReleasifyOptions : BaseOptions + { + public string package { get; set; } + public string splashImage { get; private set; } + public string iconPath { get; private set; } + public string signParams { get; private set; } + public string framework { get; private set; } + public bool noDelta { get; private set; } + public string baseUrl { get; private set; } + + public ReleasifyOptions() + { + Add("p=|package=", "Path to a nuget package to releasify", v => package = v); + Add("s=|splashImage=", "Image to be displayed during installation (can be jpg, png, gif, etc)", v => splashImage = v); + Add("i=|iconPath=", "Ico file that will be used where possible", v => iconPath = v); + Add("n=|signParams=", "Sign the installer via SignTool.exe with the parameters given", v => signParams = v); + Add("f=|framework=", "Set the required .NET framework version, e.g. net461", v => framework = v); + Add("no-delta", "Don't generate delta packages to save time", v => noDelta = true); + Add("b=|baseUrl=", "Provides a base URL to prefix the RELEASES file packages with", v => baseUrl = v, true); + } + + public override void Validate() + { + IsValidFile(nameof(iconPath)); + IsValidFile(nameof(splashImage)); + IsValidUrl(nameof(baseUrl)); + IsRequired(nameof(package)); + IsValidFile(nameof(package)); + } + } + + internal class PackOptions : ReleasifyOptions + { + public string packName { get; private set; } + public string packVersion { get; private set; } + public string packAuthors { get; private set; } + public string packDirectory { get; private set; } + + public PackOptions() + { + Add("packName=", "desc", v => packName = v); + Add("packVersion=", "desc", v => packVersion = v); + Add("packAuthors=", "desc", v => packAuthors = v); + Add("packDirectory=", "desc", v => packDirectory = v); + + // remove 'package' argument + Remove("package"); + Remove("p"); + } + + public override void Validate() + { + IsRequired(nameof(packName), nameof(packVersion), nameof(packAuthors), nameof(packDirectory)); + IsValidFile(nameof(iconPath)); + IsValidFile(nameof(splashImage)); + IsValidUrl(nameof(baseUrl)); + } + } + + internal class SyncBackblazeOptions : BaseOptions + { + public string b2KeyId { get; private set; } + public string b2AppKey { get; private set; } + public string b2BucketId { get; private set; } + + public SyncBackblazeOptions() + { + Add("b2BucketId=", "Id or name of the bucket in B2, S3, etc", v => b2BucketId = v); + Add("b2keyid=", "B2 Auth Key Id", v => b2KeyId = v); + Add("b2key=", "B2 Auth Key", v => b2AppKey = v); + } + + public override void Validate() + { + IsRequired(nameof(b2KeyId), nameof(b2AppKey), nameof(b2BucketId)); + } + } + + internal class SyncHttpOptions : BaseOptions + { + public string url { get; private set; } + public string token { get; private set; } + + public SyncHttpOptions() + { + Add("url=", "Url to the simple http folder where the releases are found", v => url = v); + } + + public override void Validate() + { + IsRequired(nameof(url)); + } + } + + internal class SyncGithubOptions : BaseOptions + { + public string repoUrl { get; private set; } + public string token { get; private set; } + + public SyncGithubOptions() + { + Add("repoUrl=", "Url to the github repository (eg. 'https://github.com/myname/myrepo')", v => repoUrl = v); + Add("token=", "The oauth token to use as login credentials", v => token = v); + } + + public override void Validate() + { + IsRequired(nameof(repoUrl)); + } + } +} diff --git a/src/SquirrelCli/Program.cs b/src/SquirrelCli/Program.cs new file mode 100644 index 00000000..6ac4c138 --- /dev/null +++ b/src/SquirrelCli/Program.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Mono.Options; +using Squirrel; +using Squirrel.Json; +using Squirrel.Lib; +using Squirrel.NuGet; +using Squirrel.SimpleSplat; +using SquirrelCli.Sources; + +namespace SquirrelCli +{ + class Program : IEnableLogger + { + public static int Main(string[] args) + { + //var pg = new Program(); + var commands = new CommandSet { + { "releasify", "Take an existing nuget package and turn it into a Squirrel release", new ReleasifyOptions(), Releasify }, + { "pack", "Creates a nuget package from a folder and releasifies it in a single step", new PackOptions(), Pack }, + { "b2-down", "Download recent releases from BackBlaze B2", new SyncBackblazeOptions(), o => new BackblazeRepository(o).DownloadRecentPackages().Wait() }, + { "b2-up", "Upload releases to BackBlaze B2", new SyncBackblazeOptions(), o => new BackblazeRepository(o).UploadMissingPackages().Wait() }, + { "http-down", "Download recent releases from an HTTP source", new SyncHttpOptions(), o => new SimpleWebRepository(o).DownloadRecentPackages().Wait() }, + //{ "http-up", "sync", new SyncHttpOptions(), o => new SimpleWebRepository(o).UploadMissingPackages().Wait() }, + { "github-down", "Download recent releases from GitHub", new SyncGithubOptions(), o => new GitHubRepository(o).DownloadRecentPackages().Wait() }, + //{ "github-up", "sync", new SyncGithubOptions(), o => new GitHubRepository(o).UploadMissingPackages().Wait() }, + }; + + var logger = new ConsoleLogger(); + + try { + // check for help argument + bool help = false; + new OptionSet() { { "h|?|help", _ => help = true }, }.Parse(args); + if (help) { + commands.WriteHelp(); + return -1; + } else { + // parse cli and run command + SquirrelLocator.CurrentMutable.Register(() => logger, typeof(Squirrel.SimpleSplat.ILogger)); + commands.Execute(args); + } + return 0; + } catch (Exception ex) { + Console.WriteLine(); + logger.Write(ex.ToString(), LogLevel.Error); + Console.WriteLine(); + commands.WriteHelp(); + return -1; + } + } + + static IFullLogger Log => SquirrelLocator.Current.GetService().GetLogger(typeof(Program)); + + static string[] VendorDirs => new string[] { + Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor") + }; + static string BootstrapperPath => Utility.FindHelperExecutable("Setup.exe", throwWhenNotFound: true); + static string UpdatePath => Utility.FindHelperExecutable("Update.exe", throwWhenNotFound: true); + static string NugetPath => Utility.FindHelperExecutable("NuGet.exe", VendorDirs, throwWhenNotFound: true); + + static void Pack(PackOptions options) + { + using (Utility.WithTempDirectory(out var tmpDir)) { + string nuspec = $@" + + + + {options.packName} + {options.packName} + {options.packName} + {options.packAuthors} + 0 + + + + + +".Trim(); + var nuspecPath = Path.Combine(tmpDir, options.packName + ".nuspec"); + File.WriteAllText(nuspecPath, nuspec); + + var args = $"pack \"{nuspecPath}\" -BasePath \"{options.packDirectory}\" -OutputDirectory \"{tmpDir}\" -Version {options.packVersion}"; + + Log.Info($"Packing '{options.packDirectory}' into nupkg."); + var res = Utility.InvokeProcessAsync(NugetPath, args, CancellationToken.None).Result; + + if (res.Item1 != 0) + throw new Exception($"Failed nuget pack (exit {res.Item1}): \r\n " + res.Item2); + + var nupkgPath = Directory.EnumerateFiles(tmpDir).Where(f => f.EndsWith(".nupkg")).FirstOrDefault(); + if (nupkgPath == null) + throw new Exception($"Failed to generate nupkg, unspecified error"); + + options.package = nupkgPath; + Releasify(options); + } + } + + static void Releasify(ReleasifyOptions options) + { + var targetDir = options.releaseDir ?? Path.Combine(".", "Releases"); + if (!Directory.Exists(targetDir)) { + Directory.CreateDirectory(targetDir); + } + + var frameworkVersion = options.framework; + var signingOpts = options.signParams; + var package = options.package; + var baseUrl = options.baseUrl; + var generateDeltas = !options.noDelta; + var backgroundGif = options.splashImage; + var setupIcon = options.iconPath; + + // validate that the provided "frameworkVersion" is supported by Setup.exe + if (!String.IsNullOrWhiteSpace(frameworkVersion)) { + var chkFrameworkResult = Utility.InvokeProcessAsync(BootstrapperPath, "--checkFramework " + frameworkVersion, CancellationToken.None).Result; + if (chkFrameworkResult.Item1 != 0) { + throw new ArgumentException($"Unsupported FrameworkVersion: '{frameworkVersion}'. {chkFrameworkResult.Item2}"); + } + } + + // copy input package to target output directory + if (!package.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase)) + throw new ArgumentException("package must be packed with nuget and end in '.nupkg'"); + var di = new DirectoryInfo(targetDir); + File.Copy(package, Path.Combine(di.FullName, Path.GetFileName(package)), true); + + var allNuGetFiles = di.EnumerateFiles() + .Where(x => x.Name.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase)); + + var toProcess = allNuGetFiles.Where(x => !x.Name.Contains("-delta") && !x.Name.Contains("-full")); + var processed = new List(); + + var releaseFilePath = Path.Combine(di.FullName, "RELEASES"); + var previousReleases = new List(); + if (File.Exists(releaseFilePath)) { + previousReleases.AddRange(ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFilePath, Encoding.UTF8))); + } + + foreach (var file in toProcess) { + Log.Info("Creating release package: " + file.FullName); + + var rp = new ReleasePackage(file.FullName); + rp.CreateReleasePackage(Path.Combine(di.FullName, rp.SuggestedReleaseFileName), contentsPostProcessHook: pkgPath => { + + // create sub executable for all exe's in this package (except Squirrel!) + new DirectoryInfo(pkgPath).GetAllFilesRecursively() + .Where(x => x.Name.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) + .Where(x => !x.Name.Contains("squirrel.exe", StringComparison.InvariantCultureIgnoreCase)) + .Where(x => Utility.IsFileTopLevelInPackage(x.FullName, pkgPath)) + .Where(x => Utility.ExecutableUsesWin32Subsystem(x.FullName)) + .ForEachAsync(x => createExecutableStubForExe(x.FullName)) + .Wait(); + + // copy myself into the package so Squirrel can also be updated + // how we find the lib dir is a huge hack here, but 'ReleasePackage' verifies there can only be one of these so it should be fine. + var re = new Regex(@"lib[\\\/][^\\\/]*[\\\/]?", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + var libDir = Directory + .EnumerateDirectories(pkgPath, "*", SearchOption.AllDirectories) + .Where(d => re.IsMatch(d)) + .OrderBy(d => d.Length) + .FirstOrDefault(); + File.Copy(UpdatePath, Path.Combine(libDir, "Squirrel.exe")); + + // sign all exe's in this package + if (signingOpts == null) return; + new DirectoryInfo(pkgPath).GetAllFilesRecursively() + .Where(x => Utility.FileIsLikelyPEImage(x.Name)) + .ForEachAsync(async x => { + if (isPEFileSigned(x.FullName)) { + Log.Info("{0} is already signed, skipping", x.FullName); + return; + } + + Log.Info("About to sign {0}", x.FullName); + await signPEFile(x.FullName, signingOpts); + }, 1) + .Wait(); + }); + + processed.Add(rp.ReleasePackageFile); + + var prev = ReleaseEntry.GetPreviousRelease(previousReleases, rp, targetDir); + if (prev != null && generateDeltas) { + var deltaBuilder = new DeltaPackageBuilder(null); + + var dp = deltaBuilder.CreateDeltaPackage(prev, rp, + Path.Combine(di.FullName, rp.SuggestedReleaseFileName.Replace("full", "delta"))); + processed.Insert(0, dp.InputPackageFile); + } + } + + foreach (var file in toProcess) { File.Delete(file.FullName); } + + var newReleaseEntries = processed + .Select(packageFilename => ReleaseEntry.GenerateFromFile(packageFilename, baseUrl)) + .ToList(); + var distinctPreviousReleases = previousReleases + .Where(x => !newReleaseEntries.Select(e => e.Version).Contains(x.Version)); + var releaseEntries = distinctPreviousReleases.Concat(newReleaseEntries).ToList(); + + ReleaseEntry.WriteReleaseFile(releaseEntries, releaseFilePath); + + var targetSetupExe = Path.Combine(di.FullName, "Setup.exe"); + var newestFullRelease = Squirrel.EnumerableExtensions.MaxBy(releaseEntries, x => x.Version).Where(x => !x.IsDelta).First(); + + File.Copy(BootstrapperPath, targetSetupExe, true); + var zipPath = createSetupEmbeddedZip(Path.Combine(di.FullName, newestFullRelease.Filename), di.FullName, signingOpts, setupIcon).Result; + + var writeZipToSetup = Utility.FindHelperExecutable("WriteZipToSetup.exe"); + + try { + string arguments = $"\"{targetSetupExe}\" \"{zipPath}\""; + if (!String.IsNullOrWhiteSpace(frameworkVersion)) { + arguments += $" --set-required-framework \"{frameworkVersion}\""; + } + if (!String.IsNullOrWhiteSpace(backgroundGif)) { + arguments += $" --set-splash \"{Path.GetFullPath(backgroundGif)}\""; + } + + var result = Utility.InvokeProcessAsync(writeZipToSetup, arguments, CancellationToken.None).Result; + if (result.Item1 != 0) throw new Exception("Failed to write Zip to Setup.exe!\n\n" + result.Item2); + } catch (Exception ex) { + Log.ErrorException("Failed to update Setup.exe with new Zip file", ex); + throw; + } finally { + File.Delete(zipPath); + } + + Utility.Retry(() => + setPEVersionInfoAndIcon(targetSetupExe, new ZipPackage(package), setupIcon).Wait()); + + if (signingOpts != null) { + signPEFile(targetSetupExe, signingOpts).Wait(); + } + + //if (generateMsi) { + // createMsiPackage(targetSetupExe, new ZipPackage(package), packageAs64Bit).Wait(); + + // if (signingOpts != null) { + // signPEFile(targetSetupExe.Replace(".exe", ".msi"), signingOpts).Wait(); + // } + //} + } + + static async Task createSetupEmbeddedZip(string fullPackage, string releasesDir, string signingOpts, string setupIcon) + { + string tempPath; + + Log.Info("Building embedded zip file for Setup.exe"); + using (Utility.WithTempDirectory(out tempPath, null)) { + Log.ErrorIfThrows(() => { + File.Copy(UpdatePath, Path.Combine(tempPath, "Update.exe")); + File.Copy(fullPackage, Path.Combine(tempPath, Path.GetFileName(fullPackage))); + }, "Failed to write package files to temp dir: " + tempPath); + + if (!String.IsNullOrWhiteSpace(setupIcon)) { + Log.ErrorIfThrows(() => { + File.Copy(setupIcon, Path.Combine(tempPath, "setupIcon.ico")); + }, "Failed to write icon to temp dir: " + tempPath); + } + + var releases = new[] { ReleaseEntry.GenerateFromFile(fullPackage) }; + ReleaseEntry.WriteReleaseFile(releases, Path.Combine(tempPath, "RELEASES")); + + var target = Path.GetTempFileName(); + File.Delete(target); + + // Sign Update.exe so that virus scanners don't think we're + // pulling one over on them + if (signingOpts != null) { + var di = new DirectoryInfo(tempPath); + + var files = di.EnumerateFiles() + .Where(x => x.Name.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) + .Select(x => x.FullName); + + await files.ForEachAsync(x => signPEFile(x, signingOpts)); + } + + Log.ErrorIfThrows(() => + ZipFile.CreateFromDirectory(tempPath, target, CompressionLevel.Optimal, false), + "Failed to create Zip file from directory: " + tempPath); + + return target; + } + } + + static async Task signPEFile(string exePath, string signingOpts) + { + // Try to find SignTool.exe + var exe = @".\signtool.exe"; + if (!File.Exists(exe)) { + exe = Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "signtool.exe"); + + // Run down PATH and hope for the best + if (!File.Exists(exe)) exe = "signtool.exe"; + } + + var processResult = await Utility.InvokeProcessAsync(exe, + String.Format("sign {0} \"{1}\"", signingOpts, exePath), CancellationToken.None); + + if (processResult.Item1 != 0) { + var optsWithPasswordHidden = new Regex(@"/p\s+\w+").Replace(signingOpts, "/p ********"); + var msg = String.Format("Failed to sign, command invoked was: '{0} sign {1} {2}'", + exe, optsWithPasswordHidden, exePath); + + throw new Exception(msg); + } else { + Console.WriteLine(processResult.Item2); + } + } + static bool isPEFileSigned(string path) + { + try { + return AuthenticodeTools.IsTrusted(path); + } catch (Exception ex) { + Log.ErrorException("Failed to determine signing status for " + path, ex); + return false; + } + } + + static async Task createExecutableStubForExe(string fullName) + { + var exe = Utility.FindHelperExecutable(@"StubExecutable.exe"); + + var target = Path.Combine( + Path.GetDirectoryName(fullName), + Path.GetFileNameWithoutExtension(fullName) + "_ExecutionStub.exe"); + + await Utility.CopyToAsync(exe, target); + + await Utility.InvokeProcessAsync( + Utility.FindHelperExecutable("WriteZipToSetup.exe"), + String.Format("--copy-stub-resources \"{0}\" \"{1}\"", fullName, target), + CancellationToken.None); + } + + static async Task setPEVersionInfoAndIcon(string exePath, IPackage package, string iconPath = null) + { + var realExePath = Path.GetFullPath(exePath); + var company = String.Join(",", package.Authors); + var verStrings = new Dictionary() { + { "CompanyName", company }, + { "LegalCopyright", package.Copyright ?? "Copyright © " + DateTime.Now.Year.ToString() + " " + company }, + { "FileDescription", package.Summary ?? package.Description ?? "Installer for " + package.Id }, + { "ProductName", package.Description ?? package.Summary ?? package.Id }, + }; + + var args = verStrings.Aggregate(new StringBuilder("\"" + realExePath + "\""), (acc, x) => { acc.AppendFormat(" --set-version-string \"{0}\" \"{1}\"", x.Key, x.Value); return acc; }); + args.AppendFormat(" --set-file-version {0} --set-product-version {0}", package.Version.ToString()); + if (iconPath != null) { + args.AppendFormat(" --set-icon \"{0}\"", Path.GetFullPath(iconPath)); + } + + // Try to find rcedit.exe + string exe = Utility.FindHelperExecutable("rcedit.exe"); + + var processResult = await Utility.InvokeProcessAsync(exe, args.ToString(), CancellationToken.None); + + if (processResult.Item1 != 0) { + var msg = String.Format( + "Failed to modify resources, command invoked was: '{0} {1}'\n\nOutput was:\n{2}", + exe, args, processResult.Item2); + + throw new Exception(msg); + } else { + Console.WriteLine(processResult.Item2); + } + } + } + + class ConsoleLogger : Squirrel.SimpleSplat.ILogger + { + readonly object gate = 42; + public LogLevel Level { get; set; } = LogLevel.Info; + public void Write(string message, LogLevel logLevel) + { + if (logLevel < Level) { + return; + } + + lock (gate) { + string lvl = logLevel.ToString().Substring(0, 4).ToUpper(); + if (logLevel == LogLevel.Error || logLevel == LogLevel.Fatal) { + Utility.ConsoleWriteWithColor($"[{lvl}] {message}\r\n", ConsoleColor.Red); + } else if (logLevel == LogLevel.Warn) { + Utility.ConsoleWriteWithColor($"[{lvl}] {message}\r\n", ConsoleColor.Yellow); + } else { + Console.WriteLine($"[{lvl}] {message}"); + } + } + } + } +} diff --git a/src/SquirrelCli/SquirrelCli.csproj b/src/SquirrelCli/SquirrelCli.csproj new file mode 100644 index 00000000..a21e0244 --- /dev/null +++ b/src/SquirrelCli/SquirrelCli.csproj @@ -0,0 +1,33 @@ + + + + Exe + net6.0 + true + true + true + true + win-x86 + squirrel.ico + Squirrel + en + + + + + + + + + + + + + + + + + + + + diff --git a/src/SyncReleases/Sources/BackblazeRepository.cs b/src/SquirrelCli/Sync/BackblazeRepository.cs similarity index 93% rename from src/SyncReleases/Sources/BackblazeRepository.cs rename to src/SquirrelCli/Sync/BackblazeRepository.cs index 7d8773c0..5af280f2 100644 --- a/src/SyncReleases/Sources/BackblazeRepository.cs +++ b/src/SquirrelCli/Sync/BackblazeRepository.cs @@ -8,17 +8,19 @@ using B2Net; using B2Net.Models; using Squirrel; -namespace Squirrel.SyncReleases.Sources +namespace SquirrelCli.Sources { internal class BackblazeRepository : IPackageRepository { private B2StorageProvider _b2; - public BackblazeRepository(string keyId, string appKey, string bucketId) + private DirectoryInfo releasesDir; + public BackblazeRepository(SyncBackblazeOptions options) { - _b2 = new B2StorageProvider(keyId, appKey, bucketId); + _b2 = new B2StorageProvider(options.b2KeyId, options.b2AppKey, options.b2BucketId); + releasesDir = new DirectoryInfo(options.releaseDir); } - public async Task DownloadRecentPackages(DirectoryInfo releasesDir) + public async Task DownloadRecentPackages() { Console.WriteLine("Downloading RELEASES"); var releasesBytes = await _b2.DownloadFile("RELEASES"); @@ -45,7 +47,7 @@ namespace Squirrel.SyncReleases.Sources } } - public async Task UploadMissingPackages(DirectoryInfo releasesDir) + public async Task UploadMissingPackages() { foreach (var f in releasesDir.GetFiles()) { await _b2.UploadFile(File.ReadAllBytes(f.FullName), f.Name); diff --git a/src/SquirrelCli/Sync/GitHubRepository.cs b/src/SquirrelCli/Sync/GitHubRepository.cs new file mode 100644 index 00000000..b448d05d --- /dev/null +++ b/src/SquirrelCli/Sync/GitHubRepository.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace SquirrelCli.Sources +{ + internal class GitHubRepository : IPackageRepository + { + private SyncGithubOptions _options; + + public GitHubRepository(SyncGithubOptions options) + { + _options = options; + } + + public Task DownloadRecentPackages() + { + return SyncImplementations.SyncFromGitHub(_options.repoUrl, _options.token, new DirectoryInfo(_options.releaseDir)); + } + + public Task UploadMissingPackages() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/SquirrelCli/Sync/IPackageRepository.cs b/src/SquirrelCli/Sync/IPackageRepository.cs new file mode 100644 index 00000000..ced04db4 --- /dev/null +++ b/src/SquirrelCli/Sync/IPackageRepository.cs @@ -0,0 +1,11 @@ +using System.IO; +using System.Threading.Tasks; + +namespace SquirrelCli.Sources +{ + internal interface IPackageRepository + { + public Task DownloadRecentPackages(); + public Task UploadMissingPackages(); + } +} diff --git a/src/SquirrelCli/Sync/SimpleWebRepository.cs b/src/SquirrelCli/Sync/SimpleWebRepository.cs new file mode 100644 index 00000000..eeed8bc5 --- /dev/null +++ b/src/SquirrelCli/Sync/SimpleWebRepository.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace SquirrelCli.Sources +{ + + internal class SimpleWebRepository : IPackageRepository + { + private readonly SyncHttpOptions options; + + public SimpleWebRepository(SyncHttpOptions options) + { + this.options = options; + } + + public Task DownloadRecentPackages() + { + return SyncImplementations.SyncRemoteReleases(new Uri(options.url), new DirectoryInfo(options.releaseDir)); + } + + public Task UploadMissingPackages() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/SyncReleases/Sources/SyncImplementations.cs b/src/SquirrelCli/Sync/SyncImplementations.cs similarity index 97% rename from src/SyncReleases/Sources/SyncImplementations.cs rename to src/SquirrelCli/Sync/SyncImplementations.cs index e3758404..4a794ae7 100644 --- a/src/SyncReleases/Sources/SyncImplementations.cs +++ b/src/SquirrelCli/Sync/SyncImplementations.cs @@ -8,7 +8,7 @@ using Octokit; using System.Reflection; using System.Net; -namespace Squirrel.SyncReleases.Sources +namespace SquirrelCli.Sources { internal class SyncImplementations { @@ -151,6 +151,7 @@ namespace Squirrel.SyncReleases.Sources } } +#pragma warning disable SYSLIB0014 // Type or member is obsolete class NotBrokenWebClient : WebClient { protected override WebRequest GetWebRequest(Uri address) @@ -164,4 +165,5 @@ namespace Squirrel.SyncReleases.Sources return hwr; } } +#pragma warning restore SYSLIB0014 // Type or member is obsolete } diff --git a/src/SquirrelCli/app.manifest b/src/SquirrelCli/app.manifest new file mode 100644 index 00000000..532ca1ee --- /dev/null +++ b/src/SquirrelCli/app.manifest @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SquirrelCli/squirrel.ico b/src/SquirrelCli/squirrel.ico new file mode 100644 index 00000000..d9bd2a75 Binary files /dev/null and b/src/SquirrelCli/squirrel.ico differ diff --git a/src/SyncReleases/Program.cs b/src/SyncReleases/Program.cs deleted file mode 100644 index 25808eb8..00000000 --- a/src/SyncReleases/Program.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Mono.Options; -using Octokit; -using Squirrel.SimpleSplat; -using Squirrel; -using Squirrel.Json; -using Squirrel.SyncReleases.Sources; - -namespace Squirrel.SyncReleases -{ - class Program : IEnableLogger - { - static OptionSet opts; - - public static int Main(string[] args) - { - var pg = new Program(); - try { - return pg.main(args).GetAwaiter().GetResult(); - } catch (Exception ex) { - Console.Error.WriteLine(ex); - Console.Error.WriteLine("> SyncReleases.exe -h for help"); - return -1; - } - } - - async Task main(string[] args) - { - using (var logger = new SetupLogLogger(false) { Level = Squirrel.SimpleSplat.LogLevel.Info }) { - Squirrel.SimpleSplat.SquirrelLocator.CurrentMutable.Register(() => logger, typeof(Squirrel.SimpleSplat.ILogger)); - - var releaseDir = default(string); - var repoUrl = default(string); - var token = default(string); - var provider = default(string); - var upload = default(bool); - var showHelp = default(bool); - - var b2KeyId = default(string); - var b2AppKey = default(string); - var bucketId = default(string); - - opts = new OptionSet() { - "Usage: SyncReleases.exe [OPTS]", - "Utility to download from or upload packages to a remote package release repository", - "Can be used to fully automate the distribution of new releases from CI or other scripts", - "", - "Options:", - { "h|?|help", "Display help and exit", v => showHelp = true }, - { "r=|releaseDir=", "Path to the local release directory to sync with remote", v => releaseDir = v}, - { "u=|url=", "A GitHub repository url, or a remote web url", v => repoUrl = v}, - { "t=|token=", "The OAuth token to use as login credentials", v => token = v}, - { "p=|provider=", "Specify the release repository type, can be 'github', 'web', or 'b2'", v => provider = v }, - { "upload", "Upload releases in the releaseDir to the remote repository", v => upload = true }, - { "bucketId=", "Id or name of the bucket in B2, S3, etc", v => bucketId = v }, - { "b2keyid=", "B2 Auth Key Id", v => b2KeyId = v }, - { "b2key=", "B2 Auth Key", v => b2AppKey = v }, - }; - - opts.Parse(args); - - if (showHelp) { - ShowHelp(); - return 0; - } - - if (String.IsNullOrWhiteSpace(provider)) throw new ArgumentNullException(nameof(provider)); - if (String.IsNullOrWhiteSpace(releaseDir)) throw new ArgumentNullException(nameof(releaseDir)); - var releaseDirectoryInfo = new DirectoryInfo(releaseDir ?? Path.Combine(".", "Releases")); - if (!releaseDirectoryInfo.Exists) releaseDirectoryInfo.Create(); - - IPackageRepository repository; - - if (provider.Equals("github", StringComparison.OrdinalIgnoreCase)) { - if (String.IsNullOrWhiteSpace(repoUrl)) throw new ArgumentNullException(nameof(repoUrl)); - if (String.IsNullOrWhiteSpace(token)) throw new ArgumentNullException(nameof(repoUrl)); - repository = new GitHubRepository(repoUrl, token); - } else if (provider.Equals("web", StringComparison.OrdinalIgnoreCase)) { - if (String.IsNullOrWhiteSpace(repoUrl)) throw new ArgumentNullException(nameof(repoUrl)); - repository = new SimpleWebRepository(new Uri(repoUrl)); - } else if (provider.Equals("b2", StringComparison.OrdinalIgnoreCase)) { - if (String.IsNullOrWhiteSpace(b2KeyId)) throw new ArgumentNullException(nameof(b2KeyId)); - if (String.IsNullOrWhiteSpace(b2AppKey)) throw new ArgumentNullException(nameof(b2AppKey)); - if (String.IsNullOrWhiteSpace(bucketId)) throw new ArgumentNullException(nameof(bucketId)); - repository = new BackblazeRepository(b2KeyId, b2AppKey, bucketId); - } else { - throw new Exception("Release provider missing or invalid"); - } - - var mode = upload ? "Uploading" : "Downloading"; - Console.WriteLine(mode + " using provider " + repository.GetType().Name); - - if (upload) { - await repository.UploadMissingPackages(releaseDirectoryInfo); - } else { - await repository.DownloadRecentPackages(releaseDirectoryInfo); - } - } - - return 0; - } - - public void ShowHelp() - { - opts.WriteOptionDescriptions(Console.Out); - } - } - - class SetupLogLogger : Squirrel.SimpleSplat.ILogger, IDisposable - { - StreamWriter inner; - readonly object gate = 42; - public Squirrel.SimpleSplat.LogLevel Level { get; set; } - - public SetupLogLogger(bool saveInTemp) - { - var dir = saveInTemp ? - Path.GetTempPath() : - AppContext.BaseDirectory; - - var file = Path.Combine(dir, "SquirrelSetup.log"); - if (File.Exists(file)) File.Delete(file); - - inner = new StreamWriter(file, false, Encoding.UTF8); - } - - public void Write(string message, LogLevel logLevel) - { - if (logLevel < Level) { - return; - } - - lock (gate) inner.WriteLine(message); - } - - public void Dispose() - { - lock (gate) inner.Dispose(); - } - } -} diff --git a/src/SyncReleases/Sources/GitHubRepository.cs b/src/SyncReleases/Sources/GitHubRepository.cs deleted file mode 100644 index dbce6710..00000000 --- a/src/SyncReleases/Sources/GitHubRepository.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; - -namespace Squirrel.SyncReleases.Sources -{ - internal class GitHubRepository : IPackageRepository - { - public string RepoUrl { get; } - public string Token { get; } - - public GitHubRepository(string repoUrl, string token) - { - RepoUrl = repoUrl; - Token = token; - } - - public Task DownloadRecentPackages(DirectoryInfo releasesDir) - { - return SyncImplementations.SyncFromGitHub(RepoUrl, Token, releasesDir); - } - - public Task UploadMissingPackages(DirectoryInfo releasesDir) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/SyncReleases/Sources/IPackageRepository.cs b/src/SyncReleases/Sources/IPackageRepository.cs deleted file mode 100644 index 1e26162b..00000000 --- a/src/SyncReleases/Sources/IPackageRepository.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.IO; -using System.Threading.Tasks; - -namespace Squirrel.SyncReleases.Sources -{ - internal interface IPackageRepository - { - public Task DownloadRecentPackages(DirectoryInfo releasesDir); - public Task UploadMissingPackages(DirectoryInfo releasesDir); - } -} diff --git a/src/SyncReleases/Sources/SimpleWebRepository.cs b/src/SyncReleases/Sources/SimpleWebRepository.cs deleted file mode 100644 index 08b5d7c7..00000000 --- a/src/SyncReleases/Sources/SimpleWebRepository.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; - -namespace Squirrel.SyncReleases.Sources -{ - - internal class SimpleWebRepository : IPackageRepository - { - public Uri TargetUri { get; } - - public SimpleWebRepository(Uri targetUri) - { - TargetUri = targetUri; - } - - public Task DownloadRecentPackages(DirectoryInfo releasesDir) - { - return SyncImplementations.SyncRemoteReleases(TargetUri, releasesDir); - } - - public Task UploadMissingPackages(DirectoryInfo releasesDir) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/SyncReleases/SyncReleases.csproj b/src/SyncReleases/SyncReleases.csproj deleted file mode 100644 index 48a7cb3f..00000000 --- a/src/SyncReleases/SyncReleases.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - - net6.0 - Exe - 9 - Squirrel.SyncReleases - en - true - true - true - win-x86 - true - - - - - - - - - - - - - - - - diff --git a/src/Update/Program.cs b/src/Update/Program.cs index 151a3684..7cf0deeb 100644 --- a/src/Update/Program.cs +++ b/src/Update/Program.cs @@ -13,6 +13,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Squirrel.NuGet; +using Squirrel.Lib; namespace Squirrel.Update { diff --git a/src/Update/Update.com b/src/Update/Update.com deleted file mode 100644 index 31bb3023..00000000 Binary files a/src/Update/Update.com and /dev/null differ diff --git a/src/Update/Update.csproj b/src/Update/Update.csproj index b13e0da1..8743edc6 100644 --- a/src/Update/Update.csproj +++ b/src/Update/Update.csproj @@ -22,12 +22,6 @@ update.ico - - - - - - diff --git a/.nuget/NuGet.exe b/vendor/NuGet.exe similarity index 100% rename from .nuget/NuGet.exe rename to vendor/NuGet.exe diff --git a/src/Update/rcedit.exe b/vendor/rcedit.exe similarity index 100% rename from src/Update/rcedit.exe rename to vendor/rcedit.exe diff --git a/src/Update/signtool.exe b/vendor/signtool.exe similarity index 100% rename from src/Update/signtool.exe rename to vendor/signtool.exe