mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
WIP: basic restructuring and renames
This commit is contained in:
22
Squirrel.sln
22
Squirrel.sln
@@ -18,14 +18,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionLevel", "SolutionLe
|
|||||||
version.json = version.json
|
version.json = version.json
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine", "src\Squirrel.CommandLine\Squirrel.CommandLine.csproj", "{352C15EA-622F-4132-80D8-9B6E3C83404E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Packaging", "src\Squirrel.Packaging\Squirrel.Packaging.csproj", "{352C15EA-622F-4132-80D8-9B6E3C83404E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Tool", "src\Squirrel.Tool\Squirrel.Tool.csproj", "{9E769C7E-A54C-4844-8362-727D37BB1578}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Csq", "src\Squirrel.Csq\Squirrel.Csq.csproj", "{9E769C7E-A54C-4844-8362-727D37BB1578}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine.Tests", "test\Squirrel.CommandLine.Tests\Squirrel.CommandLine.Tests.csproj", "{519EAB50-47B8-425F-8B20-AB9548F220B4}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine.Tests", "test\Squirrel.CommandLine.Tests\Squirrel.CommandLine.Tests.csproj", "{519EAB50-47B8-425F-8B20-AB9548F220B4}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7AC3A776-B582-4B65-9D03-BD52332B5CA3}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7AC3A776-B582-4B65-9D03-BD52332B5CA3}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Packaging.Windows", "src\Squirrel.Packaging.Windows\Squirrel.Packaging.Windows.csproj", "{E35039C8-1F98-48EB-B7D5-08E33DF061A7}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Packaging.OSX", "src\Squirrel.Packaging.OSX\Squirrel.Packaging.OSX.csproj", "{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Deployment", "src\Squirrel.Deployment\Squirrel.Deployment.csproj", "{D19EA72C-E7AE-4A7B-924A-E7550901A49C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -52,6 +58,18 @@ Global
|
|||||||
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
8
nuget.config
Normal file
8
nuget.config
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="NuGet.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="CommandLineNightly" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
|
|
||||||
<LangVersion>9</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<SignAssembly>True</SignAssembly>
|
<SignAssembly>True</SignAssembly>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
<AssemblyOriginatorKeyFile>..\..\Squirrel.snk</AssemblyOriginatorKeyFile>
|
<AssemblyOriginatorKeyFile>..\..\Squirrel.snk</AssemblyOriginatorKeyFile>
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.CommandLine;
|
|
||||||
using System.CommandLine.Parsing;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Squirrel.CommandLine.Commands
|
|
||||||
{
|
|
||||||
public class BaseCommand : Command
|
|
||||||
{
|
|
||||||
public RID TargetRuntime { get; set; }
|
|
||||||
|
|
||||||
public string ReleaseDirectory { get; private set; }
|
|
||||||
|
|
||||||
protected Option<DirectoryInfo> ReleaseDirectoryOption { get; private set; }
|
|
||||||
|
|
||||||
//protected static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(BaseCommand));
|
|
||||||
private Dictionary<Option, Action<ParseResult>> _setters = new();
|
|
||||||
|
|
||||||
protected BaseCommand(string name, string description)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
ReleaseDirectoryOption = AddOption<DirectoryInfo>((v) => ReleaseDirectory = v.ToFullNameOrNull(), "-o", "--outputDir")
|
|
||||||
.SetDescription("Output directory for Squirrel packages.")
|
|
||||||
.SetArgumentHelpName("DIR")
|
|
||||||
.SetDefault(new DirectoryInfo(".\\Releases"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public DirectoryInfo GetReleaseDirectory()
|
|
||||||
{
|
|
||||||
var di = new DirectoryInfo(ReleaseDirectory);
|
|
||||||
if (!di.Exists) di.Create();
|
|
||||||
return di;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual Option<T> AddOption<T>(Action<T> setValue, params string[] aliases)
|
|
||||||
{
|
|
||||||
return AddOption(setValue, new Option<T>(aliases));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual Option<T> AddOption<T>(Action<T> setValue, Option<T> opt)
|
|
||||||
{
|
|
||||||
_setters[opt] = (ctx) => setValue(ctx.GetValueForOption(opt));
|
|
||||||
Add(opt);
|
|
||||||
return opt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void SetProperties(ParseResult context)
|
|
||||||
{
|
|
||||||
foreach (var kvp in _setters) {
|
|
||||||
if (context.Errors.Any(e => e.SymbolResult?.Symbol?.Equals(kvp.Key) == true)) {
|
|
||||||
continue; // skip setting values for options with errors
|
|
||||||
}
|
|
||||||
kvp.Value(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual ParseResult ParseAndApply(string command)
|
|
||||||
{
|
|
||||||
var x = this.Parse(command);
|
|
||||||
SetProperties(x);
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface INugetPackCommand
|
|
||||||
{
|
|
||||||
string PackId { get; }
|
|
||||||
string PackVersion { get; }
|
|
||||||
string PackDirectory { get; }
|
|
||||||
string PackAuthors { get; }
|
|
||||||
string PackTitle { get; }
|
|
||||||
bool IncludePdb { get; }
|
|
||||||
string ReleaseNotes { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Squirrel.CommandLine.Commands
|
|
||||||
{
|
|
||||||
public class GitHubBaseCommand : BaseCommand
|
|
||||||
{
|
|
||||||
public string RepoUrl { get; private set; }
|
|
||||||
|
|
||||||
public string Token { get; private set; }
|
|
||||||
|
|
||||||
protected GitHubBaseCommand(string name, string description)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
AddOption<Uri>((v) => RepoUrl = v.ToAbsoluteOrNull(), "--repoUrl")
|
|
||||||
.SetDescription("Full url to the github repository (eg. 'https://github.com/myname/myrepo').")
|
|
||||||
.SetRequired()
|
|
||||||
.MustBeValidHttpUri();
|
|
||||||
|
|
||||||
AddOption<string>((v) => Token = v, "--token")
|
|
||||||
.SetDescription("OAuth token to use as login credentials.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GitHubDownloadCommand : GitHubBaseCommand
|
|
||||||
{
|
|
||||||
public bool Pre { get; private set; }
|
|
||||||
|
|
||||||
public GitHubDownloadCommand()
|
|
||||||
: base("github", "Download latest release from GitHub repository.")
|
|
||||||
{
|
|
||||||
AddOption<bool>((v) => Pre = v, "--pre")
|
|
||||||
.SetDescription("Get latest pre-release instead of stable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GitHubUploadCommand : GitHubBaseCommand
|
|
||||||
{
|
|
||||||
public bool Publish { get; private set; }
|
|
||||||
|
|
||||||
public string ReleaseName { get; private set; }
|
|
||||||
|
|
||||||
public GitHubUploadCommand()
|
|
||||||
: base("github", "Upload releases to a GitHub repository.")
|
|
||||||
{
|
|
||||||
AddOption<bool>((v) => Publish = v, "--publish")
|
|
||||||
.SetDescription("Publish release instead of creating draft.");
|
|
||||||
|
|
||||||
AddOption<string>((v) => ReleaseName = v, "--releaseName")
|
|
||||||
.SetDescription("A custom name for created release.")
|
|
||||||
.SetArgumentHelpName("NAME");
|
|
||||||
|
|
||||||
ReleaseDirectoryOption.SetRequired();
|
|
||||||
ReleaseDirectoryOption.MustNotBeEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Squirrel.CommandLine.Commands
|
|
||||||
{
|
|
||||||
public class HttpDownloadCommand : BaseCommand
|
|
||||||
{
|
|
||||||
public string Url { get; private set; }
|
|
||||||
|
|
||||||
public HttpDownloadCommand()
|
|
||||||
: base("http", "Download latest release from a HTTP source.")
|
|
||||||
{
|
|
||||||
AddOption<Uri>((v) => Url = v.ToAbsoluteOrNull(), "--url")
|
|
||||||
.SetDescription("Url to download remote releases from.")
|
|
||||||
.MustBeValidHttpUri()
|
|
||||||
.SetRequired();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
using System.CommandLine;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Squirrel.CommandLine.Commands
|
|
||||||
{
|
|
||||||
public class BundleOsxCommand : BaseCommand
|
|
||||||
{
|
|
||||||
public string PackId { get; private set; }
|
|
||||||
|
|
||||||
public string PackVersion { get; private set; }
|
|
||||||
|
|
||||||
public string PackDirectory { get; private set; }
|
|
||||||
|
|
||||||
public string PackAuthors { get; private set; }
|
|
||||||
|
|
||||||
public string PackTitle { get; private set; }
|
|
||||||
|
|
||||||
public string EntryExecutableName { get; private set; }
|
|
||||||
|
|
||||||
public string Icon { get; private set; }
|
|
||||||
|
|
||||||
public string BundleId { get; private set; }
|
|
||||||
|
|
||||||
public BundleOsxCommand()
|
|
||||||
: base("bundle", "Create's an OSX .app bundle from a folder containing application files.")
|
|
||||||
{
|
|
||||||
AddOption<string>((v) => PackId = v, "--packId", "-u")
|
|
||||||
.SetDescription("Unique Squirrel Id for application bundle.")
|
|
||||||
.SetArgumentHelpName("ID")
|
|
||||||
.SetRequired()
|
|
||||||
.RequiresValidNuGetId();
|
|
||||||
|
|
||||||
// TODO add parser straight to SemanticVersion?
|
|
||||||
AddOption<string>((v) => PackVersion = v, "--packVersion", "-v")
|
|
||||||
.SetDescription("Current version for application bundle.")
|
|
||||||
.SetArgumentHelpName("VERSION")
|
|
||||||
.SetRequired()
|
|
||||||
.RequiresSemverCompliant();
|
|
||||||
|
|
||||||
AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
|
|
||||||
.SetDescription("Directory containing application files for release.")
|
|
||||||
.SetArgumentHelpName("DIR")
|
|
||||||
.SetRequired()
|
|
||||||
.MustNotBeEmpty();
|
|
||||||
|
|
||||||
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
|
|
||||||
.SetDescription("Company name or comma-delimited list of authors.")
|
|
||||||
.SetArgumentHelpName("AUTHORS");
|
|
||||||
|
|
||||||
AddOption<string>((v) => PackTitle = v, "--packTitle")
|
|
||||||
.SetDescription("Display/friendly name for application.")
|
|
||||||
.SetArgumentHelpName("NAME");
|
|
||||||
|
|
||||||
AddOption<string>((v) => EntryExecutableName = v, "-e", "--mainExe")
|
|
||||||
.SetDescription("The file name of the main/entry executable.")
|
|
||||||
.SetArgumentHelpName("NAME")
|
|
||||||
.SetRequired();
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
|
|
||||||
.SetDescription("Path to the .icns file for this bundle.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly()
|
|
||||||
.SetRequired()
|
|
||||||
.RequiresExtension(".icns");
|
|
||||||
|
|
||||||
AddOption<string>((v) => BundleId = v, "--bundleId")
|
|
||||||
.SetDescription("Optional Apple bundle Id.")
|
|
||||||
.SetArgumentHelpName("ID");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ReleasifyOsxCommand : BaseCommand
|
|
||||||
{
|
|
||||||
public string BundleDirectory { get; private set; }
|
|
||||||
|
|
||||||
public bool IncludePdb { get; private set; }
|
|
||||||
|
|
||||||
public string ReleaseNotes { get; private set; }
|
|
||||||
|
|
||||||
public bool NoDelta { get; private set; }
|
|
||||||
|
|
||||||
public bool NoPackage { get; private set; }
|
|
||||||
|
|
||||||
public string PackageWelcome { get; private set; }
|
|
||||||
|
|
||||||
public string PackageReadme { get; private set; }
|
|
||||||
|
|
||||||
public string PackageLicense { get; private set; }
|
|
||||||
|
|
||||||
public string PackageConclusion { get; private set; }
|
|
||||||
|
|
||||||
public string SigningAppIdentity { get; private set; }
|
|
||||||
|
|
||||||
public string SigningInstallIdentity { get; private set; }
|
|
||||||
|
|
||||||
public string SigningEntitlements { get; private set; }
|
|
||||||
|
|
||||||
public string NotaryProfile { get; private set; }
|
|
||||||
|
|
||||||
public ReleasifyOsxCommand()
|
|
||||||
: base("releasify", "Converts an application bundle into a Squirrel release and installer.")
|
|
||||||
{
|
|
||||||
AddOption<DirectoryInfo>((v) => BundleDirectory = v.ToFullNameOrNull(), "-b", "--bundle")
|
|
||||||
.SetDescription("The bundle to convert into a Squirrel release.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.MustNotBeEmpty()
|
|
||||||
.RequiresExtension(".app")
|
|
||||||
.SetRequired();
|
|
||||||
|
|
||||||
AddOption<bool>((v) => IncludePdb = v, "--includePdb")
|
|
||||||
.SetDescription("Add *.pdb files to release package.");
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
|
|
||||||
.SetDescription("File with markdown-formatted notes for this version.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly();
|
|
||||||
|
|
||||||
AddOption<bool>((v) => NoDelta = v, "--noDelta")
|
|
||||||
.SetDescription("Skip the generation of delta packages.");
|
|
||||||
|
|
||||||
if (SquirrelRuntimeInfo.IsOSX) {
|
|
||||||
AddOption<bool>((v) => NoPackage = v, "--noPkg")
|
|
||||||
.SetDescription("Skip generating a .pkg installer.");
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => PackageWelcome = v.ToFullNameOrNull(), "--pkgWelcome")
|
|
||||||
.SetDescription("Set the installer package welcome content.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly();
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => PackageReadme = v.ToFullNameOrNull(), "--pkgReadme")
|
|
||||||
.SetDescription("Set the installer package readme content.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly();
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => PackageLicense = v.ToFullNameOrNull(), "--pkgLicense")
|
|
||||||
.SetDescription("Set the installer package license content.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly();
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => PackageConclusion = v.ToFullNameOrNull(), "--pkgConclusion")
|
|
||||||
.SetDescription("Set the installer package conclusion content.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly();
|
|
||||||
|
|
||||||
AddOption<string>((v) => SigningAppIdentity = v, "--signAppIdentity")
|
|
||||||
.SetDescription("The subject name of the cert to use for app code signing.")
|
|
||||||
.SetArgumentHelpName("SUBJECT");
|
|
||||||
|
|
||||||
AddOption<string>((v) => SigningInstallIdentity = v, "--signInstallIdentity")
|
|
||||||
.SetDescription("The subject name of the cert to use for installation packages.")
|
|
||||||
.SetArgumentHelpName("SUBJECT");
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => SigningEntitlements = v.ToFullNameOrNull(), "--signEntitlements")
|
|
||||||
.SetDescription("Path to entitlements file for hardened runtime signing.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly()
|
|
||||||
.RequiresExtension(".entitlements");
|
|
||||||
|
|
||||||
AddOption<string>((v) => NotaryProfile = v, "--notaryProfile")
|
|
||||||
.SetDescription("Name of profile containing Apple credentials stored with notarytool.")
|
|
||||||
.SetArgumentHelpName("NAME");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.CommandLine.Parsing;
|
|
||||||
|
|
||||||
namespace Squirrel.CommandLine.Commands
|
|
||||||
{
|
|
||||||
public class S3BaseCommand : BaseCommand
|
|
||||||
{
|
|
||||||
public string KeyId { get; private set; }
|
|
||||||
|
|
||||||
public string Secret { get; private set; }
|
|
||||||
|
|
||||||
public string Region { get; private set; }
|
|
||||||
|
|
||||||
public string Endpoint { get; private set; }
|
|
||||||
|
|
||||||
public string Bucket { get; private set; }
|
|
||||||
|
|
||||||
public string PathPrefix { get; private set; }
|
|
||||||
|
|
||||||
protected S3BaseCommand(string name, string description)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
AddOption<string>((v) => KeyId = v, "--keyId")
|
|
||||||
.SetDescription("Authentication identifier or access key.")
|
|
||||||
.SetArgumentHelpName("KEYID")
|
|
||||||
.SetRequired();
|
|
||||||
|
|
||||||
AddOption<string>((v) => Secret = v, "--secret")
|
|
||||||
.SetDescription("Authentication secret key.")
|
|
||||||
.SetArgumentHelpName("KEY")
|
|
||||||
.SetRequired();
|
|
||||||
|
|
||||||
var region = AddOption<string>((v) => Region = v, "--region")
|
|
||||||
.SetDescription("AWS service region (eg. us-west-1).")
|
|
||||||
.SetArgumentHelpName("REGION");
|
|
||||||
|
|
||||||
region.AddValidator(MustBeValidAwsRegion);
|
|
||||||
|
|
||||||
var endpoint = AddOption<Uri>((v) => Endpoint = v.ToAbsoluteOrNull(), "--endpoint")
|
|
||||||
.SetDescription("Custom service url (backblaze, digital ocean, etc).")
|
|
||||||
.SetArgumentHelpName("URL")
|
|
||||||
.MustBeValidHttpUri();
|
|
||||||
|
|
||||||
this.AreMutuallyExclusive(region, endpoint);
|
|
||||||
this.AtLeastOneRequired(region, endpoint);
|
|
||||||
|
|
||||||
AddOption<string>((v) => Bucket = v, "--bucket")
|
|
||||||
.SetDescription("Name of the S3 bucket.")
|
|
||||||
.SetArgumentHelpName("NAME")
|
|
||||||
.SetRequired();
|
|
||||||
|
|
||||||
AddOption<string>((v) => PathPrefix = v, "--pathPrefix")
|
|
||||||
.SetDescription("A sub-folder used for files in the bucket, for creating release channels (eg. 'stable' or 'dev').")
|
|
||||||
.SetArgumentHelpName("PREFIX");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MustBeValidAwsRegion(OptionResult result)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
var region = result.Tokens[i].Value;
|
|
||||||
if (!string.IsNullOrWhiteSpace(region)) {
|
|
||||||
var r = Amazon.RegionEndpoint.GetBySystemName(result.Tokens[0].Value);
|
|
||||||
if (r is null || r.DisplayName == "Unknown") {
|
|
||||||
result.ErrorMessage = $"Region '{region}' lookup failed, is this a valid AWS region?";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.ErrorMessage = "A region value is required";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class S3DownloadCommand : S3BaseCommand
|
|
||||||
{
|
|
||||||
public S3DownloadCommand()
|
|
||||||
: base("s3", "Download latest release from an S3 bucket.")
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class S3UploadCommand : S3BaseCommand
|
|
||||||
{
|
|
||||||
public bool Overwrite { get; private set; }
|
|
||||||
|
|
||||||
public int KeepMaxReleases { get; private set; }
|
|
||||||
|
|
||||||
public S3UploadCommand()
|
|
||||||
: base("s3", "Upload releases to a S3 bucket.")
|
|
||||||
{
|
|
||||||
AddOption<bool>((v) => Overwrite = v, "--overwrite")
|
|
||||||
.SetDescription("Replace remote files if local files have changed.");
|
|
||||||
|
|
||||||
AddOption<int>((v) => KeepMaxReleases = v, "--keepMaxReleases")
|
|
||||||
.SetDescription("Apply a retention policy which keeps only the specified number of old versions in remote source.")
|
|
||||||
.SetArgumentHelpName("NUMBER");
|
|
||||||
|
|
||||||
ReleaseDirectoryOption.SetRequired();
|
|
||||||
ReleaseDirectoryOption.MustNotBeEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.CommandLine;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Squirrel.CommandLine.Commands
|
|
||||||
{
|
|
||||||
public class SigningCommand : BaseCommand
|
|
||||||
{
|
|
||||||
public string SignParameters { get; private set; }
|
|
||||||
|
|
||||||
public bool SignSkipDll { get; private set; }
|
|
||||||
|
|
||||||
public int SignParallel { get; private set; }
|
|
||||||
|
|
||||||
public string SignTemplate { get; private set; }
|
|
||||||
|
|
||||||
protected SigningCommand(string name, string description)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
var signTemplate = AddOption<string>((v) => SignTemplate = v, "--signTemplate")
|
|
||||||
.SetDescription("Use a custom signing command. {{file}} will be replaced by the path to sign.")
|
|
||||||
.SetArgumentHelpName("COMMAND")
|
|
||||||
.MustContain("{{file}}");
|
|
||||||
|
|
||||||
AddOption<bool>((v) => SignSkipDll = v, "--signSkipDll")
|
|
||||||
.SetDescription("Only signs EXE files, and skips signing DLL files.");
|
|
||||||
|
|
||||||
if (SquirrelRuntimeInfo.IsWindows) {
|
|
||||||
var signParams = AddOption<string>((v) => SignParameters = v, "--signParams", "-n")
|
|
||||||
.SetDescription("Sign files via signtool.exe using these parameters.")
|
|
||||||
.SetArgumentHelpName("PARAMS");
|
|
||||||
|
|
||||||
this.AreMutuallyExclusive(signTemplate, signParams);
|
|
||||||
|
|
||||||
AddOption<int>((v) => SignParallel = v, "--signParallel")
|
|
||||||
.SetDescription("The number of files to sign in each call to signtool.exe.")
|
|
||||||
.SetArgumentHelpName("NUM")
|
|
||||||
.MustBeBetween(1, 1000)
|
|
||||||
.SetDefaultValue(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ReleasifyWindowsCommand : SigningCommand
|
|
||||||
{
|
|
||||||
public string Package { get; set; }
|
|
||||||
|
|
||||||
public string BaseUrl { get; private set; }
|
|
||||||
|
|
||||||
public string DebugSetupExe { get; private set; }
|
|
||||||
|
|
||||||
public bool NoDelta { get; private set; }
|
|
||||||
|
|
||||||
public string Runtimes { get; private set; }
|
|
||||||
|
|
||||||
public string SplashImage { get; private set; }
|
|
||||||
|
|
||||||
public string Icon { get; private set; }
|
|
||||||
|
|
||||||
public string[] SquirrelAwareExecutableNames { get; private set; }
|
|
||||||
|
|
||||||
public string AppIcon { get; private set; }
|
|
||||||
|
|
||||||
public bool BuildMsi { get; private set; }
|
|
||||||
|
|
||||||
public string MsiVersion { get; private set; }
|
|
||||||
|
|
||||||
public ReleasifyWindowsCommand()
|
|
||||||
: this("releasify", "Take an existing nuget package and convert it into a Squirrel release.")
|
|
||||||
{
|
|
||||||
AddOption<FileInfo>((v) => Package = v.ToFullNameOrNull(), "-p", "--package")
|
|
||||||
.SetDescription("Path to a '.nupkg' package to releasify.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.SetRequired()
|
|
||||||
.ExistingOnly()
|
|
||||||
.RequiresExtension(".nupkg");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This constructor is used by the pack command, which requires all the same properties but
|
|
||||||
/// does not allow the user to provide the Package (it is created/populated by Squirrel).
|
|
||||||
/// </summary>
|
|
||||||
protected ReleasifyWindowsCommand(string name, string description)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
AddOption<Uri>((v) => BaseUrl = v.ToAbsoluteOrNull(), "-b", "--baseUrl")
|
|
||||||
.SetDescription("Provides a base URL to prefix the RELEASES file packages with.")
|
|
||||||
.SetHidden()
|
|
||||||
.MustBeValidHttpUri();
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => DebugSetupExe = v.ToFullNameOrNull(), "--debugSetupExe")
|
|
||||||
.SetDescription("Uses the Setup.exe at this {PATH} to create the bundle, and then replaces it with the bundle. " +
|
|
||||||
"Used for locally debugging Setup.exe with a real bundle attached.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.SetHidden()
|
|
||||||
.ExistingOnly()
|
|
||||||
.RequiresExtension(".exe");
|
|
||||||
|
|
||||||
AddOption<bool>((v) => NoDelta = v, "--noDelta")
|
|
||||||
.SetDescription("Skip the generation of delta packages.");
|
|
||||||
|
|
||||||
AddOption<string>((v) => Runtimes = v, "-f", "--framework")
|
|
||||||
.SetDescription("List of required runtimes to install during setup. example: 'net6,vcredist143'.")
|
|
||||||
.SetArgumentHelpName("RUNTIMES")
|
|
||||||
.MustBeValidFrameworkString();
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => SplashImage = v.ToFullNameOrNull(), "-s", "--splashImage")
|
|
||||||
.SetDescription("Path to image displayed during installation.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly();
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
|
|
||||||
.SetDescription("Path to .ico for Setup.exe and Update.exe.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly()
|
|
||||||
.RequiresExtension(".ico");
|
|
||||||
|
|
||||||
AddOption<string[]>((v) => SquirrelAwareExecutableNames = v ?? new string[0], "-e", "--mainExe")
|
|
||||||
.SetDescription("Name of one or more SquirrelAware executables.")
|
|
||||||
.SetArgumentHelpName("NAME");
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => AppIcon = v.ToFullNameOrNull(), "--appIcon")
|
|
||||||
.SetDescription("Path to .ico for 'Apps and Features' list.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly()
|
|
||||||
.RequiresExtension(".ico");
|
|
||||||
|
|
||||||
if (SquirrelRuntimeInfo.IsWindows) {
|
|
||||||
AddOption<bool>((v) => BuildMsi = v, "--msi")
|
|
||||||
.SetDescription("Compile a .msi machine-wide deployment tool.")
|
|
||||||
.SetArgumentHelpName("BITNESS");
|
|
||||||
|
|
||||||
AddOption<string>((v) => MsiVersion = v, "--msiVersion")
|
|
||||||
.SetDescription("Override the product version for the generated msi.")
|
|
||||||
.SetArgumentHelpName("VERSION")
|
|
||||||
.MustBeValidMsiVersion();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PackWindowsCommand : ReleasifyWindowsCommand, INugetPackCommand
|
|
||||||
{
|
|
||||||
public string PackId { get; private set; }
|
|
||||||
|
|
||||||
public string PackVersion { get; private set; }
|
|
||||||
|
|
||||||
public string PackDirectory { get; private set; }
|
|
||||||
|
|
||||||
public string PackAuthors { get; private set; }
|
|
||||||
|
|
||||||
public string PackTitle { get; private set; }
|
|
||||||
|
|
||||||
public bool IncludePdb { get; private set; }
|
|
||||||
|
|
||||||
public string ReleaseNotes { get; private set; }
|
|
||||||
|
|
||||||
public PackWindowsCommand()
|
|
||||||
: base("pack", "Creates a Squirrel release from a folder containing application files.")
|
|
||||||
{
|
|
||||||
AddOption<string>((v) => PackId = v, "--packId", "-u")
|
|
||||||
.SetDescription("Unique Id for application bundle.")
|
|
||||||
.SetArgumentHelpName("ID")
|
|
||||||
.SetRequired()
|
|
||||||
.RequiresValidNuGetId();
|
|
||||||
|
|
||||||
// TODO add parser straight to SemanticVersion
|
|
||||||
AddOption<string>((v) => PackVersion = v, "--packVersion", "-v")
|
|
||||||
.SetDescription("Current version for application bundle.")
|
|
||||||
.SetArgumentHelpName("VERSION")
|
|
||||||
.SetRequired()
|
|
||||||
.RequiresSemverCompliant();
|
|
||||||
|
|
||||||
AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
|
|
||||||
.SetDescription("Directory containing application files for release.")
|
|
||||||
.SetArgumentHelpName("DIR")
|
|
||||||
.SetRequired()
|
|
||||||
.MustNotBeEmpty();
|
|
||||||
|
|
||||||
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
|
|
||||||
.SetDescription("Company name or comma-delimited list of authors.")
|
|
||||||
.SetArgumentHelpName("AUTHORS");
|
|
||||||
|
|
||||||
AddOption<string>((v) => PackTitle = v, "--packTitle")
|
|
||||||
.SetDescription("Display/friendly name for application.")
|
|
||||||
.SetArgumentHelpName("NAME");
|
|
||||||
|
|
||||||
AddOption<bool>((v) => IncludePdb = v, "--includePdb")
|
|
||||||
.SetDescription("Add *.pdb files to release package");
|
|
||||||
|
|
||||||
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
|
|
||||||
.SetDescription("File with markdown-formatted notes for this version.")
|
|
||||||
.SetArgumentHelpName("PATH")
|
|
||||||
.ExistingOnly();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
using NugetLevel = NuGet.Common.LogLevel;
|
|
||||||
using NugetLogger = NuGet.Common.ILogger;
|
|
||||||
using NugetMessage = NuGet.Common.ILogMessage;
|
|
||||||
|
|
||||||
namespace Squirrel.CommandLine
|
|
||||||
{
|
|
||||||
class ConsoleLogger : ILogger, NugetLogger
|
|
||||||
{
|
|
||||||
public LogLevel Level { get; set; } = LogLevel.Info;
|
|
||||||
|
|
||||||
private readonly object gate = new object();
|
|
||||||
|
|
||||||
private readonly string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData).TrimEnd('/', '\\');
|
|
||||||
private readonly string localTemp = Path.GetTempPath().TrimEnd('/', '\\');
|
|
||||||
|
|
||||||
public void Write(string message, LogLevel logLevel)
|
|
||||||
{
|
|
||||||
if (logLevel < Level) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SquirrelRuntimeInfo.IsWindows) {
|
|
||||||
message = message.Replace(localTemp, "%temp%", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
message = message.Replace(localAppData, "%localappdata%", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (gate) {
|
|
||||||
string lvl = logLevel.ToString().Substring(0, 4).ToUpper();
|
|
||||||
if (logLevel == LogLevel.Error || logLevel == LogLevel.Fatal) {
|
|
||||||
Utility.ConsoleWriteWithColor($"[{lvl}] {message}{Environment.NewLine}", ConsoleColor.Red);
|
|
||||||
} else if (logLevel == LogLevel.Warn) {
|
|
||||||
Utility.ConsoleWriteWithColor($"[{lvl}] {message}{Environment.NewLine}", ConsoleColor.Yellow);
|
|
||||||
} else {
|
|
||||||
Console.WriteLine($"[{lvl}] {message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ConsoleLogger RegisterLogger()
|
|
||||||
{
|
|
||||||
var logger = new ConsoleLogger();
|
|
||||||
SquirrelLocator.CurrentMutable.Register(() => logger, typeof(ILogger));
|
|
||||||
SquirrelLocator.CurrentMutable.Register(() => logger, typeof(NugetLogger));
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region NuGet.Common.ILogger
|
|
||||||
|
|
||||||
void NugetLogger.LogDebug(string data)
|
|
||||||
{
|
|
||||||
Write(data, LogLevel.Debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
void NugetLogger.LogVerbose(string data)
|
|
||||||
{
|
|
||||||
Write(data, LogLevel.Debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
void NugetLogger.LogInformation(string data)
|
|
||||||
{
|
|
||||||
Write(data, LogLevel.Info);
|
|
||||||
}
|
|
||||||
|
|
||||||
void NugetLogger.LogMinimal(string data)
|
|
||||||
{
|
|
||||||
Write(data, LogLevel.Info);
|
|
||||||
}
|
|
||||||
|
|
||||||
void NugetLogger.LogWarning(string data)
|
|
||||||
{
|
|
||||||
Write(data, LogLevel.Warn);
|
|
||||||
}
|
|
||||||
|
|
||||||
void NugetLogger.LogError(string data)
|
|
||||||
{
|
|
||||||
Write(data, LogLevel.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
void NugetLogger.LogInformationSummary(string data)
|
|
||||||
{
|
|
||||||
Write(data, LogLevel.Info);
|
|
||||||
}
|
|
||||||
|
|
||||||
LogLevel NugetToLogLevel(NugetLevel level)
|
|
||||||
{
|
|
||||||
return level switch {
|
|
||||||
NugetLevel.Debug => LogLevel.Debug,
|
|
||||||
NugetLevel.Verbose => LogLevel.Debug,
|
|
||||||
NugetLevel.Information => LogLevel.Info,
|
|
||||||
NugetLevel.Minimal => LogLevel.Info,
|
|
||||||
NugetLevel.Warning => LogLevel.Warn,
|
|
||||||
NugetLevel.Error => LogLevel.Error,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void NugetLogger.Log(NugetLevel level, string data)
|
|
||||||
{
|
|
||||||
Write(data, NugetToLogLevel(level));
|
|
||||||
}
|
|
||||||
|
|
||||||
Task NugetLogger.LogAsync(NugetLevel level, string data)
|
|
||||||
{
|
|
||||||
Write(data, NugetToLogLevel(level));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
void NugetLogger.Log(NugetMessage message)
|
|
||||||
{
|
|
||||||
Write(message.Message, NugetToLogLevel(message.Level));
|
|
||||||
}
|
|
||||||
|
|
||||||
Task NugetLogger.LogAsync(NugetMessage message)
|
|
||||||
{
|
|
||||||
Write(message.Message, NugetToLogLevel(message.Level));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
[assembly: ComVisible(false)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel.Tests, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("SquirrelMac, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine.OSX, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine.Windows, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Update, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("UpdateMac, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Update.Windows, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Update.OSX, PublicKey=" + SNK.SHA1)]
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Octokit;
|
|
||||||
using Squirrel.CommandLine.Commands;
|
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
using Squirrel.Sources;
|
|
||||||
|
|
||||||
namespace Squirrel.CommandLine.Sync
|
|
||||||
{
|
|
||||||
internal static class GitHubRepository
|
|
||||||
{
|
|
||||||
internal readonly static IFullLogger Log = SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(GitHubRepository));
|
|
||||||
|
|
||||||
public static async Task DownloadRecentPackages(GitHubDownloadCommand options)
|
|
||||||
{
|
|
||||||
var releaseDirectoryInfo = options.GetReleaseDirectory();
|
|
||||||
|
|
||||||
if (String.IsNullOrWhiteSpace(options.Token))
|
|
||||||
Log.Warn("No GitHub access token provided. Unauthenticated requests will be limited to 60 per hour.");
|
|
||||||
|
|
||||||
Log.Info("Fetching RELEASES...");
|
|
||||||
var source = new GithubSource(options.RepoUrl, options.Token, options.Pre);
|
|
||||||
var latestReleaseEntries = await source.GetReleaseFeed();
|
|
||||||
|
|
||||||
if (latestReleaseEntries == null || latestReleaseEntries.Length == 0) {
|
|
||||||
Log.Warn("No github release or assets found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Info($"Found {latestReleaseEntries.Length} assets in RELEASES file for GitHub version {source.Release.Name}.");
|
|
||||||
|
|
||||||
var releasesToDownload = latestReleaseEntries
|
|
||||||
.Where(x => !x.IsDelta)
|
|
||||||
.OrderByDescending(x => x.Version)
|
|
||||||
.Take(1)
|
|
||||||
.Select(x => new {
|
|
||||||
Obj = x,
|
|
||||||
LocalPath = Path.Combine(releaseDirectoryInfo.FullName, x.Filename),
|
|
||||||
Filename = x.Filename,
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var entry in releasesToDownload) {
|
|
||||||
if (File.Exists(entry.LocalPath)) {
|
|
||||||
Log.Warn($"File '{entry.Filename}' exists on disk, skipping download.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Info($"Downloading {entry.Filename}...");
|
|
||||||
await source.DownloadReleaseEntry(entry.Obj, entry.LocalPath, (p) => { });
|
|
||||||
}
|
|
||||||
|
|
||||||
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
|
||||||
Log.Info("Done.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task UploadMissingPackages(GitHubUploadCommand options)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrWhiteSpace(options.Token))
|
|
||||||
throw new InvalidOperationException("Must provide access token to create a GitHub release.");
|
|
||||||
|
|
||||||
var releaseDirectoryInfo = options.GetReleaseDirectory();
|
|
||||||
|
|
||||||
var repoUri = new Uri(options.RepoUrl);
|
|
||||||
var repoParts = repoUri.AbsolutePath.Trim('/').Split('/');
|
|
||||||
if (repoParts.Length != 2)
|
|
||||||
throw new Exception($"Invalid GitHub URL, '{repoUri.AbsolutePath}' should be in the format 'owner/repo'");
|
|
||||||
|
|
||||||
var repoOwner = repoParts[0];
|
|
||||||
var repoName = repoParts[1];
|
|
||||||
|
|
||||||
var client = new GitHubClient(new ProductHeaderValue("Clowd.Squirrel")) {
|
|
||||||
Credentials = new Credentials(options.Token)
|
|
||||||
};
|
|
||||||
|
|
||||||
var releasesPath = Path.Combine(releaseDirectoryInfo.FullName, "RELEASES");
|
|
||||||
if (!File.Exists(releasesPath))
|
|
||||||
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
|
||||||
|
|
||||||
var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath)).ToArray();
|
|
||||||
if (releases.Length == 0)
|
|
||||||
throw new Exception("There are no nupkg's in the releases directory to upload");
|
|
||||||
|
|
||||||
var ver = Enumerable.MaxBy(releases, x => x.Version);
|
|
||||||
if (ver == null)
|
|
||||||
throw new Exception("There are no nupkg's in the releases directory to upload");
|
|
||||||
var semVer = ver.Version;
|
|
||||||
|
|
||||||
Log.Info($"Preparing to upload latest local release to GitHub");
|
|
||||||
|
|
||||||
var newReleaseReq = new NewRelease(semVer.ToString()) {
|
|
||||||
Body = ver.GetReleaseNotes(releaseDirectoryInfo.FullName, ReleaseNotesFormat.Markdown),
|
|
||||||
Draft = true,
|
|
||||||
Prerelease = semVer.HasMetadata || semVer.IsPrerelease,
|
|
||||||
Name = string.IsNullOrWhiteSpace(options.ReleaseName)
|
|
||||||
? semVer.ToString()
|
|
||||||
: options.ReleaseName,
|
|
||||||
};
|
|
||||||
|
|
||||||
Log.Info($"Creating draft release titled '{semVer.ToString()}'");
|
|
||||||
|
|
||||||
var existingReleases = await client.Repository.Release.GetAll(repoOwner, repoName);
|
|
||||||
if (existingReleases.Any(r => r.TagName == semVer.ToString())) {
|
|
||||||
throw new Exception($"There is already an existing release tagged '{semVer}'. Please delete this release or choose a new version number.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var release = await client.Repository.Release.Create(repoOwner, repoName, newReleaseReq);
|
|
||||||
|
|
||||||
// locate files to upload
|
|
||||||
var files = releaseDirectoryInfo.GetFiles("*", SearchOption.TopDirectoryOnly);
|
|
||||||
var msiFile = files.SingleOrDefault(f => f.FullName.EndsWith(".msi", StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
var setupFile = files.Where(f => f.FullName.EndsWith("Setup.exe", StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
.ContextualSingle("release directory", "Setup.exe file");
|
|
||||||
|
|
||||||
var releasesToUpload = releases.Where(x => x.Version == semVer).ToArray();
|
|
||||||
MemoryStream releasesFileToUpload = new MemoryStream();
|
|
||||||
ReleaseEntry.WriteReleaseFile(releasesToUpload, releasesFileToUpload);
|
|
||||||
var releasesBytes = releasesFileToUpload.ToArray();
|
|
||||||
|
|
||||||
// upload nupkg's
|
|
||||||
foreach (var r in releasesToUpload) {
|
|
||||||
var path = Path.Combine(releaseDirectoryInfo.FullName, r.Filename);
|
|
||||||
await UploadFileAsAsset(client, release, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// other files
|
|
||||||
await UploadFileAsAsset(client, release, setupFile.FullName);
|
|
||||||
if (msiFile != null) await UploadFileAsAsset(client, release, msiFile.FullName);
|
|
||||||
|
|
||||||
// RELEASES
|
|
||||||
Log.Info($"Uploading RELEASES");
|
|
||||||
var data = new ReleaseAssetUpload("RELEASES", "application/octet-stream", new MemoryStream(releasesBytes), TimeSpan.FromMinutes(1));
|
|
||||||
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
|
||||||
|
|
||||||
Log.Info($"Done creating draft GitHub release.");
|
|
||||||
|
|
||||||
// convert draft to full release
|
|
||||||
if (options.Publish) {
|
|
||||||
Log.Info("Converting draft to full published release.");
|
|
||||||
var upd = release.ToUpdate();
|
|
||||||
upd.Draft = false;
|
|
||||||
release = await client.Repository.Release.Edit(repoOwner, repoName, release.Id, upd);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Info("Release URL: " + release.HtmlUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task UploadFileAsAsset(GitHubClient client, Release release, string filePath)
|
|
||||||
{
|
|
||||||
Log.Info($"Uploading asset '{Path.GetFileName(filePath)}'");
|
|
||||||
using var stream = File.OpenRead(filePath);
|
|
||||||
var data = new ReleaseAssetUpload(Path.GetFileName(filePath), "application/octet-stream", stream, TimeSpan.FromMinutes(30));
|
|
||||||
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.CommandLine;
|
|
||||||
using System.CommandLine.Parsing;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using NuGet.Versioning;
|
|
||||||
using Squirrel.NuGet;
|
|
||||||
|
|
||||||
namespace Squirrel.CommandLine
|
|
||||||
{
|
|
||||||
internal static class SystemCommandLineExtensions
|
|
||||||
{
|
|
||||||
public static string ToFullNameOrNull(this FileSystemInfo fsi)
|
|
||||||
{
|
|
||||||
return fsi?.FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ToAbsoluteOrNull(this Uri uri)
|
|
||||||
{
|
|
||||||
if (uri?.IsAbsoluteUri == true) return uri.AbsoluteUri;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<T> SetDescription<T>(this Option<T> option, string description)
|
|
||||||
{
|
|
||||||
option.Description = description;
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<T> SetHidden<T>(this Option<T> option, bool isHidden = true)
|
|
||||||
{
|
|
||||||
option.IsHidden = isHidden;
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<T> SetRequired<T>(this Option<T> option, bool isRequired = true)
|
|
||||||
{
|
|
||||||
option.IsRequired = isRequired;
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<T> SetDefault<T>(this Option<T> option, T defaultValue)
|
|
||||||
{
|
|
||||||
option.SetDefaultValue(defaultValue);
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<T> SetArgumentHelpName<T>(this Option<T> option, string argumentHelpName)
|
|
||||||
{
|
|
||||||
option.ArgumentHelpName = argumentHelpName;
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<int> MustBeBetween(this Option<int> option, int minimum, int maximum)
|
|
||||||
{
|
|
||||||
option.AddValidator(x => Validate.MustBeBetween(x, minimum, maximum));
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<Uri> MustBeValidHttpUri(this Option<Uri> option)
|
|
||||||
{
|
|
||||||
option.RequiresScheme(Uri.UriSchemeHttp, Uri.UriSchemeHttps).RequiresAbsolute();
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<FileInfo> RequiresExtension(this Option<FileInfo> option, string extension)
|
|
||||||
{
|
|
||||||
option.AddValidator(x => Validate.RequiresExtension(x, extension));
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<DirectoryInfo> RequiresExtension(this Option<DirectoryInfo> option, string extension)
|
|
||||||
{
|
|
||||||
option.AddValidator(x => Validate.RequiresExtension(x, extension));
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Command AreMutuallyExclusive(this Command command, params Option[] options)
|
|
||||||
{
|
|
||||||
command.AddValidator(x => Validate.AreMutuallyExclusive(x, options));
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
//public static Command RequiredAllowObsoleteFallback(this Command command, Option option, Option obsoleteOption)
|
|
||||||
//{
|
|
||||||
// command.AddValidator(x => Validate.AtLeastOneRequired(x, new[] { option, obsoleteOption }, true));
|
|
||||||
// return command;
|
|
||||||
//}
|
|
||||||
|
|
||||||
public static Command AtLeastOneRequired(this Command command, params Option[] options)
|
|
||||||
{
|
|
||||||
command.AddValidator(x => Validate.AtLeastOneRequired(x, options, false));
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<string> MustContain(this Option<string> option, string value)
|
|
||||||
{
|
|
||||||
option.AddValidator(x => Validate.MustContain(x, value));
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<Uri> RequiresScheme(this Option<Uri> option, params string[] validSchemes)
|
|
||||||
{
|
|
||||||
option.AddValidator(x => Validate.RequiresScheme(x, validSchemes));
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<Uri> RequiresAbsolute(this Option<Uri> option, params string[] validSchemes)
|
|
||||||
{
|
|
||||||
option.AddValidator(Validate.RequiresAbsolute);
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<string> RequiresValidNuGetId(this Option<string> option)
|
|
||||||
{
|
|
||||||
option.AddValidator(Validate.RequiresValidNuGetId);
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Could setup the options to accept type SemanticVersion and apply an appropriate parser for it
|
|
||||||
public static Option<string> RequiresSemverCompliant(this Option<string> option)
|
|
||||||
{
|
|
||||||
option.AddValidator(Validate.RequiresSemverCompliant);
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<DirectoryInfo> MustNotBeEmpty(this Option<DirectoryInfo> option)
|
|
||||||
{
|
|
||||||
option.AddValidator(Validate.MustNotBeEmpty);
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<string> MustBeValidFrameworkString(this Option<string> option)
|
|
||||||
{
|
|
||||||
option.AddValidator(Validate.MustBeValidFrameworkString);
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<string> MustBeValidMsiVersion(this Option<string> option)
|
|
||||||
{
|
|
||||||
option.AddValidator(Validate.MustBeValidMsiVersion);
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Option<string> MustBeSupportedRid(this Option<string> option)
|
|
||||||
{
|
|
||||||
option.AddValidator(Validate.MustBeSupportedRid);
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Validate
|
|
||||||
{
|
|
||||||
public static void MustBeBetween(OptionResult result, int minimum, int maximum)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
if (int.TryParse(result.Tokens[i].Value, out int value)) {
|
|
||||||
if (value < minimum || value > maximum) {
|
|
||||||
result.ErrorMessage = $"The value for {result.Token.Value} must be greater than {minimum} and less than {maximum}";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.ErrorMessage = $"{result.Tokens[i].Value} is not a valid integer for {result.Token.Value}";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void RequiresExtension(OptionResult result, string extension)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
if (!string.Equals(Path.GetExtension(result.Tokens[i].Value), extension, StringComparison.InvariantCultureIgnoreCase)) {
|
|
||||||
result.ErrorMessage = $"{result.Token.Value} does not have an {extension} extension";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void AreMutuallyExclusive(CommandResult result, Option[] options)
|
|
||||||
{
|
|
||||||
var specifiedOptions = options
|
|
||||||
.Where(x => result.FindResultFor(x) is not null)
|
|
||||||
.ToList();
|
|
||||||
if (specifiedOptions.Count > 1) {
|
|
||||||
string optionsString = string.Join(" and ", specifiedOptions.Select(x => $"'{x.Aliases.First()}'"));
|
|
||||||
result.ErrorMessage = $"Cannot use {optionsString} options together, please choose one.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void AtLeastOneRequired(CommandResult result, Option[] options, bool onlyShowFirst = false)
|
|
||||||
{
|
|
||||||
var anySpecifiedOptions = options
|
|
||||||
.Any(x => result.FindResultFor(x) is not null);
|
|
||||||
if (!anySpecifiedOptions) {
|
|
||||||
if (onlyShowFirst) {
|
|
||||||
result.ErrorMessage = $"Required argument missing for option: {options.First().Aliases.First()}";
|
|
||||||
} else {
|
|
||||||
string optionsString = string.Join(" and ", options.Select(x => $"'{x.Aliases.First()}'"));
|
|
||||||
result.ErrorMessage = $"At least one of the following options are required {optionsString}.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void MustContain(OptionResult result, string value)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
if (result.Tokens[i].Value?.Contains(value) == false) {
|
|
||||||
result.ErrorMessage = $"{result.Token.Value} must contain '{value}'. Current value is '{result.Tokens[i].Value}'";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void RequiresScheme(OptionResult result, string[] validSchemes)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
if (Uri.TryCreate(result.Tokens[i].Value, UriKind.RelativeOrAbsolute, out Uri uri) &&
|
|
||||||
uri.IsAbsoluteUri &&
|
|
||||||
!validSchemes.Contains(uri.Scheme)) {
|
|
||||||
result.ErrorMessage = $"{result.Token.Value} must contain a Uri with one of the following schems: {string.Join(", ", validSchemes)}. Current value is '{result.Tokens[i].Value}'";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void RequiresAbsolute(OptionResult result)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
if (!Uri.TryCreate(result.Tokens[i].Value, UriKind.Absolute, out Uri _)) {
|
|
||||||
result.ErrorMessage = $"{result.Token.Value} must contain an absolute Uri. Current value is '{result.Tokens[i].Value}'";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void RequiresValidNuGetId(OptionResult result)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
if (!NugetUtil.IsValidNuGetId(result.Tokens[i].Value)) {
|
|
||||||
result.ErrorMessage = $"{result.Token.Value} is an invalid NuGet package id. It must contain only alphanumeric characters, underscores, dashes, and dots.. Current value is '{result.Tokens[i].Value}'";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void RequiresSemverCompliant(OptionResult result)
|
|
||||||
{
|
|
||||||
string specifiedAlias = result.Token.Value;
|
|
||||||
|
|
||||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
string version = result.Tokens[i].Value;
|
|
||||||
//TODO: This is duplicating NugetUtil.ThrowIfVersionNotSemverCompliant
|
|
||||||
if (SemanticVersion.TryParse(version, out var parsed)) {
|
|
||||||
if (parsed < new SemanticVersion(0, 0, 1)) {
|
|
||||||
result.ErrorMessage = $"{result.Token.Value} contains an invalid package version '{version}', it must be >= 0.0.1.";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.ErrorMessage = $"{result.Token.Value} contains an invalid package version '{version}', it must be a 3-part SemVer2 compliant version string.";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void MustNotBeEmpty(OptionResult result)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
var token = result.Tokens[i];
|
|
||||||
|
|
||||||
if (!Directory.Exists(token.Value) ||
|
|
||||||
!Directory.EnumerateFileSystemEntries(token.Value).Any()) {
|
|
||||||
result.ErrorMessage = $"{result.Token.Value} must be a non-empty directory, but the specified directory '{token.Value}' was empty.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void MustBeValidFrameworkString(OptionResult result)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
var framework = result.Tokens[i].Value;
|
|
||||||
try {
|
|
||||||
Runtimes.ParseDependencyString(framework);
|
|
||||||
} catch (Exception e) {
|
|
||||||
result.ErrorMessage = e.Message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void MustBeValidMsiVersion(OptionResult result)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
var version = result.Tokens[i].Value;
|
|
||||||
if (Version.TryParse(version, out var parsed)) {
|
|
||||||
if (parsed.Major > 255 || parsed.Minor > 255 || parsed.Build > 65535 || parsed.Revision > 0) {
|
|
||||||
result.ErrorMessage = $"MSI ProductVersion out of bounds '{version}'. Valid range is [0-255].[0-255].[0-65535].[0]";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.ErrorMessage = "Version string is invalid / could not be parsed.";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void MustBeSupportedRid(OptionResult result)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
|
||||||
if (!Regex.IsMatch(result.Tokens[i].Value, @"^(?<os>osx|win)\.?(?<ver>[\d\.]+)?(?:-(?<arch>(?:x|arm)\d{2}))$"))
|
|
||||||
result.ErrorMessage = $"Invalid or unsupported runtime '{result.Token.Value}'. Valid example: win-x64, osx-arm64.";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
69
src/Squirrel.Csq/Commands/BaseCommand.cs
Normal file
69
src/Squirrel.Csq/Commands/BaseCommand.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
namespace Squirrel.Csq.Commands;
|
||||||
|
|
||||||
|
public class BaseCommand : CliCommand
|
||||||
|
{
|
||||||
|
public RID TargetRuntime { get; set; }
|
||||||
|
|
||||||
|
public string ReleaseDirectory { get; private set; }
|
||||||
|
|
||||||
|
protected CliOption<DirectoryInfo> ReleaseDirectoryOption { get; private set; }
|
||||||
|
|
||||||
|
//protected static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(BaseCommand));
|
||||||
|
private Dictionary<CliOption, Action<ParseResult>> _setters = new();
|
||||||
|
|
||||||
|
protected BaseCommand(string name, string description)
|
||||||
|
: base(name, description)
|
||||||
|
{
|
||||||
|
ReleaseDirectoryOption = AddOption<DirectoryInfo>((v) => ReleaseDirectory = v.ToFullNameOrNull(), "-o", "--outputDir")
|
||||||
|
.SetDescription("Output directory for Squirrel packages.")
|
||||||
|
.SetArgumentHelpName("DIR")
|
||||||
|
.SetDefault(new DirectoryInfo(".\\Releases"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DirectoryInfo GetReleaseDirectory()
|
||||||
|
{
|
||||||
|
var di = new DirectoryInfo(ReleaseDirectory);
|
||||||
|
if (!di.Exists) di.Create();
|
||||||
|
return di;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual CliOption<T> AddOption<T>(Action<T> setValue, params string[] aliases)
|
||||||
|
{
|
||||||
|
return AddOption(setValue, new CliOption<T>(aliases));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual CliOption<T> AddOption<T>(Action<T> setValue, CliOption<T> opt)
|
||||||
|
{
|
||||||
|
_setters[opt] = (ctx) => setValue(ctx.GetValue(opt));
|
||||||
|
Add(opt);
|
||||||
|
return opt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void SetProperties(ParseResult context)
|
||||||
|
{
|
||||||
|
foreach (var kvp in _setters) {
|
||||||
|
if (context.Errors.Any(e => e.SymbolResult?.Symbol?.Equals(kvp.Key) == true)) {
|
||||||
|
continue; // skip setting values for options with errors
|
||||||
|
}
|
||||||
|
kvp.Value(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ParseResult ParseAndApply(string command)
|
||||||
|
{
|
||||||
|
var x = this.Parse(command);
|
||||||
|
SetProperties(x);
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface INugetPackCommand
|
||||||
|
{
|
||||||
|
string PackId { get; }
|
||||||
|
string PackVersion { get; }
|
||||||
|
string PackDirectory { get; }
|
||||||
|
string PackAuthors { get; }
|
||||||
|
string PackTitle { get; }
|
||||||
|
bool IncludePdb { get; }
|
||||||
|
string ReleaseNotes { get; }
|
||||||
|
}
|
||||||
53
src/Squirrel.Csq/Commands/GitHubCommands.cs
Normal file
53
src/Squirrel.Csq/Commands/GitHubCommands.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace Squirrel.Csq.Commands;
|
||||||
|
|
||||||
|
public class GitHubBaseCommand : BaseCommand
|
||||||
|
{
|
||||||
|
public string RepoUrl { get; private set; }
|
||||||
|
|
||||||
|
public string Token { get; private set; }
|
||||||
|
|
||||||
|
protected GitHubBaseCommand(string name, string description)
|
||||||
|
: base(name, description)
|
||||||
|
{
|
||||||
|
AddOption<Uri>((v) => RepoUrl = v.ToAbsoluteOrNull(), "--repoUrl")
|
||||||
|
.SetDescription("Full url to the github repository (eg. 'https://github.com/myname/myrepo').")
|
||||||
|
.SetRequired()
|
||||||
|
.MustBeValidHttpUri();
|
||||||
|
|
||||||
|
AddOption<string>((v) => Token = v, "--token")
|
||||||
|
.SetDescription("OAuth token to use as login credentials.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GitHubDownloadCommand : GitHubBaseCommand
|
||||||
|
{
|
||||||
|
public bool Pre { get; private set; }
|
||||||
|
|
||||||
|
public GitHubDownloadCommand()
|
||||||
|
: base("github", "Download latest release from GitHub repository.")
|
||||||
|
{
|
||||||
|
AddOption<bool>((v) => Pre = v, "--pre")
|
||||||
|
.SetDescription("Get latest pre-release instead of stable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GitHubUploadCommand : GitHubBaseCommand
|
||||||
|
{
|
||||||
|
public bool Publish { get; private set; }
|
||||||
|
|
||||||
|
public string ReleaseName { get; private set; }
|
||||||
|
|
||||||
|
public GitHubUploadCommand()
|
||||||
|
: base("github", "Upload releases to a GitHub repository.")
|
||||||
|
{
|
||||||
|
AddOption<bool>((v) => Publish = v, "--publish")
|
||||||
|
.SetDescription("Publish release instead of creating draft.");
|
||||||
|
|
||||||
|
AddOption<string>((v) => ReleaseName = v, "--releaseName")
|
||||||
|
.SetDescription("A custom name for created release.")
|
||||||
|
.SetArgumentHelpName("NAME");
|
||||||
|
|
||||||
|
ReleaseDirectoryOption.SetRequired();
|
||||||
|
ReleaseDirectoryOption.MustNotBeEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/Squirrel.Csq/Commands/HttpCommands.cs
Normal file
15
src/Squirrel.Csq/Commands/HttpCommands.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Squirrel.Csq.Commands;
|
||||||
|
|
||||||
|
public class HttpDownloadCommand : BaseCommand
|
||||||
|
{
|
||||||
|
public string Url { get; private set; }
|
||||||
|
|
||||||
|
public HttpDownloadCommand()
|
||||||
|
: base("http", "Download latest release from a HTTP source.")
|
||||||
|
{
|
||||||
|
AddOption<Uri>((v) => Url = v.ToAbsoluteOrNull(), "--url")
|
||||||
|
.SetDescription("Url to download remote releases from.")
|
||||||
|
.MustBeValidHttpUri()
|
||||||
|
.SetRequired();
|
||||||
|
}
|
||||||
|
}
|
||||||
161
src/Squirrel.Csq/Commands/OsxCommands.cs
Normal file
161
src/Squirrel.Csq/Commands/OsxCommands.cs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
namespace Squirrel.Csq.Commands;
|
||||||
|
|
||||||
|
public class BundleOsxCommand : BaseCommand
|
||||||
|
{
|
||||||
|
public string PackId { get; private set; }
|
||||||
|
|
||||||
|
public string PackVersion { get; private set; }
|
||||||
|
|
||||||
|
public string PackDirectory { get; private set; }
|
||||||
|
|
||||||
|
public string PackAuthors { get; private set; }
|
||||||
|
|
||||||
|
public string PackTitle { get; private set; }
|
||||||
|
|
||||||
|
public string EntryExecutableName { get; private set; }
|
||||||
|
|
||||||
|
public string Icon { get; private set; }
|
||||||
|
|
||||||
|
public string BundleId { get; private set; }
|
||||||
|
|
||||||
|
public BundleOsxCommand()
|
||||||
|
: base("bundle", "Create's an OSX .app bundle from a folder containing application files.")
|
||||||
|
{
|
||||||
|
AddOption<string>((v) => PackId = v, "--packId", "-u")
|
||||||
|
.SetDescription("Unique Squirrel Id for application bundle.")
|
||||||
|
.SetArgumentHelpName("ID")
|
||||||
|
.SetRequired()
|
||||||
|
.RequiresValidNuGetId();
|
||||||
|
|
||||||
|
// TODO add parser straight to SemanticVersion?
|
||||||
|
AddOption<string>((v) => PackVersion = v, "--packVersion", "-v")
|
||||||
|
.SetDescription("Current version for application bundle.")
|
||||||
|
.SetArgumentHelpName("VERSION")
|
||||||
|
.SetRequired()
|
||||||
|
.RequiresSemverCompliant();
|
||||||
|
|
||||||
|
AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
|
||||||
|
.SetDescription("Directory containing application files for release.")
|
||||||
|
.SetArgumentHelpName("DIR")
|
||||||
|
.SetRequired()
|
||||||
|
.MustNotBeEmpty();
|
||||||
|
|
||||||
|
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
|
||||||
|
.SetDescription("Company name or comma-delimited list of authors.")
|
||||||
|
.SetArgumentHelpName("AUTHORS");
|
||||||
|
|
||||||
|
AddOption<string>((v) => PackTitle = v, "--packTitle")
|
||||||
|
.SetDescription("Display/friendly name for application.")
|
||||||
|
.SetArgumentHelpName("NAME");
|
||||||
|
|
||||||
|
AddOption<string>((v) => EntryExecutableName = v, "-e", "--mainExe")
|
||||||
|
.SetDescription("The file name of the main/entry executable.")
|
||||||
|
.SetArgumentHelpName("NAME")
|
||||||
|
.SetRequired();
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
|
||||||
|
.SetDescription("Path to the .icns file for this bundle.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly()
|
||||||
|
.SetRequired()
|
||||||
|
.RequiresExtension(".icns");
|
||||||
|
|
||||||
|
AddOption<string>((v) => BundleId = v, "--bundleId")
|
||||||
|
.SetDescription("Optional Apple bundle Id.")
|
||||||
|
.SetArgumentHelpName("ID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReleasifyOsxCommand : BaseCommand
|
||||||
|
{
|
||||||
|
public string BundleDirectory { get; private set; }
|
||||||
|
|
||||||
|
public bool IncludePdb { get; private set; }
|
||||||
|
|
||||||
|
public string ReleaseNotes { get; private set; }
|
||||||
|
|
||||||
|
public bool NoDelta { get; private set; }
|
||||||
|
|
||||||
|
public bool NoPackage { get; private set; }
|
||||||
|
|
||||||
|
public string PackageWelcome { get; private set; }
|
||||||
|
|
||||||
|
public string PackageReadme { get; private set; }
|
||||||
|
|
||||||
|
public string PackageLicense { get; private set; }
|
||||||
|
|
||||||
|
public string PackageConclusion { get; private set; }
|
||||||
|
|
||||||
|
public string SigningAppIdentity { get; private set; }
|
||||||
|
|
||||||
|
public string SigningInstallIdentity { get; private set; }
|
||||||
|
|
||||||
|
public string SigningEntitlements { get; private set; }
|
||||||
|
|
||||||
|
public string NotaryProfile { get; private set; }
|
||||||
|
|
||||||
|
public ReleasifyOsxCommand()
|
||||||
|
: base("releasify", "Converts an application bundle into a Squirrel release and installer.")
|
||||||
|
{
|
||||||
|
AddOption<DirectoryInfo>((v) => BundleDirectory = v.ToFullNameOrNull(), "-b", "--bundle")
|
||||||
|
.SetDescription("The bundle to convert into a Squirrel release.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.MustNotBeEmpty()
|
||||||
|
.RequiresExtension(".app")
|
||||||
|
.SetRequired();
|
||||||
|
|
||||||
|
AddOption<bool>((v) => IncludePdb = v, "--includePdb")
|
||||||
|
.SetDescription("Add *.pdb files to release package.");
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
|
||||||
|
.SetDescription("File with markdown-formatted notes for this version.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly();
|
||||||
|
|
||||||
|
AddOption<bool>((v) => NoDelta = v, "--noDelta")
|
||||||
|
.SetDescription("Skip the generation of delta packages.");
|
||||||
|
|
||||||
|
if (SquirrelRuntimeInfo.IsOSX) {
|
||||||
|
AddOption<bool>((v) => NoPackage = v, "--noPkg")
|
||||||
|
.SetDescription("Skip generating a .pkg installer.");
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => PackageWelcome = v.ToFullNameOrNull(), "--pkgWelcome")
|
||||||
|
.SetDescription("Set the installer package welcome content.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly();
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => PackageReadme = v.ToFullNameOrNull(), "--pkgReadme")
|
||||||
|
.SetDescription("Set the installer package readme content.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly();
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => PackageLicense = v.ToFullNameOrNull(), "--pkgLicense")
|
||||||
|
.SetDescription("Set the installer package license content.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly();
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => PackageConclusion = v.ToFullNameOrNull(), "--pkgConclusion")
|
||||||
|
.SetDescription("Set the installer package conclusion content.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly();
|
||||||
|
|
||||||
|
AddOption<string>((v) => SigningAppIdentity = v, "--signAppIdentity")
|
||||||
|
.SetDescription("The subject name of the cert to use for app code signing.")
|
||||||
|
.SetArgumentHelpName("SUBJECT");
|
||||||
|
|
||||||
|
AddOption<string>((v) => SigningInstallIdentity = v, "--signInstallIdentity")
|
||||||
|
.SetDescription("The subject name of the cert to use for installation packages.")
|
||||||
|
.SetArgumentHelpName("SUBJECT");
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => SigningEntitlements = v.ToFullNameOrNull(), "--signEntitlements")
|
||||||
|
.SetDescription("Path to entitlements file for hardened runtime signing.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly()
|
||||||
|
.RequiresExtension(".entitlements");
|
||||||
|
|
||||||
|
AddOption<string>((v) => NotaryProfile = v, "--notaryProfile")
|
||||||
|
.SetDescription("Name of profile containing Apple credentials stored with notarytool.")
|
||||||
|
.SetArgumentHelpName("NAME");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/Squirrel.Csq/Commands/S3Commands.cs
Normal file
98
src/Squirrel.Csq/Commands/S3Commands.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
|
||||||
|
namespace Squirrel.Csq.Commands;
|
||||||
|
|
||||||
|
public class S3BaseCommand : BaseCommand
|
||||||
|
{
|
||||||
|
public string KeyId { get; private set; }
|
||||||
|
|
||||||
|
public string Secret { get; private set; }
|
||||||
|
|
||||||
|
public string Region { get; private set; }
|
||||||
|
|
||||||
|
public string Endpoint { get; private set; }
|
||||||
|
|
||||||
|
public string Bucket { get; private set; }
|
||||||
|
|
||||||
|
public string PathPrefix { get; private set; }
|
||||||
|
|
||||||
|
protected S3BaseCommand(string name, string description)
|
||||||
|
: base(name, description)
|
||||||
|
{
|
||||||
|
AddOption<string>((v) => KeyId = v, "--keyId")
|
||||||
|
.SetDescription("Authentication identifier or access key.")
|
||||||
|
.SetArgumentHelpName("KEYID")
|
||||||
|
.SetRequired();
|
||||||
|
|
||||||
|
AddOption<string>((v) => Secret = v, "--secret")
|
||||||
|
.SetDescription("Authentication secret key.")
|
||||||
|
.SetArgumentHelpName("KEY")
|
||||||
|
.SetRequired();
|
||||||
|
|
||||||
|
var region = AddOption<string>((v) => Region = v, "--region")
|
||||||
|
.SetDescription("AWS service region (eg. us-west-1).")
|
||||||
|
.SetArgumentHelpName("REGION");
|
||||||
|
|
||||||
|
region.Validators.Add(MustBeValidAwsRegion);
|
||||||
|
|
||||||
|
var endpoint = AddOption<Uri>((v) => Endpoint = v.ToAbsoluteOrNull(), "--endpoint")
|
||||||
|
.SetDescription("Custom service url (backblaze, digital ocean, etc).")
|
||||||
|
.SetArgumentHelpName("URL")
|
||||||
|
.MustBeValidHttpUri();
|
||||||
|
|
||||||
|
this.AreMutuallyExclusive(region, endpoint);
|
||||||
|
this.AtLeastOneRequired(region, endpoint);
|
||||||
|
|
||||||
|
AddOption<string>((v) => Bucket = v, "--bucket")
|
||||||
|
.SetDescription("Name of the S3 bucket.")
|
||||||
|
.SetArgumentHelpName("NAME")
|
||||||
|
.SetRequired();
|
||||||
|
|
||||||
|
AddOption<string>((v) => PathPrefix = v, "--pathPrefix")
|
||||||
|
.SetDescription("A sub-folder used for files in the bucket, for creating release channels (eg. 'stable' or 'dev').")
|
||||||
|
.SetArgumentHelpName("PREFIX");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MustBeValidAwsRegion(OptionResult result)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
var region = result.Tokens[i].Value;
|
||||||
|
if (!string.IsNullOrWhiteSpace(region)) {
|
||||||
|
var r = Amazon.RegionEndpoint.GetBySystemName(result.Tokens[0].Value);
|
||||||
|
if (r is null || r.DisplayName == "Unknown") {
|
||||||
|
result.AddError($"Region '{region}' lookup failed, is this a valid AWS region?");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.AddError("A region value is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class S3DownloadCommand : S3BaseCommand
|
||||||
|
{
|
||||||
|
public S3DownloadCommand()
|
||||||
|
: base("s3", "Download latest release from an S3 bucket.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class S3UploadCommand : S3BaseCommand
|
||||||
|
{
|
||||||
|
public bool Overwrite { get; private set; }
|
||||||
|
|
||||||
|
public int KeepMaxReleases { get; private set; }
|
||||||
|
|
||||||
|
public S3UploadCommand()
|
||||||
|
: base("s3", "Upload releases to a S3 bucket.")
|
||||||
|
{
|
||||||
|
AddOption<bool>((v) => Overwrite = v, "--overwrite")
|
||||||
|
.SetDescription("Replace remote files if local files have changed.");
|
||||||
|
|
||||||
|
AddOption<int>((v) => KeepMaxReleases = v, "--keepMaxReleases")
|
||||||
|
.SetDescription("Apply a retention policy which keeps only the specified number of old versions in remote source.")
|
||||||
|
.SetArgumentHelpName("NUMBER");
|
||||||
|
|
||||||
|
ReleaseDirectoryOption.SetRequired();
|
||||||
|
ReleaseDirectoryOption.MustNotBeEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
308
src/Squirrel.Csq/Commands/SystemCommandLineExtensions.cs
Normal file
308
src/Squirrel.Csq/Commands/SystemCommandLineExtensions.cs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using NuGet.Versioning;
|
||||||
|
|
||||||
|
namespace Squirrel.Csq.Commands;
|
||||||
|
|
||||||
|
internal static class SystemCommandLineExtensions
|
||||||
|
{
|
||||||
|
public static string ToFullNameOrNull(this FileSystemInfo fsi)
|
||||||
|
{
|
||||||
|
return fsi?.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToAbsoluteOrNull(this Uri uri)
|
||||||
|
{
|
||||||
|
if (uri?.IsAbsoluteUri == true) return uri.AbsoluteUri;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<T> SetDescription<T>(this CliOption<T> option, string description)
|
||||||
|
{
|
||||||
|
option.Description = description;
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<T> SetHidden<T>(this CliOption<T> option, bool isHidden = true)
|
||||||
|
{
|
||||||
|
option.Hidden = isHidden;
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<T> SetRequired<T>(this CliOption<T> option, bool isRequired = true)
|
||||||
|
{
|
||||||
|
option.Required = isRequired;
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<T> SetDefault<T>(this CliOption<T> option, T defaultValue)
|
||||||
|
{
|
||||||
|
option.SetDefault(defaultValue);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<T> SetArgumentHelpName<T>(this CliOption<T> option, string argumentHelpName)
|
||||||
|
{
|
||||||
|
option.HelpName = argumentHelpName;
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<int> MustBeBetween(this CliOption<int> option, int minimum, int maximum)
|
||||||
|
{
|
||||||
|
option.Validators.Add(x => Validate.MustBeBetween(x, minimum, maximum));
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<Uri> MustBeValidHttpUri(this CliOption<Uri> option)
|
||||||
|
{
|
||||||
|
option.RequiresScheme(Uri.UriSchemeHttp, Uri.UriSchemeHttps).RequiresAbsolute();
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<FileInfo> RequiresExtension(this CliOption<FileInfo> option, string extension)
|
||||||
|
{
|
||||||
|
option.Validators.Add(x => Validate.RequiresExtension(x, extension));
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<DirectoryInfo> RequiresExtension(this CliOption<DirectoryInfo> option, string extension)
|
||||||
|
{
|
||||||
|
option.Validators.Add(x => Validate.RequiresExtension(x, extension));
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliCommand AreMutuallyExclusive(this CliCommand command, params CliOption[] options)
|
||||||
|
{
|
||||||
|
command.Validators.Add(x => Validate.AreMutuallyExclusive(x, options));
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
//public static Command RequiredAllowObsoleteFallback(this Command command, Option option, Option obsoleteOption)
|
||||||
|
//{
|
||||||
|
// command.AddValidator(x => Validate.AtLeastOneRequired(x, new[] { option, obsoleteOption }, true));
|
||||||
|
// return command;
|
||||||
|
//}
|
||||||
|
|
||||||
|
public static CliCommand AtLeastOneRequired(this CliCommand command, params CliOption[] options)
|
||||||
|
{
|
||||||
|
command.Validators.Add(x => Validate.AtLeastOneRequired(x, options, false));
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<string> MustContain(this CliOption<string> option, string value)
|
||||||
|
{
|
||||||
|
option.Validators.Add(x => Validate.MustContain(x, value));
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<Uri> RequiresScheme(this CliOption<Uri> option, params string[] validSchemes)
|
||||||
|
{
|
||||||
|
option.Validators.Add(x => Validate.RequiresScheme(x, validSchemes));
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<Uri> RequiresAbsolute(this CliOption<Uri> option, params string[] validSchemes)
|
||||||
|
{
|
||||||
|
option.Validators.Add(Validate.RequiresAbsolute);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<string> RequiresValidNuGetId(this CliOption<string> option)
|
||||||
|
{
|
||||||
|
option.Validators.Add(Validate.RequiresValidNuGetId);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Could setup the options to accept type SemanticVersion and apply an appropriate parser for it
|
||||||
|
public static CliOption<string> RequiresSemverCompliant(this CliOption<string> option)
|
||||||
|
{
|
||||||
|
option.Validators.Add(Validate.RequiresSemverCompliant);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<DirectoryInfo> MustNotBeEmpty(this CliOption<DirectoryInfo> option)
|
||||||
|
{
|
||||||
|
option.Validators.Add(Validate.MustNotBeEmpty);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<string> MustBeValidFrameworkString(this CliOption<string> option)
|
||||||
|
{
|
||||||
|
option.Validators.Add(Validate.MustBeValidFrameworkString);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<string> MustBeValidMsiVersion(this CliOption<string> option)
|
||||||
|
{
|
||||||
|
option.Validators.Add(Validate.MustBeValidMsiVersion);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CliOption<string> MustBeSupportedRid(this CliOption<string> option)
|
||||||
|
{
|
||||||
|
option.Validators.Add(Validate.MustBeSupportedRid);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Validate
|
||||||
|
{
|
||||||
|
public static void MustBeBetween(OptionResult result, int minimum, int maximum)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
if (int.TryParse(result.Tokens[i].Value, out int value)) {
|
||||||
|
if (value < minimum || value > maximum) {
|
||||||
|
result.AddError($"The value for {result.IdentifierToken.Value} must be greater than {minimum} and less than {maximum}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.AddError($"{result.Tokens[i].Value} is not a valid integer for {result.IdentifierToken.Value}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RequiresExtension(OptionResult result, string extension)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
if (!string.Equals(Path.GetExtension(result.Tokens[i].Value), extension, StringComparison.InvariantCultureIgnoreCase)) {
|
||||||
|
result.AddError($"{result.IdentifierToken.Value} does not have an {extension} extension");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AreMutuallyExclusive(CommandResult result, CliOption[] options)
|
||||||
|
{
|
||||||
|
var specifiedOptions = options
|
||||||
|
.Where(x => result.GetResult(x) is not null)
|
||||||
|
.ToList();
|
||||||
|
if (specifiedOptions.Count > 1) {
|
||||||
|
string optionsString = string.Join(" and ", specifiedOptions.Select(x => $"'{x.Aliases.First()}'"));
|
||||||
|
result.AddError($"Cannot use {optionsString} options together, please choose one.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AtLeastOneRequired(CommandResult result, CliOption[] options, bool onlyShowFirst = false)
|
||||||
|
{
|
||||||
|
var anySpecifiedOptions = options
|
||||||
|
.Any(x => result.GetResult(x) is not null);
|
||||||
|
if (!anySpecifiedOptions) {
|
||||||
|
if (onlyShowFirst) {
|
||||||
|
result.AddError($"Required argument missing for option: {options.First().Aliases.First()}");
|
||||||
|
} else {
|
||||||
|
string optionsString = string.Join(" and ", options.Select(x => $"'{x.Aliases.First()}'"));
|
||||||
|
result.AddError($"At least one of the following options are required {optionsString}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MustContain(OptionResult result, string value)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
if (result.Tokens[i].Value?.Contains(value) == false) {
|
||||||
|
result.AddError($"{result.IdentifierToken.Value} must contain '{value}'. Current value is '{result.Tokens[i].Value}'");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RequiresScheme(OptionResult result, string[] validSchemes)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
if (Uri.TryCreate(result.Tokens[i].Value, UriKind.RelativeOrAbsolute, out Uri uri) &&
|
||||||
|
uri.IsAbsoluteUri &&
|
||||||
|
!validSchemes.Contains(uri.Scheme)) {
|
||||||
|
result.AddError($"{result.IdentifierToken.Value} must contain a Uri with one of the following schems: {string.Join(", ", validSchemes)}. Current value is '{result.Tokens[i].Value}'");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RequiresAbsolute(OptionResult result)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
if (!Uri.TryCreate(result.Tokens[i].Value, UriKind.Absolute, out Uri _)) {
|
||||||
|
result.AddError($"{result.IdentifierToken.Value} must contain an absolute Uri. Current value is '{result.Tokens[i].Value}'");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RequiresValidNuGetId(OptionResult result)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
if (!NugetUtil.IsValidNuGetId(result.Tokens[i].Value)) {
|
||||||
|
result.AddError($"{result.IdentifierToken.Value} is an invalid NuGet package id. It must contain only alphanumeric characters, underscores, dashes, and dots.. Current value is '{result.Tokens[i].Value}'");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RequiresSemverCompliant(OptionResult result)
|
||||||
|
{
|
||||||
|
string specifiedAlias = result.IdentifierToken.Value;
|
||||||
|
|
||||||
|
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
string version = result.Tokens[i].Value;
|
||||||
|
//TODO: This is duplicating NugetUtil.ThrowIfVersionNotSemverCompliant
|
||||||
|
if (SemanticVersion.TryParse(version, out var parsed)) {
|
||||||
|
if (parsed < new SemanticVersion(0, 0, 1)) {
|
||||||
|
result.AddError($"{result.IdentifierToken.Value} contains an invalid package version '{version}', it must be >= 0.0.1.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.AddError($"{result.IdentifierToken.Value} contains an invalid package version '{version}', it must be a 3-part SemVer2 compliant version string.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MustNotBeEmpty(OptionResult result)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
var token = result.Tokens[i];
|
||||||
|
|
||||||
|
if (!Directory.Exists(token.Value) ||
|
||||||
|
!Directory.EnumerateFileSystemEntries(token.Value).Any()) {
|
||||||
|
result.AddError($"{result.IdentifierToken.Value} must be a non-empty directory, but the specified directory '{token.Value}' was empty.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MustBeValidFrameworkString(OptionResult result)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
var framework = result.Tokens[i].Value;
|
||||||
|
try {
|
||||||
|
Runtimes.ParseDependencyString(framework);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.AddError(e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MustBeValidMsiVersion(OptionResult result)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
var version = result.Tokens[i].Value;
|
||||||
|
if (Version.TryParse(version, out var parsed)) {
|
||||||
|
if (parsed.Major > 255 || parsed.Minor > 255 || parsed.Build > 65535 || parsed.Revision > 0) {
|
||||||
|
result.AddError($"MSI ProductVersion out of bounds '{version}'. Valid range is [0-255].[0-255].[0-65535].[0]");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.AddError("Version string is invalid / could not be parsed.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MustBeSupportedRid(OptionResult result)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||||
|
if (!Regex.IsMatch(result.Tokens[i].Value, @"^(?<os>osx|win)\.?(?<ver>[\d\.]+)?(?:-(?<arch>(?:x|arm)\d{2}))$"))
|
||||||
|
result.AddError($"Invalid or unsupported runtime '{result.IdentifierToken.Value}'. Valid example: win-x64, osx-arm64.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
192
src/Squirrel.Csq/Commands/WindowsCommands.cs
Normal file
192
src/Squirrel.Csq/Commands/WindowsCommands.cs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
|
||||||
|
namespace Squirrel.Csq.Commands;
|
||||||
|
|
||||||
|
public class SigningCommand : BaseCommand
|
||||||
|
{
|
||||||
|
public string SignParameters { get; private set; }
|
||||||
|
|
||||||
|
public bool SignSkipDll { get; private set; }
|
||||||
|
|
||||||
|
public int SignParallel { get; private set; }
|
||||||
|
|
||||||
|
public string SignTemplate { get; private set; }
|
||||||
|
|
||||||
|
protected SigningCommand(string name, string description)
|
||||||
|
: base(name, description)
|
||||||
|
{
|
||||||
|
var signTemplate = AddOption<string>((v) => SignTemplate = v, "--signTemplate")
|
||||||
|
.SetDescription("Use a custom signing command. {{file}} will be replaced by the path to sign.")
|
||||||
|
.SetArgumentHelpName("COMMAND")
|
||||||
|
.MustContain("{{file}}");
|
||||||
|
|
||||||
|
AddOption<bool>((v) => SignSkipDll = v, "--signSkipDll")
|
||||||
|
.SetDescription("Only signs EXE files, and skips signing DLL files.");
|
||||||
|
|
||||||
|
if (SquirrelRuntimeInfo.IsWindows) {
|
||||||
|
var signParams = AddOption<string>((v) => SignParameters = v, "--signParams", "-n")
|
||||||
|
.SetDescription("Sign files via signtool.exe using these parameters.")
|
||||||
|
.SetArgumentHelpName("PARAMS");
|
||||||
|
|
||||||
|
this.AreMutuallyExclusive(signTemplate, signParams);
|
||||||
|
|
||||||
|
AddOption<int>((v) => SignParallel = v, "--signParallel")
|
||||||
|
.SetDescription("The number of files to sign in each call to signtool.exe.")
|
||||||
|
.SetArgumentHelpName("NUM")
|
||||||
|
.MustBeBetween(1, 1000)
|
||||||
|
.SetDefault(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReleasifyWindowsCommand : SigningCommand
|
||||||
|
{
|
||||||
|
public string Package { get; set; }
|
||||||
|
|
||||||
|
public string BaseUrl { get; private set; }
|
||||||
|
|
||||||
|
public string DebugSetupExe { get; private set; }
|
||||||
|
|
||||||
|
public bool NoDelta { get; private set; }
|
||||||
|
|
||||||
|
public string Runtimes { get; private set; }
|
||||||
|
|
||||||
|
public string SplashImage { get; private set; }
|
||||||
|
|
||||||
|
public string Icon { get; private set; }
|
||||||
|
|
||||||
|
public string[] SquirrelAwareExecutableNames { get; private set; }
|
||||||
|
|
||||||
|
public string AppIcon { get; private set; }
|
||||||
|
|
||||||
|
public bool BuildMsi { get; private set; }
|
||||||
|
|
||||||
|
public string MsiVersion { get; private set; }
|
||||||
|
|
||||||
|
public ReleasifyWindowsCommand()
|
||||||
|
: this("releasify", "Take an existing nuget package and convert it into a Squirrel release.")
|
||||||
|
{
|
||||||
|
AddOption<FileInfo>((v) => Package = v.ToFullNameOrNull(), "-p", "--package")
|
||||||
|
.SetDescription("Path to a '.nupkg' package to releasify.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.SetRequired()
|
||||||
|
.AcceptExistingOnly()
|
||||||
|
.RequiresExtension(".nupkg");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This constructor is used by the pack command, which requires all the same properties but
|
||||||
|
/// does not allow the user to provide the Package (it is created/populated by Squirrel).
|
||||||
|
/// </summary>
|
||||||
|
protected ReleasifyWindowsCommand(string name, string description)
|
||||||
|
: base(name, description)
|
||||||
|
{
|
||||||
|
AddOption<Uri>((v) => BaseUrl = v.ToAbsoluteOrNull(), "-b", "--baseUrl")
|
||||||
|
.SetDescription("Provides a base URL to prefix the RELEASES file packages with.")
|
||||||
|
.SetHidden()
|
||||||
|
.MustBeValidHttpUri();
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => DebugSetupExe = v.ToFullNameOrNull(), "--debugSetupExe")
|
||||||
|
.SetDescription("Uses the Setup.exe at this {PATH} to create the bundle, and then replaces it with the bundle. " +
|
||||||
|
"Used for locally debugging Setup.exe with a real bundle attached.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.SetHidden()
|
||||||
|
.AcceptExistingOnly()
|
||||||
|
.RequiresExtension(".exe");
|
||||||
|
|
||||||
|
AddOption<bool>((v) => NoDelta = v, "--noDelta")
|
||||||
|
.SetDescription("Skip the generation of delta packages.");
|
||||||
|
|
||||||
|
AddOption<string>((v) => Runtimes = v, "-f", "--framework")
|
||||||
|
.SetDescription("List of required runtimes to install during setup. example: 'net6,vcredist143'.")
|
||||||
|
.SetArgumentHelpName("RUNTIMES")
|
||||||
|
.MustBeValidFrameworkString();
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => SplashImage = v.ToFullNameOrNull(), "-s", "--splashImage")
|
||||||
|
.SetDescription("Path to image displayed during installation.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly();
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
|
||||||
|
.SetDescription("Path to .ico for Setup.exe and Update.exe.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly()
|
||||||
|
.RequiresExtension(".ico");
|
||||||
|
|
||||||
|
AddOption<string[]>((v) => SquirrelAwareExecutableNames = v ?? new string[0], "-e", "--mainExe")
|
||||||
|
.SetDescription("Name of one or more SquirrelAware executables.")
|
||||||
|
.SetArgumentHelpName("NAME");
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => AppIcon = v.ToFullNameOrNull(), "--appIcon")
|
||||||
|
.SetDescription("Path to .ico for 'Apps and Features' list.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly()
|
||||||
|
.RequiresExtension(".ico");
|
||||||
|
|
||||||
|
if (SquirrelRuntimeInfo.IsWindows) {
|
||||||
|
AddOption<bool>((v) => BuildMsi = v, "--msi")
|
||||||
|
.SetDescription("Compile a .msi machine-wide deployment tool.")
|
||||||
|
.SetArgumentHelpName("BITNESS");
|
||||||
|
|
||||||
|
AddOption<string>((v) => MsiVersion = v, "--msiVersion")
|
||||||
|
.SetDescription("Override the product version for the generated msi.")
|
||||||
|
.SetArgumentHelpName("VERSION")
|
||||||
|
.MustBeValidMsiVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PackWindowsCommand : ReleasifyWindowsCommand, INugetPackCommand
|
||||||
|
{
|
||||||
|
public string PackId { get; private set; }
|
||||||
|
|
||||||
|
public string PackVersion { get; private set; }
|
||||||
|
|
||||||
|
public string PackDirectory { get; private set; }
|
||||||
|
|
||||||
|
public string PackAuthors { get; private set; }
|
||||||
|
|
||||||
|
public string PackTitle { get; private set; }
|
||||||
|
|
||||||
|
public bool IncludePdb { get; private set; }
|
||||||
|
|
||||||
|
public string ReleaseNotes { get; private set; }
|
||||||
|
|
||||||
|
public PackWindowsCommand()
|
||||||
|
: base("pack", "Creates a Squirrel release from a folder containing application files.")
|
||||||
|
{
|
||||||
|
AddOption<string>((v) => PackId = v, "--packId", "-u")
|
||||||
|
.SetDescription("Unique Id for application bundle.")
|
||||||
|
.SetArgumentHelpName("ID")
|
||||||
|
.SetRequired()
|
||||||
|
.RequiresValidNuGetId();
|
||||||
|
|
||||||
|
// TODO add parser straight to SemanticVersion
|
||||||
|
AddOption<string>((v) => PackVersion = v, "--packVersion", "-v")
|
||||||
|
.SetDescription("Current version for application bundle.")
|
||||||
|
.SetArgumentHelpName("VERSION")
|
||||||
|
.SetRequired()
|
||||||
|
.RequiresSemverCompliant();
|
||||||
|
|
||||||
|
AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
|
||||||
|
.SetDescription("Directory containing application files for release.")
|
||||||
|
.SetArgumentHelpName("DIR")
|
||||||
|
.SetRequired()
|
||||||
|
.MustNotBeEmpty();
|
||||||
|
|
||||||
|
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
|
||||||
|
.SetDescription("Company name or comma-delimited list of authors.")
|
||||||
|
.SetArgumentHelpName("AUTHORS");
|
||||||
|
|
||||||
|
AddOption<string>((v) => PackTitle = v, "--packTitle")
|
||||||
|
.SetDescription("Display/friendly name for application.")
|
||||||
|
.SetArgumentHelpName("NAME");
|
||||||
|
|
||||||
|
AddOption<bool>((v) => IncludePdb = v, "--includePdb")
|
||||||
|
.SetDescription("Add *.pdb files to release package");
|
||||||
|
|
||||||
|
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
|
||||||
|
.SetDescription("File with markdown-formatted notes for this version.")
|
||||||
|
.SetArgumentHelpName("PATH")
|
||||||
|
.AcceptExistingOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
#if false
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using System.CommandLine.Builder;
|
using System.CommandLine.Builder;
|
||||||
@@ -12,8 +13,9 @@ using System.Threading.Tasks;
|
|||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Microsoft.Build.Construction;
|
using Microsoft.Build.Construction;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Squirrel.CommandLine;
|
using Squirrel.CommandLine;
|
||||||
using LogLevel = Squirrel.SimpleSplat.LogLevel;
|
|
||||||
|
|
||||||
namespace Squirrel.Tool
|
namespace Squirrel.Tool
|
||||||
{
|
{
|
||||||
@@ -23,18 +25,26 @@ namespace Squirrel.Tool
|
|||||||
|
|
||||||
private static ConsoleLogger _logger;
|
private static ConsoleLogger _logger;
|
||||||
|
|
||||||
private static Option<string> CsqVersion { get; }
|
private static CliOption<string> CsqVersion { get; }
|
||||||
= new Option<string>("--csq-version");
|
= new CliOption<string>("--csq-version");
|
||||||
private static Option<FileSystemInfo> CsqSolutionPath { get; }
|
private static CliOption<FileSystemInfo> CsqSolutionPath { get; }
|
||||||
= new Option<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly();
|
= new CliOption<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly();
|
||||||
private static Option<bool> Verbose { get; }
|
private static CliOption<bool> Verbose { get; }
|
||||||
= new Option<bool>("--verbose");
|
= new CliOption<bool>("--verbose");
|
||||||
|
|
||||||
static Task<int> Main(string[] inargs)
|
static Task<int> Main(string[] args)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||||
|
builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
|
||||||
|
builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
|
||||||
|
builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
|
||||||
|
builder.Configuration.AddCommandLine(args);
|
||||||
_logger = ConsoleLogger.RegisterLogger();
|
_logger = ConsoleLogger.RegisterLogger();
|
||||||
|
|
||||||
RootCommand rootCommand = new RootCommand() {
|
System.CommandLine.Hosting.HostingExtensions.
|
||||||
|
|
||||||
|
CliRootCommand rootCommand = new CliRootCommand() {
|
||||||
CsqVersion,
|
CsqVersion,
|
||||||
CsqSolutionPath,
|
CsqSolutionPath,
|
||||||
Verbose
|
Verbose
|
||||||
7
src/Squirrel.Csq/GlobalUsings.cs
Normal file
7
src/Squirrel.Csq/GlobalUsings.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
global using System;
|
||||||
|
global using System.Collections.Generic;
|
||||||
|
global using System.CommandLine;
|
||||||
|
global using System.CommandLine.Parsing;
|
||||||
|
global using System.IO;
|
||||||
|
global using System.Linq;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
80
src/Squirrel.Csq/Program.cs
Normal file
80
src/Squirrel.Csq/Program.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Serilog.Events;
|
||||||
|
using Serilog;
|
||||||
|
using CliFx;
|
||||||
|
|
||||||
|
namespace Squirrel.Csq;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings {
|
||||||
|
Args = args,
|
||||||
|
ApplicationName = "My App",
|
||||||
|
EnvironmentName = "Production",
|
||||||
|
ContentRootPath = Environment.CurrentDirectory,
|
||||||
|
Configuration = new ConfigurationManager(),
|
||||||
|
});
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
host.Services.
|
||||||
|
|
||||||
|
|
||||||
|
return await new CliApplicationBuilder()
|
||||||
|
.AddCommandsFromThisAssembly()
|
||||||
|
.Build()
|
||||||
|
.RunAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Parser CreateParser()
|
||||||
|
{
|
||||||
|
//var rootCommand = new RootCommand("Bicep registry module tool")
|
||||||
|
// .AddSubcommand(new ValidateCommand())
|
||||||
|
// .AddSubcommand(new GenerateCommand());
|
||||||
|
|
||||||
|
var parser = new CliConfiguration(null)
|
||||||
|
.UseHost(Host.CreateDefaultBuilder, ConfigureHost)
|
||||||
|
.UseDefaults()
|
||||||
|
.UseVerboseOption()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Have to use parser.Invoke instead of rootCommand.Invoke due to the
|
||||||
|
// System.CommandLine bug: https://github.com/dotnet/command-line-api/issues/1691.
|
||||||
|
rootCommand.Handler = CommandHandler.Create(() => parser.Invoke("-h"));
|
||||||
|
|
||||||
|
return parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IHostBuilder CreateBuilder(string[] args)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureHost(IHostBuilder builder)
|
||||||
|
{
|
||||||
|
builder.UseSerilog((context, logging) => logging
|
||||||
|
.MinimumLevel.Is(GetMinimumLogEventLevel(context))
|
||||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
||||||
|
.WriteTo.Console())
|
||||||
|
.UseCommandHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LogEventLevel GetMinimumLogEventLevel(HostBuilderContext context)
|
||||||
|
{
|
||||||
|
var verboseSpecified =
|
||||||
|
context.Properties.TryGetValue(typeof(InvocationContext), out var value) &&
|
||||||
|
value is InvocationContext invocationContext &&
|
||||||
|
invocationContext.ParseResult.FindResultFor(GlobalOptions.Verbose) is not null;
|
||||||
|
|
||||||
|
return verboseSpecified ? LogEventLevel.Debug : LogEventLevel.Fatal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
<AssemblyName>csq</AssemblyName>
|
<AssemblyName>csq</AssemblyName>
|
||||||
<PackageId>csq</PackageId>
|
<PackageId>csq</PackageId>
|
||||||
@@ -13,21 +13,20 @@
|
|||||||
<PackAsTool>true</PackAsTool>
|
<PackAsTool>true</PackAsTool>
|
||||||
<Description>A .NET Core Tool that uses the Squirrel framework to create installers and update packages for dotnet applications.</Description>
|
<Description>A .NET Core Tool that uses the Squirrel framework to create installers and update packages for dotnet applications.</Description>
|
||||||
<PackageIcon>Clowd_200.png</PackageIcon>
|
<PackageIcon>Clowd_200.png</PackageIcon>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\..\docs\artwork\Clowd_200.png" Pack="true" PackagePath="\" />
|
<None Include="..\..\docs\artwork\Clowd_200.png" Pack="true" PackagePath="\" />
|
||||||
<Compile Include="..\Squirrel.CommandLine\ConsoleLogger.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Build" Version="17.3.2" />
|
<PackageReference Include="Microsoft.Build" Version="17.3.2" />
|
||||||
<PackageReference Include="NuGet.Protocol" Version="6.7.0" />
|
<PackageReference Include="NuGet.Protocol" Version="6.7.0" />
|
||||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||||
</ItemGroup>
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||||
<ItemGroup>
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.23407.1" />
|
||||||
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
155
src/Squirrel.Deployment/GitHubRepository.cs
Normal file
155
src/Squirrel.Deployment/GitHubRepository.cs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Octokit;
|
||||||
|
using Squirrel.Extensions;
|
||||||
|
using Squirrel.Sources;
|
||||||
|
|
||||||
|
namespace Squirrel.CommandLine.Sync;
|
||||||
|
|
||||||
|
public class GitHubRepository
|
||||||
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
|
|
||||||
|
public GitHubRepository(ILogger logger)
|
||||||
|
{
|
||||||
|
_log = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DownloadRecentPackages(DirectoryInfo releaseDirectoryInfo, string repoUrl, string token, bool asPrerelease)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(token))
|
||||||
|
_log.Warn("No GitHub access token provided. Unauthenticated requests will be limited to 60 per hour.");
|
||||||
|
|
||||||
|
_log.Info("Fetching RELEASES...");
|
||||||
|
var source = new GithubSource(repoUrl, token, asPrerelease);
|
||||||
|
var latestReleaseEntries = await source.GetReleaseFeed();
|
||||||
|
|
||||||
|
if (latestReleaseEntries == null || latestReleaseEntries.Length == 0) {
|
||||||
|
_log.Warn("No github release or assets found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Info($"Found {latestReleaseEntries.Length} assets in RELEASES file for GitHub version {source.Release.Name}.");
|
||||||
|
|
||||||
|
var releasesToDownload = latestReleaseEntries
|
||||||
|
.Where(x => !x.IsDelta)
|
||||||
|
.OrderByDescending(x => x.Version)
|
||||||
|
.Take(1)
|
||||||
|
.Select(x => new {
|
||||||
|
Obj = x,
|
||||||
|
LocalPath = Path.Combine(releaseDirectoryInfo.FullName, x.Filename),
|
||||||
|
Filename = x.Filename,
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var entry in releasesToDownload) {
|
||||||
|
if (File.Exists(entry.LocalPath)) {
|
||||||
|
_log.Warn($"File '{entry.Filename}' exists on disk, skipping download.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Info($"Downloading {entry.Filename}...");
|
||||||
|
await source.DownloadReleaseEntry(entry.Obj, entry.LocalPath, (p) => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
||||||
|
_log.Info("Done.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task UploadMissingPackages(GitHubUploadCommand options)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(options.Token))
|
||||||
|
throw new InvalidOperationException("Must provide access token to create a GitHub release.");
|
||||||
|
|
||||||
|
var releaseDirectoryInfo = options.GetReleaseDirectory();
|
||||||
|
|
||||||
|
var repoUri = new Uri(options.RepoUrl);
|
||||||
|
var repoParts = repoUri.AbsolutePath.Trim('/').Split('/');
|
||||||
|
if (repoParts.Length != 2)
|
||||||
|
throw new Exception($"Invalid GitHub URL, '{repoUri.AbsolutePath}' should be in the format 'owner/repo'");
|
||||||
|
|
||||||
|
var repoOwner = repoParts[0];
|
||||||
|
var repoName = repoParts[1];
|
||||||
|
|
||||||
|
var client = new GitHubClient(new ProductHeaderValue("Clowd.Squirrel")) {
|
||||||
|
Credentials = new Credentials(options.Token)
|
||||||
|
};
|
||||||
|
|
||||||
|
var releasesPath = Path.Combine(releaseDirectoryInfo.FullName, "RELEASES");
|
||||||
|
if (!File.Exists(releasesPath))
|
||||||
|
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
||||||
|
|
||||||
|
var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath)).ToArray();
|
||||||
|
if (releases.Length == 0)
|
||||||
|
throw new Exception("There are no nupkg's in the releases directory to upload");
|
||||||
|
|
||||||
|
var ver = Enumerable.MaxBy(releases, x => x.Version);
|
||||||
|
if (ver == null)
|
||||||
|
throw new Exception("There are no nupkg's in the releases directory to upload");
|
||||||
|
var semVer = ver.Version;
|
||||||
|
|
||||||
|
_log.Info($"Preparing to upload latest local release to GitHub");
|
||||||
|
|
||||||
|
var newReleaseReq = new NewRelease(semVer.ToString()) {
|
||||||
|
Body = ver.GetReleaseNotes(releaseDirectoryInfo.FullName, ReleaseNotesFormat.Markdown),
|
||||||
|
Draft = true,
|
||||||
|
Prerelease = semVer.HasMetadata || semVer.IsPrerelease,
|
||||||
|
Name = string.IsNullOrWhiteSpace(options.ReleaseName)
|
||||||
|
? semVer.ToString()
|
||||||
|
: options.ReleaseName,
|
||||||
|
};
|
||||||
|
|
||||||
|
_log.Info($"Creating draft release titled '{semVer.ToString()}'");
|
||||||
|
|
||||||
|
var existingReleases = await client.Repository.Release.GetAll(repoOwner, repoName);
|
||||||
|
if (existingReleases.Any(r => r.TagName == semVer.ToString())) {
|
||||||
|
throw new Exception($"There is already an existing release tagged '{semVer}'. Please delete this release or choose a new version number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var release = await client.Repository.Release.Create(repoOwner, repoName, newReleaseReq);
|
||||||
|
|
||||||
|
// locate files to upload
|
||||||
|
var files = releaseDirectoryInfo.GetFiles("*", SearchOption.TopDirectoryOnly);
|
||||||
|
var msiFile = files.SingleOrDefault(f => f.FullName.EndsWith(".msi", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
var setupFile = files.Where(f => f.FullName.EndsWith("Setup.exe", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
.ContextualSingle("release directory", "Setup.exe file");
|
||||||
|
|
||||||
|
var releasesToUpload = releases.Where(x => x.Version == semVer).ToArray();
|
||||||
|
MemoryStream releasesFileToUpload = new MemoryStream();
|
||||||
|
ReleaseEntry.WriteReleaseFile(releasesToUpload, releasesFileToUpload);
|
||||||
|
var releasesBytes = releasesFileToUpload.ToArray();
|
||||||
|
|
||||||
|
// upload nupkg's
|
||||||
|
foreach (var r in releasesToUpload) {
|
||||||
|
var path = Path.Combine(releaseDirectoryInfo.FullName, r.Filename);
|
||||||
|
await UploadFileAsAsset(client, release, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// other files
|
||||||
|
await UploadFileAsAsset(client, release, setupFile.FullName);
|
||||||
|
if (msiFile != null) await UploadFileAsAsset(client, release, msiFile.FullName);
|
||||||
|
|
||||||
|
// RELEASES
|
||||||
|
_log.Info($"Uploading RELEASES");
|
||||||
|
var data = new ReleaseAssetUpload("RELEASES", "application/octet-stream", new MemoryStream(releasesBytes), TimeSpan.FromMinutes(1));
|
||||||
|
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
||||||
|
|
||||||
|
_log.Info($"Done creating draft GitHub release.");
|
||||||
|
|
||||||
|
// convert draft to full release
|
||||||
|
if (options.Publish) {
|
||||||
|
_log.Info("Converting draft to full published release.");
|
||||||
|
var upd = release.ToUpdate();
|
||||||
|
upd.Draft = false;
|
||||||
|
release = await client.Repository.Release.Edit(repoOwner, repoName, release.Id, upd);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Info("Release URL: " + release.HtmlUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task UploadFileAsAsset(GitHubClient client, Release release, string filePath)
|
||||||
|
{
|
||||||
|
_log.Info($"Uploading asset '{Path.GetFileName(filePath)}'");
|
||||||
|
using var stream = File.OpenRead(filePath);
|
||||||
|
var data = new ReleaseAssetUpload(Path.GetFileName(filePath), "application/octet-stream", stream, TimeSpan.FromMinutes(30));
|
||||||
|
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/Squirrel.Deployment/Squirrel.Deployment.csproj
Normal file
26
src/Squirrel.Deployment/Squirrel.Deployment.csproj
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AWSSDK.S3" Version="3.7.205.22" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="Octokit" Version="9.0.0" />
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
|
<PackageReference Include="System.IO" Version="4.3.0" />
|
||||||
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Algorithms" Version="4.3.1" />
|
||||||
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
|
<PackageReference Include="NuGet.Commands" Version="6.7.0" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||||
|
<PackageReference Include="PeNet" Version="4.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
8
src/Squirrel.Packaging.OSX/Squirrel.Packaging.OSX.csproj
Normal file
8
src/Squirrel.Packaging.OSX/Squirrel.Packaging.OSX.csproj
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,423 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
using NuGet.Versioning;
|
|
||||||
using Squirrel.NuGet;
|
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A base class describing where Squirrel can find key folders and files.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class AppDesc : IEnableLogger
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Auto-detect the platform from the current operating system.
|
|
||||||
/// </summary>
|
|
||||||
public static AppDesc GetCurrentPlatform()
|
|
||||||
{
|
|
||||||
if (SquirrelRuntimeInfo.IsWindows)
|
|
||||||
return new AppDescWindows();
|
|
||||||
|
|
||||||
if (SquirrelRuntimeInfo.IsOSX)
|
|
||||||
return new AppDescOsx();
|
|
||||||
|
|
||||||
throw new NotSupportedException($"OS platform '{SquirrelRuntimeInfo.SystemOs.GetOsLongName()}' is not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Instantiate base class <see cref="AppDesc"/>.
|
|
||||||
/// </summary>
|
|
||||||
protected AppDesc()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> The unique application Id. This is used in various app paths. </summary>
|
|
||||||
public abstract string AppId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The root directory of the application. On Windows, this folder contains all
|
|
||||||
/// the application files, but that may not be the case on other operating systems.
|
|
||||||
/// </summary>
|
|
||||||
public abstract string RootAppDir { get; }
|
|
||||||
|
|
||||||
/// <summary> The directory in which nupkg files are stored for this application. </summary>
|
|
||||||
public abstract string PackagesDir { get; }
|
|
||||||
|
|
||||||
/// <summary> The temporary directory for this application. </summary>
|
|
||||||
public abstract string AppTempDir { get; }
|
|
||||||
|
|
||||||
/// <summary> True if the current binary is Update.exe within the specified application. </summary>
|
|
||||||
public abstract bool IsUpdateExe { get; }
|
|
||||||
|
|
||||||
/// <summary> The directory where new versions are stored, before they are applied. </summary>
|
|
||||||
public abstract string VersionStagingDir { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The directory where the current version of the application is stored.
|
|
||||||
/// This directory will be swapped out for a new version in <see cref="VersionStagingDir"/>.
|
|
||||||
/// </summary>
|
|
||||||
public abstract string CurrentVersionDir { get; }
|
|
||||||
|
|
||||||
/// <summary> The path to the current Update.exe or similar on other operating systems. </summary>
|
|
||||||
public abstract string UpdateExePath { get; }
|
|
||||||
|
|
||||||
/// <summary> The path to the RELEASES index detailing the local packages. </summary>
|
|
||||||
public virtual string ReleasesFilePath => Path.Combine(PackagesDir, "RELEASES");
|
|
||||||
|
|
||||||
/// <summary> The path to the .betaId file which contains a unique GUID for this user. </summary>
|
|
||||||
public virtual string BetaIdFilePath => Path.Combine(PackagesDir, ".betaId");
|
|
||||||
|
|
||||||
/// <summary> The currently installed version of the application. </summary>
|
|
||||||
public abstract SemanticVersion CurrentlyInstalledVersion { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="version">The application version</param>
|
|
||||||
/// <returns>The full path to the version staging directory</returns>
|
|
||||||
public virtual string GetVersionStagingPath(SemanticVersion version)
|
|
||||||
{
|
|
||||||
return Path.Combine(VersionStagingDir, "app-" + version);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal List<(string PackagePath, SemanticVersion PackageVersion, bool IsDelta)> GetLocalPackages()
|
|
||||||
{
|
|
||||||
var query = from x in Directory.EnumerateFiles(PackagesDir, "*.nupkg")
|
|
||||||
let re = ReleaseEntry.ParseEntryFileName(x)
|
|
||||||
where re.Version != null
|
|
||||||
select (x, re.Version, re.IsDelta);
|
|
||||||
return query.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal string UpdateAndRetrieveCurrentFolder(bool force)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
var releases = GetVersions();
|
|
||||||
var latestVer = releases.OrderByDescending(m => m.Version).First();
|
|
||||||
var currentVer = releases.FirstOrDefault(f => f.IsCurrent);
|
|
||||||
|
|
||||||
// if the latest ver is already current, or it does not support
|
|
||||||
// being in a current directory.
|
|
||||||
if (latestVer.IsCurrent) {
|
|
||||||
this.Log().Info($"Current directory already pointing to latest version.");
|
|
||||||
return latestVer.DirectoryPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (force) {
|
|
||||||
PlatformUtil.KillProcessesInDirectory(RootAppDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'current' does exist, and it's wrong, so lets get rid of it
|
|
||||||
if (currentVer != default) {
|
|
||||||
string legacyVersionDir = GetVersionStagingPath(currentVer.Version);
|
|
||||||
this.Log().Info($"Moving '{currentVer.DirectoryPath}' to '{legacyVersionDir}'.");
|
|
||||||
Utility.Retry(() => Directory.Move(currentVer.DirectoryPath, legacyVersionDir));
|
|
||||||
}
|
|
||||||
|
|
||||||
// this directory does not support being named 'current'
|
|
||||||
if (latestVer.Manifest == null) {
|
|
||||||
this.Log().Info($"Cannot promote {latestVer.Version} as current as it has no manifest");
|
|
||||||
return latestVer.DirectoryPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'current' doesn't exist right now, lets move the latest version
|
|
||||||
var latestDir = CurrentVersionDir;
|
|
||||||
this.Log().Info($"Moving '{latestVer.DirectoryPath}' to '{latestDir}'.");
|
|
||||||
Utility.DeleteFileOrDirectoryHard(latestDir, renameFirst: true, throwOnFailure: false);
|
|
||||||
Utility.Retry(() => Directory.Move(latestVer.DirectoryPath, latestDir));
|
|
||||||
|
|
||||||
this.Log().Info("Current app is now: " + latestDir);
|
|
||||||
return latestDir;
|
|
||||||
} catch (Exception e) {
|
|
||||||
var releases = GetVersions();
|
|
||||||
string fallback = releases.OrderByDescending(m => m.Version).First().DirectoryPath;
|
|
||||||
var currentVer = releases.FirstOrDefault(f => f.IsCurrent);
|
|
||||||
if (currentVer != default && Directory.Exists(currentVer.DirectoryPath)) {
|
|
||||||
fallback = currentVer.DirectoryPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.Log().WarnException("Unable to update 'current' directory", e);
|
|
||||||
this.Log().Info("Running app in: " + fallback);
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Given a base dir and a directory name, will create a new sub directory of that name.
|
|
||||||
/// Will return null if baseDir is null, or if baseDir does not exist.
|
|
||||||
/// </summary>
|
|
||||||
protected static string CreateSubDirIfDoesNotExist(string baseDir, string newDir)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrEmpty(baseDir) || string.IsNullOrEmpty(newDir)) return null;
|
|
||||||
var infoBase = new DirectoryInfo(baseDir);
|
|
||||||
if (!infoBase.Exists) return null;
|
|
||||||
var info = new DirectoryInfo(Path.Combine(baseDir, newDir));
|
|
||||||
if (!info.Exists) info.Create();
|
|
||||||
return info.FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Starts Update.exe with the correct arguments to restart this process.
|
|
||||||
/// Update.exe will wait for this process to exit, and apply any pending version updates
|
|
||||||
/// before re-launching the latest version.
|
|
||||||
/// </summary>
|
|
||||||
public virtual Process StartRestartingProcess(string exeToStart = null, string arguments = null)
|
|
||||||
{
|
|
||||||
// NB: Here's how this method works:
|
|
||||||
//
|
|
||||||
// 1. We're going to pass the *name* of our EXE and the params to
|
|
||||||
// Update.exe
|
|
||||||
// 2. Update.exe is going to grab our PID (via getting its parent),
|
|
||||||
// then wait for us to exit.
|
|
||||||
// 3. Return control and new Process back to caller and allow them to Exit as desired.
|
|
||||||
// 4. After our process exits, Update.exe unblocks, then we launch the app again, possibly
|
|
||||||
// launching a different version than we started with (this is why
|
|
||||||
// we take the app's *name* rather than a full path)
|
|
||||||
|
|
||||||
exeToStart = exeToStart ?? Path.GetFileName(SquirrelRuntimeInfo.EntryExePath);
|
|
||||||
|
|
||||||
List<string> args = new() {
|
|
||||||
"--forceLatest",
|
|
||||||
"--processStartAndWait",
|
|
||||||
exeToStart,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (arguments != null) {
|
|
||||||
args.Add("-a");
|
|
||||||
args.Add(arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PlatformUtil.StartProcessNonBlocking(UpdateExePath, args, Path.GetDirectoryName(UpdateExePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal VersionDirInfo GetLatestVersion()
|
|
||||||
{
|
|
||||||
return GetLatestVersion(GetVersions());
|
|
||||||
}
|
|
||||||
|
|
||||||
internal VersionDirInfo GetLatestVersion(IEnumerable<VersionDirInfo> versions)
|
|
||||||
{
|
|
||||||
return versions.OrderByDescending(r => r.Version).FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal VersionDirInfo GetVersionInfoFromDirectory(string d)
|
|
||||||
{
|
|
||||||
bool isCurrent = CurrentVersionDir != null ? Utility.FullPathEquals(d, CurrentVersionDir) : false;
|
|
||||||
var directoryName = Path.GetFileName(d);
|
|
||||||
bool isExecuting = Utility.IsFileInDirectory(SquirrelRuntimeInfo.EntryExePath, d);
|
|
||||||
var manifest = Utility.ReadManifestFromVersionDir(d);
|
|
||||||
|
|
||||||
if (manifest != null) {
|
|
||||||
return new(manifest, manifest.Version, d, isCurrent, isExecuting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Utility.PathPartStartsWith(directoryName, "app-") && NuGetVersion.TryParse(directoryName.Substring(4), out var ver)) {
|
|
||||||
return new(null, ver, d, isCurrent, isExecuting);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal record VersionDirInfo(IPackage Manifest, SemanticVersion Version, string DirectoryPath, bool IsCurrent, bool IsExecuting);
|
|
||||||
|
|
||||||
internal VersionDirInfo[] GetVersions()
|
|
||||||
{
|
|
||||||
List<string> directories = new List<string>() { CurrentVersionDir };
|
|
||||||
if (Directory.Exists(RootAppDir))
|
|
||||||
directories.AddRange(Directory.GetDirectories(RootAppDir, "app-*", SearchOption.TopDirectoryOnly));
|
|
||||||
|
|
||||||
if (Directory.Exists(VersionStagingDir))
|
|
||||||
directories.AddRange(Directory.GetDirectories(VersionStagingDir, "app-*", SearchOption.TopDirectoryOnly));
|
|
||||||
|
|
||||||
return directories
|
|
||||||
.Where(Directory.Exists)
|
|
||||||
.Select(Utility.NormalizePath)
|
|
||||||
.Distinct(SquirrelRuntimeInfo.PathStringComparer)
|
|
||||||
.Select(GetVersionInfoFromDirectory)
|
|
||||||
.Where(d => d != null)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An implementation for Windows which uses the Squirrel defaults and installs to
|
|
||||||
/// local app data.
|
|
||||||
/// </summary>
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
public class AppDescWindows : AppDesc
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string AppId { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string RootAppDir { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string UpdateExePath { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool IsUpdateExe { get; }
|
|
||||||
|
|
||||||
/// <summary> True if Update.exe is currently performing first app install. </summary>
|
|
||||||
public bool IsInstalling { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override SemanticVersion CurrentlyInstalledVersion { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string PackagesDir => CreateSubDirIfDoesNotExist(RootAppDir, "packages");
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string AppTempDir => CreateSubDirIfDoesNotExist(PackagesDir, "SquirrelClowdTemp");
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string VersionStagingDir => CreateSubDirIfDoesNotExist(RootAppDir, "staging");
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string CurrentVersionDir => CreateSubDirIfDoesNotExist(RootAppDir, "current");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new Platform and tries to auto-detect the application details from
|
|
||||||
/// the current context.
|
|
||||||
/// </summary>
|
|
||||||
public AppDescWindows() : this(SquirrelRuntimeInfo.EntryExePath)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Internal use only. Creates a AppDescWindows from the following rootAppDir and
|
|
||||||
/// does not perform any path auto-detection.
|
|
||||||
/// </summary>
|
|
||||||
internal AppDescWindows(string rootAppDir, string appId, bool isInstalling = false)
|
|
||||||
{
|
|
||||||
AppId = appId;
|
|
||||||
RootAppDir = rootAppDir;
|
|
||||||
var updateExe = Path.Combine(rootAppDir, "Update.exe");
|
|
||||||
UpdateExePath = updateExe;
|
|
||||||
IsUpdateExe = Utility.FullPathEquals(updateExe, SquirrelRuntimeInfo.EntryExePath);
|
|
||||||
CurrentlyInstalledVersion = GetLatestVersion()?.Version;
|
|
||||||
IsInstalling = isInstalling;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Internal use only. Auto detect app details from the specified EXE path.
|
|
||||||
/// </summary>
|
|
||||||
internal AppDescWindows(string ourExePath)
|
|
||||||
{
|
|
||||||
if (!SquirrelRuntimeInfo.IsWindows)
|
|
||||||
throw new NotSupportedException("Cannot instantiate AppDescWindows on a non-Windows system.");
|
|
||||||
|
|
||||||
ourExePath = Path.GetFullPath(ourExePath);
|
|
||||||
var myDir = Path.GetDirectoryName(ourExePath);
|
|
||||||
|
|
||||||
// Am I update.exe at the application root?
|
|
||||||
if (ourExePath != null &&
|
|
||||||
Path.GetFileName(ourExePath).Equals("update.exe", StringComparison.InvariantCultureIgnoreCase) &&
|
|
||||||
ourExePath.IndexOf("app-", StringComparison.InvariantCultureIgnoreCase) == -1 &&
|
|
||||||
ourExePath.IndexOf("SquirrelClowdTemp", StringComparison.InvariantCultureIgnoreCase) == -1) {
|
|
||||||
UpdateExePath = ourExePath;
|
|
||||||
RootAppDir = myDir;
|
|
||||||
var ver = GetLatestVersion();
|
|
||||||
if (ver != null) {
|
|
||||||
AppId = ver.Manifest?.Id ?? Path.GetFileName(myDir);
|
|
||||||
CurrentlyInstalledVersion = ver.Version;
|
|
||||||
IsUpdateExe = true;
|
|
||||||
} else {
|
|
||||||
UpdateExePath = null;
|
|
||||||
RootAppDir = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Am I running from within an app-* or current dir?
|
|
||||||
// 'info' will be null in any portable / non-installed app.
|
|
||||||
var info = GetVersionInfoFromDirectory(myDir);
|
|
||||||
if (info != null) {
|
|
||||||
var updateExe = Path.Combine(myDir, "..\\Update.exe");
|
|
||||||
var updateExe2 = Path.Combine(myDir, "..\\..\\Update.exe");
|
|
||||||
string updateLocation = null;
|
|
||||||
|
|
||||||
if (File.Exists(updateExe)) {
|
|
||||||
updateLocation = Path.GetFullPath(updateExe);
|
|
||||||
} else if (File.Exists(updateExe2)) {
|
|
||||||
updateLocation = Path.GetFullPath(updateExe2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateLocation != null) {
|
|
||||||
RootAppDir = Path.GetDirectoryName(updateLocation);
|
|
||||||
UpdateExePath = updateLocation;
|
|
||||||
AppId = info.Manifest?.Id ?? Path.GetFileName(Path.GetDirectoryName(updateLocation));
|
|
||||||
CurrentlyInstalledVersion = info.Version;
|
|
||||||
IsUpdateExe = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The default for OSX. All application files will remain in the '.app'.
|
|
||||||
/// All additional files (log, etc) will be placed in a temporary directory.
|
|
||||||
/// </summary>
|
|
||||||
[SupportedOSPlatform("osx")]
|
|
||||||
public class AppDescOsx : AppDesc
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string AppId { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string RootAppDir { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string UpdateExePath { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool IsUpdateExe { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override SemanticVersion CurrentlyInstalledVersion { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string CurrentVersionDir => RootAppDir;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string AppTempDir => CreateSubDirIfDoesNotExist(Utility.GetDefaultTempBaseDirectory(), AppId);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string PackagesDir => CreateSubDirIfDoesNotExist(AppTempDir, "packages");
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string VersionStagingDir => CreateSubDirIfDoesNotExist(AppTempDir, "staging");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new <see cref="AppDescOsx"/> and auto-detects the
|
|
||||||
/// app information from metadata embedded in the .app.
|
|
||||||
/// </summary>
|
|
||||||
public AppDescOsx()
|
|
||||||
{
|
|
||||||
if (!SquirrelRuntimeInfo.IsOSX)
|
|
||||||
throw new NotSupportedException("Cannot instantiate AppDescOsx on a non-osx system.");
|
|
||||||
|
|
||||||
// are we inside a .app?
|
|
||||||
var ourPath = SquirrelRuntimeInfo.EntryExePath;
|
|
||||||
var ix = ourPath.IndexOf(".app/", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
if (ix < 0) return;
|
|
||||||
|
|
||||||
var appPath = ourPath.Substring(0, ix + 4);
|
|
||||||
var contentsDir = Path.Combine(appPath, "Contents");
|
|
||||||
var updateExe = Path.Combine(contentsDir, "UpdateMac");
|
|
||||||
var info = GetVersionInfoFromDirectory(contentsDir);
|
|
||||||
|
|
||||||
if (File.Exists(updateExe) && info?.Manifest != null) {
|
|
||||||
AppId = info.Manifest.Id;
|
|
||||||
RootAppDir = appPath;
|
|
||||||
UpdateExePath = updateExe;
|
|
||||||
CurrentlyInstalledVersion = info.Version;
|
|
||||||
IsUpdateExe = Utility.FullPathEquals(updateExe, ourPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
using System;
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
|
||||||
namespace Squirrel
|
namespace Squirrel.Compression
|
||||||
{
|
{
|
||||||
internal sealed class BZip2Stream : Stream
|
public sealed class BZip2Stream : Stream
|
||||||
{
|
{
|
||||||
private readonly Stream stream;
|
private readonly Stream stream;
|
||||||
private bool isDisposed;
|
private bool isDisposed;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
using System;
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
// Adapted from https://github.com/LogosBible/bsdiff.net/blob/master/src/bsdiff/BinaryPatchUtility.cs
|
// Adapted from https://github.com/LogosBible/bsdiff.net/blob/master/src/bsdiff/BinaryPatchUtility.cs
|
||||||
|
|
||||||
namespace Squirrel.Bsdiff
|
namespace Squirrel.Compression
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
The original bsdiff.c source code (http://www.daemonology.net/bsdiff/) is
|
The original bsdiff.c source code (http://www.daemonology.net/bsdiff/) is
|
||||||
@@ -35,7 +36,7 @@ namespace Squirrel.Bsdiff
|
|||||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
class BinaryPatchUtility
|
public class BinaryPatchUtility
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used
|
/// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Squirrel
|
namespace Squirrel.Compression
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an error that occurs when a package does not match it's expected SHA checksum
|
/// Represents an error that occurs when a package does not match it's expected SHA checksum
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Contracts;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Squirrel.Bsdiff;
|
using Microsoft.Extensions.Logging;
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel
|
namespace Squirrel.Compression
|
||||||
{
|
{
|
||||||
internal class DeltaPackage : IEnableLogger
|
public class DeltaPackage
|
||||||
{
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
private readonly string _baseTempDir;
|
private readonly string _baseTempDir;
|
||||||
|
|
||||||
public DeltaPackage(string baseTempDir = null)
|
public DeltaPackage(ILogger logger, string baseTempDir = null)
|
||||||
{
|
{
|
||||||
|
_log = logger;
|
||||||
_baseTempDir = baseTempDir ?? Utility.GetDefaultTempBaseDirectory();
|
_baseTempDir = baseTempDir ?? Utility.GetDefaultTempBaseDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,15 +24,16 @@ namespace Squirrel
|
|||||||
{
|
{
|
||||||
progress = progress ?? (x => { });
|
progress = progress ?? (x => { });
|
||||||
|
|
||||||
Contract.Requires(deltaPackageZip != null);
|
if (deltaPackageZip is null) throw new ArgumentNullException(nameof(deltaPackageZip));
|
||||||
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
|
if (String.IsNullOrEmpty(outputFile)) throw new ArgumentNullException(nameof(outputFile));
|
||||||
|
if (File.Exists(outputFile)) throw new ArgumentException("File already exists", nameof(outputFile));
|
||||||
|
|
||||||
using (Utility.GetTempDirectory(out var deltaPath, _baseTempDir))
|
using (Utility.GetTempDirectory(out var deltaPath, _baseTempDir))
|
||||||
using (Utility.GetTempDirectory(out var workingPath, _baseTempDir)) {
|
using (Utility.GetTempDirectory(out var workingPath, _baseTempDir)) {
|
||||||
EasyZip.ExtractZipToDirectory(deltaPackageZip, deltaPath);
|
EasyZip.ExtractZipToDirectory(_log, deltaPackageZip, deltaPath);
|
||||||
progress(25);
|
progress(25);
|
||||||
|
|
||||||
EasyZip.ExtractZipToDirectory(basePackageZip, workingPath);
|
EasyZip.ExtractZipToDirectory(_log, basePackageZip, workingPath);
|
||||||
progress(50);
|
progress(50);
|
||||||
|
|
||||||
var pathsVisited = new List<string>();
|
var pathsVisited = new List<string>();
|
||||||
@@ -59,7 +61,7 @@ namespace Squirrel
|
|||||||
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
|
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
|
||||||
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
|
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
|
||||||
.ForEach(x => {
|
.ForEach(x => {
|
||||||
this.Log().Info("{0} was in old package but not in new one, deleting", x);
|
_log.Info($"{x} was in old package but not in new one, deleting");
|
||||||
File.Delete(Path.Combine(workingPath, x));
|
File.Delete(Path.Combine(workingPath, x));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,13 +72,13 @@ namespace Squirrel
|
|||||||
deltaPathRelativePaths
|
deltaPathRelativePaths
|
||||||
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
|
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
|
||||||
.ForEach(x => {
|
.ForEach(x => {
|
||||||
this.Log().Info("Updating metadata file: {0}", x);
|
_log.Info($"Updating metadata file: {x}");
|
||||||
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
|
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.Log().Info("Repacking into full package: {0}", outputFile);
|
_log.Info($"Repacking into full package: {outputFile}");
|
||||||
|
|
||||||
EasyZip.CreateZipFromDirectory(outputFile, workingPath);
|
EasyZip.CreateZipFromDirectory(_log, outputFile, workingPath);
|
||||||
|
|
||||||
progress(100);
|
progress(100);
|
||||||
}
|
}
|
||||||
@@ -88,10 +90,10 @@ namespace Squirrel
|
|||||||
{
|
{
|
||||||
progress = progress ?? (x => { });
|
progress = progress ?? (x => { });
|
||||||
|
|
||||||
Contract.Requires(deltaPackageZip != null);
|
if (deltaPackageZip is null) throw new ArgumentNullException(nameof(deltaPackageZip));
|
||||||
|
|
||||||
using var _1 = Utility.GetTempDirectory(out var deltaPath, _baseTempDir);
|
using var _1 = Utility.GetTempDirectory(out var deltaPath, _baseTempDir);
|
||||||
EasyZip.ExtractZipToDirectory(deltaPackageZip, deltaPath);
|
EasyZip.ExtractZipToDirectory(_log, deltaPackageZip, deltaPath);
|
||||||
progress(10);
|
progress(10);
|
||||||
|
|
||||||
var pathsVisited = new List<string>();
|
var pathsVisited = new List<string>();
|
||||||
@@ -112,8 +114,8 @@ namespace Squirrel
|
|||||||
var file = files[index];
|
var file = files[index];
|
||||||
pathsVisited.Add(Regex.Replace(file, @"\.(bs)?diff$", "").ToLowerInvariant());
|
pathsVisited.Add(Regex.Replace(file, @"\.(bs)?diff$", "").ToLowerInvariant());
|
||||||
applyDiffToFile(deltaPath, file, workingPath);
|
applyDiffToFile(deltaPath, file, workingPath);
|
||||||
var perc = (index + 1) / (double)files.Length * 100;
|
var perc = (index + 1) / (double) files.Length * 100;
|
||||||
Utility.CalculateProgress((int)perc, 10, 90);
|
Utility.CalculateProgress((int) perc, 10, 90);
|
||||||
}
|
}
|
||||||
|
|
||||||
progress(90);
|
progress(90);
|
||||||
@@ -124,7 +126,7 @@ namespace Squirrel
|
|||||||
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
|
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
|
||||||
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
|
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
|
||||||
.ForEach(x => {
|
.ForEach(x => {
|
||||||
this.Log().Info("{0} was in old package but not in new one, deleting", x);
|
_log.Info($"{x} was in old package but not in new one, deleting");
|
||||||
File.Delete(Path.Combine(workingPath, x));
|
File.Delete(Path.Combine(workingPath, x));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,7 +137,7 @@ namespace Squirrel
|
|||||||
deltaPathRelativePaths
|
deltaPathRelativePaths
|
||||||
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
|
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
|
||||||
.ForEach(x => {
|
.ForEach(x => {
|
||||||
this.Log().Info("Updating metadata file: {0}", x);
|
_log.Info($"Updating metadata file: {x}");
|
||||||
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
|
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,20 +153,20 @@ namespace Squirrel
|
|||||||
|
|
||||||
// NB: Zero-length diffs indicate the file hasn't actually changed
|
// NB: Zero-length diffs indicate the file hasn't actually changed
|
||||||
if (new FileInfo(inputFile).Length == 0) {
|
if (new FileInfo(inputFile).Length == 0) {
|
||||||
this.Log().Info("{0} exists unchanged, skipping", relativeFilePath);
|
_log.Info($"{relativeFilePath} exists unchanged, skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relativeFilePath.EndsWith(".bsdiff", StringComparison.InvariantCultureIgnoreCase)) {
|
if (relativeFilePath.EndsWith(".bsdiff", StringComparison.InvariantCultureIgnoreCase)) {
|
||||||
using (var of = File.OpenWrite(tempTargetFile))
|
using (var of = File.OpenWrite(tempTargetFile))
|
||||||
using (var inf = File.OpenRead(finalTarget)) {
|
using (var inf = File.OpenRead(finalTarget)) {
|
||||||
this.Log().Info("Applying bsdiff to {0}", relativeFilePath);
|
_log.Info($"Applying bsdiff to {relativeFilePath}");
|
||||||
BinaryPatchUtility.Apply(inf, () => File.OpenRead(inputFile), of);
|
BinaryPatchUtility.Apply(inf, () => File.OpenRead(inputFile), of);
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile);
|
verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile);
|
||||||
} else if (relativeFilePath.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase)) {
|
} else if (relativeFilePath.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase)) {
|
||||||
this.Log().Info("Applying msdiff to {0}", relativeFilePath);
|
_log.Info($"Applying msdiff to {relativeFilePath}");
|
||||||
|
|
||||||
if (SquirrelRuntimeInfo.IsWindows) {
|
if (SquirrelRuntimeInfo.IsWindows) {
|
||||||
MsDeltaCompression.ApplyDelta(inputFile, finalTarget, tempTargetFile);
|
MsDeltaCompression.ApplyDelta(inputFile, finalTarget, tempTargetFile);
|
||||||
@@ -176,7 +178,7 @@ namespace Squirrel
|
|||||||
} else {
|
} else {
|
||||||
using (var of = File.OpenWrite(tempTargetFile))
|
using (var of = File.OpenWrite(tempTargetFile))
|
||||||
using (var inf = File.OpenRead(inputFile)) {
|
using (var inf = File.OpenRead(inputFile)) {
|
||||||
this.Log().Info("Adding new file: {0}", relativeFilePath);
|
_log.Info($"Adding new file: {relativeFilePath}");
|
||||||
inf.CopyTo(of);
|
inf.CopyTo(of);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,14 +198,12 @@ namespace Squirrel
|
|||||||
var actualReleaseEntry = ReleaseEntry.GenerateFromFile(tempTargetFile);
|
var actualReleaseEntry = ReleaseEntry.GenerateFromFile(tempTargetFile);
|
||||||
|
|
||||||
if (expectedReleaseEntry.Filesize != actualReleaseEntry.Filesize) {
|
if (expectedReleaseEntry.Filesize != actualReleaseEntry.Filesize) {
|
||||||
this.Log().Warn("Patched file {0} has incorrect size, expected {1}, got {2}", relativeFilePath,
|
_log.Warn($"Patched file {relativeFilePath} has incorrect size, expected {expectedReleaseEntry.Filesize}, got {actualReleaseEntry.Filesize}");
|
||||||
expectedReleaseEntry.Filesize, actualReleaseEntry.Filesize);
|
|
||||||
throw new ChecksumFailedException() { Filename = relativeFilePath };
|
throw new ChecksumFailedException() { Filename = relativeFilePath };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expectedReleaseEntry.SHA1 != actualReleaseEntry.SHA1) {
|
if (expectedReleaseEntry.SHA1 != actualReleaseEntry.SHA1) {
|
||||||
this.Log().Warn("Patched file {0} has incorrect SHA1, expected {1}, got {2}", relativeFilePath,
|
_log.Warn($"Patched file {relativeFilePath} has incorrect SHA1, expected {expectedReleaseEntry.SHA1}, got {actualReleaseEntry.SHA1}");
|
||||||
expectedReleaseEntry.SHA1, actualReleaseEntry.SHA1);
|
|
||||||
throw new ChecksumFailedException() { Filename = relativeFilePath };
|
throw new ChecksumFailedException() { Filename = relativeFilePath };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
41
src/Squirrel/Compression/EasyZip.cs
Normal file
41
src/Squirrel/Compression/EasyZip.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
using System;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Squirrel.Compression
|
||||||
|
{
|
||||||
|
public static class EasyZip
|
||||||
|
{
|
||||||
|
public static void ExtractZipToDirectory(ILogger logger, string inputFile, string outputDirectory)
|
||||||
|
{
|
||||||
|
logger.Info($"Extracting '{inputFile}' to '{outputDirectory}' using System.IO.Compression...");
|
||||||
|
Utility.DeleteFileOrDirectoryHard(outputDirectory);
|
||||||
|
ZipFile.ExtractToDirectory(inputFile, outputDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateZipFromDirectory(ILogger logger, string outputFile, string directoryToCompress)
|
||||||
|
{
|
||||||
|
logger.Info($"Compressing '{directoryToCompress}' to '{outputFile}' using System.IO.Compression...");
|
||||||
|
ZipFile.CreateFromDirectory(directoryToCompress, outputFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
//private static void AddAllFromDirectoryInNestedDir(
|
||||||
|
// IWritableArchive writableArchive,
|
||||||
|
// string filePath, string searchPattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories)
|
||||||
|
//{
|
||||||
|
// var di = new DirectoryInfo(filePath);
|
||||||
|
// var parent = di.Parent;
|
||||||
|
|
||||||
|
// using (writableArchive.PauseEntryRebuilding())
|
||||||
|
// {
|
||||||
|
// foreach (var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption))
|
||||||
|
// {
|
||||||
|
// var fileInfo = new FileInfo(path);
|
||||||
|
// writableArchive.AddEntry(fileInfo.FullName.Substring(parent.FullName.Length), fileInfo.OpenRead(), true, fileInfo.Length,
|
||||||
|
// fileInfo.LastWriteTime);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
#nullable enable
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
|
|
||||||
namespace Squirrel
|
namespace Squirrel.Compression
|
||||||
{
|
{
|
||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
internal class MsDeltaCompression
|
public class MsDeltaCompression
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ApplyDelta function use the specified delta and source files to create a new copy of the target file.
|
/// The ApplyDelta function use the specified delta and source files to create a new copy of the target file.
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
using NuGet.Versioning;
|
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies several common places where shortcuts can be installed on a user's system
|
|
||||||
/// </summary>
|
|
||||||
[Flags]
|
|
||||||
public enum ShortcutLocation
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A shortcut in ProgramFiles within a publisher sub-directory
|
|
||||||
/// </summary>
|
|
||||||
StartMenu = 1 << 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A shortcut on the current user desktop
|
|
||||||
/// </summary>
|
|
||||||
Desktop = 1 << 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A shortcut in Startup/Run folder will cause the app to be automatially started on user login.
|
|
||||||
/// </summary>
|
|
||||||
Startup = 1 << 2,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A shortcut in the application folder, useful for portable applications.
|
|
||||||
/// </summary>
|
|
||||||
AppRoot = 1 << 3,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A shortcut in ProgramFiles root folder (not in a company/publisher sub-directory). This is commonplace as of more recent versions of windows.
|
|
||||||
/// </summary>
|
|
||||||
StartMenuRoot = 1 << 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates whether the UpdateManager is used in a Install or Update scenario.
|
|
||||||
/// </summary>
|
|
||||||
public enum UpdaterIntention
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The current intent is to perform a full app install, and overwrite or
|
|
||||||
/// repair any app already installed of the same name.
|
|
||||||
/// </summary>
|
|
||||||
Install,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The current intent is to perform an app update, and to do nothing if there
|
|
||||||
/// is no newer version available to install.
|
|
||||||
/// </summary>
|
|
||||||
Update
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides update functionality to applications, and general helper
|
|
||||||
/// functions for managing installed shortcuts and registry entries. Use this
|
|
||||||
/// to check if the current app is installed or not before performing an update.
|
|
||||||
/// </summary>
|
|
||||||
public interface IUpdateManager : IDisposable, IEnableLogger, IAppTools
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Fetch the remote store for updates and compare against the current
|
|
||||||
/// version to determine what updates to download.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="intention">Indicates whether the UpdateManager is used
|
|
||||||
/// in a Install or Update scenario.</param>
|
|
||||||
/// <param name="ignoreDeltaUpdates">Set this flag if applying a release
|
|
||||||
/// fails to fall back to a full release, which takes longer to download
|
|
||||||
/// but is less error-prone.</param>
|
|
||||||
/// <param name="progress">A Observer which can be used to report Progress -
|
|
||||||
/// will return values from 0-100 and Complete, or Throw</param>
|
|
||||||
/// <returns>An UpdateInfo object representing the updates to install.
|
|
||||||
/// </returns>
|
|
||||||
Task<UpdateInfo> CheckForUpdate(bool ignoreDeltaUpdates = false, Action<int> progress = null, UpdaterIntention intention = UpdaterIntention.Update);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Download a list of releases into the local package directory.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="releasesToDownload">The list of releases to download,
|
|
||||||
/// almost always from UpdateInfo.ReleasesToApply.</param>
|
|
||||||
/// <param name="progress">A Observer which can be used to report Progress -
|
|
||||||
/// will return values from 0-100 and Complete, or Throw</param>
|
|
||||||
/// <returns>A completion Observable - either returns a single
|
|
||||||
/// Unit.Default then Complete, or Throw</returns>
|
|
||||||
Task DownloadReleases(IEnumerable<ReleaseEntry> releasesToDownload, Action<int> progress = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Take an already downloaded set of releases and apply them,
|
|
||||||
/// copying in the new files from the NuGet package and rewriting
|
|
||||||
/// the application shortcuts.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="updateInfo">The UpdateInfo instance acquired from
|
|
||||||
/// CheckForUpdate</param>
|
|
||||||
/// <param name="progress">A Observer which can be used to report Progress -
|
|
||||||
/// will return values from 0-100 and Complete, or Throw</param>
|
|
||||||
/// <returns>The path to the installed application (i.e. the path where
|
|
||||||
/// your package's contents ended up</returns>
|
|
||||||
Task<string> ApplyReleases(UpdateInfo updateInfo, Action<int> progress = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This will check for updates, download any new available updates, and apply those
|
|
||||||
/// updates in a single step. The same task can be accomplished by using <see cref="IUpdateManager.CheckForUpdate"/>,
|
|
||||||
/// followed by <see cref="IUpdateManager.DownloadReleases"/> and <see cref="IUpdateManager.ApplyReleases"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The installed update, or null if there were no updates available</returns>
|
|
||||||
Task<ReleaseEntry> UpdateApp(Action<int> progress = null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides accessory functions such as managing uninstall registry or
|
|
||||||
/// creating, updating, and removing shortcuts.
|
|
||||||
/// </summary>
|
|
||||||
public interface IAppTools
|
|
||||||
{
|
|
||||||
/// <summary>True if the current executable is inside the target <see cref="AppDirectory"/>.</summary>
|
|
||||||
bool IsInstalledApp { get; }
|
|
||||||
|
|
||||||
/// <summary>The directory the app is (or will be) installed in.</summary>
|
|
||||||
string AppDirectory { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the currently installed version of the given executable, or if
|
|
||||||
/// not given, the currently running assembly
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The running version, or null if this is not a Squirrel
|
|
||||||
/// installed app (i.e. you're running from VS)</returns>
|
|
||||||
SemanticVersion CurrentlyInstalledVersion();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a shortcut on the Desktop / Start Menu for the given
|
|
||||||
/// executable. Metadata from the currently installed NuGet package
|
|
||||||
/// and information from the Version Header of the EXE will be used
|
|
||||||
/// to construct the shortcut folder / name.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exeName">The name of the executable, relative to the
|
|
||||||
/// app install directory.</param>
|
|
||||||
/// <param name="locations">The locations to install the shortcut</param>
|
|
||||||
/// <param name="updateOnly">Set to false during initial install, true
|
|
||||||
/// during app update.</param>
|
|
||||||
/// <param name="programArguments">The arguments to code into the shortcut</param>
|
|
||||||
/// <param name="icon">The shortcut icon</param>
|
|
||||||
void CreateShortcutsForExecutable(string exeName, ShortcutLocation locations, bool updateOnly, string programArguments, string icon);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes shortcuts created by CreateShortcutsForExecutable
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exeName">The name of the executable, relative to the
|
|
||||||
/// app install directory.</param>
|
|
||||||
/// <param name="locations">The locations to install the shortcut</param>
|
|
||||||
void RemoveShortcutsForExecutable(string exeName, ShortcutLocation locations);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains extension methods for <see cref="IUpdateManager"/> which provide simplified functionality
|
|
||||||
/// </summary>
|
|
||||||
public static class EasyModeMixin
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Create a shortcut to the currently running executable at the specified locations.
|
|
||||||
/// See <see cref="IAppTools.CreateShortcutsForExecutable"/> to create a shortcut to a different program
|
|
||||||
/// </summary>
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
public static void CreateShortcutForThisExe(this IAppTools This, ShortcutLocation location = ShortcutLocation.Desktop | ShortcutLocation.StartMenu)
|
|
||||||
{
|
|
||||||
This.CreateShortcutsForExecutable(
|
|
||||||
Path.GetFileName(SquirrelRuntimeInfo.EntryExePath),
|
|
||||||
location,
|
|
||||||
Environment.CommandLine.Contains("squirrel-install") == false,
|
|
||||||
null, // shortcut arguments
|
|
||||||
null); // shortcut icon
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a shortcut for the currently running executable at the specified locations.
|
|
||||||
/// </summary>
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
public static void RemoveShortcutForThisExe(this IAppTools This, ShortcutLocation location = ShortcutLocation.Desktop | ShortcutLocation.StartMenu)
|
|
||||||
{
|
|
||||||
This.RemoveShortcutsForExecutable(
|
|
||||||
Path.GetFileName(SquirrelRuntimeInfo.EntryExePath),
|
|
||||||
location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel
|
|
||||||
{
|
|
||||||
internal static class EasyZip
|
|
||||||
{
|
|
||||||
private static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(EasyZip));
|
|
||||||
|
|
||||||
public static void ExtractZipToDirectory(string inputFile, string outputDirectory)
|
|
||||||
{
|
|
||||||
Log.Info($"Extracting '{inputFile}' to '{outputDirectory}' using System.IO.Compression...");
|
|
||||||
Utility.DeleteFileOrDirectoryHard(outputDirectory);
|
|
||||||
ZipFile.ExtractToDirectory(inputFile, outputDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void CreateZipFromDirectory(string outputFile, string directoryToCompress, bool nestDirectory = false)
|
|
||||||
{
|
|
||||||
if (nestDirectory) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
//AddAllFromDirectoryInNestedDir(archive, directoryToCompress);
|
|
||||||
} else {
|
|
||||||
Log.Info($"Compressing '{directoryToCompress}' to '{outputFile}' using System.IO.Compression...");
|
|
||||||
ZipFile.CreateFromDirectory(directoryToCompress, outputFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//private static void AddAllFromDirectoryInNestedDir(
|
|
||||||
// IWritableArchive writableArchive,
|
|
||||||
// string filePath, string searchPattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories)
|
|
||||||
//{
|
|
||||||
// var di = new DirectoryInfo(filePath);
|
|
||||||
// var parent = di.Parent;
|
|
||||||
|
|
||||||
// using (writableArchive.PauseEntryRebuilding())
|
|
||||||
// {
|
|
||||||
// foreach (var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption))
|
|
||||||
// {
|
|
||||||
// var fileInfo = new FileInfo(path);
|
|
||||||
// writableArchive.AddEntry(fileInfo.FullName.Substring(parent.FullName.Length), fileInfo.OpenRead(), true, fileInfo.Length,
|
|
||||||
// fileInfo.LastWriteTime);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
public static bool IsDirectory(this ZipArchiveEntry entry)
|
|
||||||
{
|
|
||||||
return entry.FullName.EndsWith("/") || entry.FullName.EndsWith("\\") || String.IsNullOrEmpty(entry.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,14 +9,12 @@ using System.Runtime.Versioning;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Squirrel.SimpleSplat;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Squirrel
|
namespace Squirrel
|
||||||
{
|
{
|
||||||
internal static class PlatformUtil
|
internal static class PlatformUtil
|
||||||
{
|
{
|
||||||
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(PlatformUtil));
|
|
||||||
|
|
||||||
private const string OSX_CSTD_LIB = "libSystem.dylib";
|
private const string OSX_CSTD_LIB = "libSystem.dylib";
|
||||||
private const string NIX_CSTD_LIB = "libc";
|
private const string NIX_CSTD_LIB = "libc";
|
||||||
private const string WIN_KERNEL32 = "kernel32.dll";
|
private const string WIN_KERNEL32 = "kernel32.dll";
|
||||||
@@ -24,84 +22,6 @@ namespace Squirrel
|
|||||||
private const string WIN_NTDLL = "NTDLL.DLL";
|
private const string WIN_NTDLL = "NTDLL.DLL";
|
||||||
private const string WIN_PSAPI = "psapi.dll";
|
private const string WIN_PSAPI = "psapi.dll";
|
||||||
|
|
||||||
[SupportedOSPlatform("linux")]
|
|
||||||
[DllImport(NIX_CSTD_LIB, EntryPoint = "getppid")]
|
|
||||||
private static extern int nix_getppid();
|
|
||||||
|
|
||||||
[SupportedOSPlatform("osx")]
|
|
||||||
[DllImport(OSX_CSTD_LIB, EntryPoint = "getppid")]
|
|
||||||
private static extern int osx_getppid();
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
[DllImport(WIN_KERNEL32)]
|
|
||||||
private static extern IntPtr GetCurrentProcess();
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
[DllImport(WIN_NTDLL, SetLastError = true)]
|
|
||||||
private static extern int NtQueryInformationProcess(IntPtr hProcess, int pic, ref PROCESS_BASIC_INFORMATION pbi, int cb, out int pSize);
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
|
||||||
private struct PROCESS_BASIC_INFORMATION
|
|
||||||
{
|
|
||||||
public nint ExitStatus;
|
|
||||||
public nint PebBaseAddress;
|
|
||||||
public nint AffinityMask;
|
|
||||||
public nint BasePriority;
|
|
||||||
public nuint UniqueProcessId;
|
|
||||||
public nint InheritedFromUniqueProcessId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Process GetParentProcess()
|
|
||||||
{
|
|
||||||
int parentId;
|
|
||||||
|
|
||||||
if (SquirrelRuntimeInfo.IsWindows) {
|
|
||||||
var pbi = new PROCESS_BASIC_INFORMATION();
|
|
||||||
NtQueryInformationProcess(GetCurrentProcess(), 0, ref pbi, Marshal.SizeOf(typeof(PROCESS_BASIC_INFORMATION)), out _);
|
|
||||||
parentId = (int) pbi.InheritedFromUniqueProcessId;
|
|
||||||
} else if (SquirrelRuntimeInfo.IsLinux) {
|
|
||||||
parentId = nix_getppid();
|
|
||||||
} else if (SquirrelRuntimeInfo.IsOSX) {
|
|
||||||
parentId = osx_getppid();
|
|
||||||
} else {
|
|
||||||
throw new PlatformNotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// the parent process has exited (nix/osx)
|
|
||||||
if (parentId <= 1)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
var p = Process.GetProcessById(parentId);
|
|
||||||
|
|
||||||
// the retrieved process is not our parent, the pid has been reused
|
|
||||||
if (p.StartTime > Process.GetCurrentProcess().StartTime)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return p;
|
|
||||||
} catch (ArgumentException) {
|
|
||||||
// the process has exited (windows)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WaitForParentProcessToExit()
|
|
||||||
{
|
|
||||||
var p = GetParentProcess();
|
|
||||||
if (p == null) {
|
|
||||||
Log.Warn("Will not wait. Parent process has already exited.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Info($"Waiting for PID {p.Id} to exit (60s timeout)...");
|
|
||||||
var exited = p.WaitForExit(60_000);
|
|
||||||
if (!exited) {
|
|
||||||
throw new Exception("Parent wait timed out.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Info($"PID {p.Id} has exited.");
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("osx")]
|
[SupportedOSPlatform("osx")]
|
||||||
[DllImport(OSX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)]
|
[DllImport(OSX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)]
|
||||||
private static extern int osx_chmod(string pathname, int mode);
|
private static extern int osx_chmod(string pathname, int mode);
|
||||||
@@ -257,14 +177,14 @@ namespace Squirrel
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void KillProcessesInDirectory(string directoryToKill)
|
public static void KillProcessesInDirectory(ILogger logger, string directoryToKill)
|
||||||
{
|
{
|
||||||
Log.Info("Killing all processes in " + directoryToKill);
|
logger.Info("Killing all processes in " + directoryToKill);
|
||||||
var myPid = Process.GetCurrentProcess().Id;
|
var myPid = Process.GetCurrentProcess().Id;
|
||||||
int c = 0;
|
int c = 0;
|
||||||
foreach (var x in GetRunningProcessesInDirectory(directoryToKill)) {
|
foreach (var x in GetRunningProcessesInDirectory(directoryToKill)) {
|
||||||
if (myPid == x.ProcessId) {
|
if (myPid == x.ProcessId) {
|
||||||
Log.Info($"Skipping '{x.ProcessExePath}' (is current process)");
|
logger.Info($"Skipping '{x.ProcessExePath}' (is current process)");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,11 +192,11 @@ namespace Squirrel
|
|||||||
Process.GetProcessById(x.ProcessId).Kill();
|
Process.GetProcessById(x.ProcessId).Kill();
|
||||||
c++;
|
c++;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Log.WarnException($"Unable to terminate process (pid.{x.ProcessId})", ex);
|
logger.Warn(ex, $"Unable to terminate process (pid.{x.ProcessId})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info($"Terminated {c} processes successfully.");
|
logger.Info($"Terminated {c} processes successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
|
|
||||||
namespace Squirrel.Lib
|
|
||||||
{
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
internal class ResourceReader : IDisposable
|
|
||||||
{
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
private static extern IntPtr LoadLibraryEx(string lpModuleName, IntPtr hFile, uint dwFlags);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
private static extern IntPtr FindResource(IntPtr hModule, string lpName, string lpType);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
private static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, IntPtr lpType);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
private static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, string lpType);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
private static extern IntPtr FindResourceEx(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLanguage);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
private static extern IntPtr FindResourceEx(IntPtr hModule, string lpType, IntPtr lpName, ushort wLanguage);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
private static extern IntPtr FindResourceEx(IntPtr hModule, string lpType, string lpName, ushort wLanguage);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern uint SizeofResource(IntPtr hModule, IntPtr handle);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern IntPtr LoadResource(IntPtr hModule, IntPtr handle);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern IntPtr LockResource(IntPtr hglobal);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern bool FreeLibrary(IntPtr hModule);
|
|
||||||
|
|
||||||
private IntPtr hModule;
|
|
||||||
const uint LOAD_LIBRARY_AS_DATAFILE = 2;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public ResourceReader(string peFile)
|
|
||||||
{
|
|
||||||
hModule = LoadLibraryEx(peFile, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE);
|
|
||||||
if (hModule == IntPtr.Zero) {
|
|
||||||
throw new Win32Exception();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~ResourceReader()
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] ReadResource(string resourceType, string resourceName)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
|
||||||
|
|
||||||
var hResource = FindResource(hModule, resourceName, resourceType);
|
|
||||||
if (hResource == IntPtr.Zero)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ReadResourceToBytes(hResource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] ReadResource(string resourceType, string resourceName, ushort lang)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
|
||||||
|
|
||||||
var hResource = FindResourceEx(hModule, resourceType, resourceName, lang);
|
|
||||||
if (hResource == IntPtr.Zero)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ReadResourceToBytes(hResource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] ReadResource(string resourceType, IntPtr resourceName)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
|
||||||
|
|
||||||
var hResource = FindResource(hModule, resourceName, resourceType);
|
|
||||||
if (hResource == IntPtr.Zero)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ReadResourceToBytes(hResource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] ReadResource(string resourceType, IntPtr resourceName, ushort lang)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
|
||||||
|
|
||||||
var hResource = FindResourceEx(hModule, resourceType, resourceName, lang);
|
|
||||||
if (hResource == IntPtr.Zero)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ReadResourceToBytes(hResource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] ReadResource(IntPtr resourceType, IntPtr resourceName)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
|
||||||
|
|
||||||
var hResource = FindResource(hModule, resourceName, resourceType);
|
|
||||||
if (hResource == IntPtr.Zero)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ReadResourceToBytes(hResource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] ReadResource(IntPtr resourceType, IntPtr resourceName, ushort lang)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
|
||||||
|
|
||||||
var hResource = FindResourceEx(hModule, resourceType, resourceName, lang);
|
|
||||||
if (hResource == IntPtr.Zero)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ReadResourceToBytes(hResource);
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] ReadResourceToBytes(IntPtr hResource)
|
|
||||||
{
|
|
||||||
uint size = SizeofResource(hModule, hResource);
|
|
||||||
if (size == 0)
|
|
||||||
throw new Win32Exception();
|
|
||||||
|
|
||||||
var hGlobal = LoadResource(hModule, hResource);
|
|
||||||
if (hGlobal == IntPtr.Zero)
|
|
||||||
throw new Win32Exception();
|
|
||||||
|
|
||||||
var data = LockResource(hGlobal);
|
|
||||||
if (data == IntPtr.Zero)
|
|
||||||
throw new Win32Exception(0x21);
|
|
||||||
|
|
||||||
var buf = new byte[size];
|
|
||||||
Marshal.Copy(data, buf, 0, (int) size);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] ReadAssemblyManifest()
|
|
||||||
{
|
|
||||||
return ReadResource(new IntPtr(24) /*RT_MANIFEST*/, new IntPtr(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (!_disposed) {
|
|
||||||
_disposed = true;
|
|
||||||
FreeLibrary(hModule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel
|
|
||||||
{
|
|
||||||
internal sealed class SingleGlobalInstance : IDisposable, IEnableLogger
|
|
||||||
{
|
|
||||||
IDisposable handle = null;
|
|
||||||
|
|
||||||
public SingleGlobalInstance(string key, TimeSpan timeOut)
|
|
||||||
{
|
|
||||||
if (ModeDetector.InUnitTestRunner()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = Path.Combine(Path.GetTempPath(), ".squirrel-lock-" + key);
|
|
||||||
|
|
||||||
var st = new Stopwatch();
|
|
||||||
st.Start();
|
|
||||||
|
|
||||||
var fh = default(FileStream);
|
|
||||||
while (st.Elapsed < timeOut) {
|
|
||||||
try {
|
|
||||||
fh = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Delete);
|
|
||||||
fh.Write(new byte[] { 0xba, 0xad, 0xf0, 0x0d, }, 0, 4);
|
|
||||||
break;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
this.Log().WarnException("Failed to grab lockfile, will retry: " + path, ex);
|
|
||||||
Thread.Sleep(250);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
st.Stop();
|
|
||||||
|
|
||||||
if (fh == null) {
|
|
||||||
throw new Exception("Couldn't acquire lock, is another instance running?");
|
|
||||||
}
|
|
||||||
|
|
||||||
handle = Disposable.Create(() => {
|
|
||||||
fh.Dispose();
|
|
||||||
File.Delete(path);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (ModeDetector.InUnitTestRunner()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var disp = Interlocked.Exchange(ref handle, null);
|
|
||||||
if (disp != null) disp.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
~SingleGlobalInstance()
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using Squirrel.Lib;
|
|
||||||
using Squirrel.NuGet;
|
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel
|
|
||||||
{
|
|
||||||
internal static class SquirrelAwareExecutableDetector
|
|
||||||
{
|
|
||||||
const string SQUIRREL_AWARE_KEY = "SquirrelAwareVersion";
|
|
||||||
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(SquirrelAwareExecutableDetector));
|
|
||||||
|
|
||||||
public static List<string> GetAllSquirrelAwareApps(string directory, int minimumVersion = 1)
|
|
||||||
{
|
|
||||||
var di = new DirectoryInfo(directory);
|
|
||||||
|
|
||||||
return di.EnumerateFiles()
|
|
||||||
.Where(x => Utility.FileHasExtension(x.Name, ".exe"))
|
|
||||||
.Select(x => x.FullName)
|
|
||||||
.Where(x => (GetSquirrelAwareVersion(x) ?? -1) >= minimumVersion)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int? GetSquirrelAwareVersion(string exePath)
|
|
||||||
{
|
|
||||||
if (!File.Exists(exePath)) return null;
|
|
||||||
var fullname = Path.GetFullPath(exePath);
|
|
||||||
|
|
||||||
// ways to search for SquirrelAwareVersion, ordered by precedence
|
|
||||||
// search exe-embedded values first, and if not found, move on to sidecar files
|
|
||||||
var detectors = new List<Func<string, int?>>();
|
|
||||||
|
|
||||||
if (SquirrelRuntimeInfo.IsWindows) {
|
|
||||||
detectors.Add(GetEmbeddedManifestSquirrelAwareValue);
|
|
||||||
detectors.Add(GetVersionBlockSquirrelAwareValue);
|
|
||||||
} else {
|
|
||||||
Log.Warn("Disabled embedded manifest SquirrelAware detection (only supported on windows).");
|
|
||||||
}
|
|
||||||
|
|
||||||
detectors.Add(GetSidecarSquirrelAwareValue);
|
|
||||||
detectors.Add(GetSideBySideManifestSquirrelAwareValue);
|
|
||||||
detectors.Add(GetSideBySideDllManifestSquirrelAwareValue);
|
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
bool error = false;
|
|
||||||
foreach (var fn in detectors) {
|
|
||||||
try {
|
|
||||||
var v = fn(exePath);
|
|
||||||
if (v != null) return v;
|
|
||||||
} catch {
|
|
||||||
error = true;
|
|
||||||
// do not throw, otherwise other detectors will not run
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!error) {
|
|
||||||
// we tried all the detectors and none of them threw, so we don't need to retry
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// retry 3 times with 100ms delay
|
|
||||||
Thread.Sleep(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
static int? GetVersionBlockSquirrelAwareValue(string executable)
|
|
||||||
{
|
|
||||||
return StringFileInfo.ReadVersionInfo(executable, out var vi)
|
|
||||||
.Where(i => i.Key == SQUIRREL_AWARE_KEY)
|
|
||||||
.Where(i => int.TryParse(i.Value, out var _))
|
|
||||||
.Select(i => (int?) int.Parse(i.Value))
|
|
||||||
.FirstOrDefault(i => i > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int? GetSidecarSquirrelAwareValue(string executable)
|
|
||||||
{
|
|
||||||
// Looks for a "MyApp.exe.squirrel" sidecar file
|
|
||||||
// the file should contain just the integer version (eg. "1")
|
|
||||||
var sidecarPath = executable + ".squirrel";
|
|
||||||
if (File.Exists(sidecarPath)) {
|
|
||||||
var txt = File.ReadAllText(sidecarPath);
|
|
||||||
if (int.TryParse(txt, out var pv)) {
|
|
||||||
return pv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int? GetSideBySideManifestSquirrelAwareValue(string executable)
|
|
||||||
{
|
|
||||||
// Looks for an external application manifest eg. "MyApp.exe.manifest"
|
|
||||||
var manifestPath = executable + ".manifest";
|
|
||||||
if (File.Exists(manifestPath)) {
|
|
||||||
return ParseManifestAwareValue(File.ReadAllBytes(manifestPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int? GetSideBySideDllManifestSquirrelAwareValue(string executable)
|
|
||||||
{
|
|
||||||
// Looks for an external application DLL manifest eg. "MyApp.dll.manifest"
|
|
||||||
var manifestPath = Path.Combine(
|
|
||||||
Path.GetDirectoryName(executable),
|
|
||||||
Path.GetFileNameWithoutExtension(executable) + ".dll.manifest");
|
|
||||||
if (File.Exists(manifestPath)) {
|
|
||||||
return ParseManifestAwareValue(File.ReadAllBytes(manifestPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
static int? GetEmbeddedManifestSquirrelAwareValue(string executable)
|
|
||||||
{
|
|
||||||
// Looks for an embedded application manifest
|
|
||||||
byte[] buffer = null;
|
|
||||||
using (var rr = new ResourceReader(executable))
|
|
||||||
buffer = rr.ReadAssemblyManifest();
|
|
||||||
return ParseManifestAwareValue(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int? ParseManifestAwareValue(byte[] buffer)
|
|
||||||
{
|
|
||||||
if (buffer == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var document = XDocument.Load(new MemoryStream(buffer));
|
|
||||||
var aware = document.Root.ElementsNoNamespace(SQUIRREL_AWARE_KEY).FirstOrDefault();
|
|
||||||
if (aware != null && int.TryParse(aware.Value, out var pv)) {
|
|
||||||
return pv;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Squirrel
|
|
||||||
{
|
|
||||||
internal class SubStream : Stream
|
|
||||||
{
|
|
||||||
private readonly Stream _wrappedStream;
|
|
||||||
private readonly long _startOffset;
|
|
||||||
|
|
||||||
public SubStream(Stream wrappedStream, long startOffset)
|
|
||||||
{
|
|
||||||
_wrappedStream = wrappedStream;
|
|
||||||
_startOffset = startOffset;
|
|
||||||
|
|
||||||
if (startOffset >= wrappedStream.Length)
|
|
||||||
throw new ArgumentException("Offset+Length must be less than or equal to the length of the wrapped stream");
|
|
||||||
|
|
||||||
Seek(0, SeekOrigin.Begin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanRead => _wrappedStream.CanRead;
|
|
||||||
|
|
||||||
public override bool CanSeek => _wrappedStream.CanSeek;
|
|
||||||
|
|
||||||
public override bool CanWrite => _wrappedStream.CanWrite;
|
|
||||||
|
|
||||||
public override long Length => _wrappedStream.Length - _startOffset;
|
|
||||||
|
|
||||||
public override long Position {
|
|
||||||
get => _wrappedStream.Position - _startOffset;
|
|
||||||
set => Seek(value, SeekOrigin.Begin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Flush()
|
|
||||||
{
|
|
||||||
_wrappedStream.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int Read(byte[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
return _wrappedStream.Read(buffer, offset, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override long Seek(long offset, SeekOrigin origin)
|
|
||||||
{
|
|
||||||
if (offset > Length)
|
|
||||||
throw new ArgumentException("Offset can not be greater than stream length");
|
|
||||||
|
|
||||||
if (origin == SeekOrigin.Begin) {
|
|
||||||
return _wrappedStream.Seek(_startOffset + offset, SeekOrigin.Begin) - _startOffset;
|
|
||||||
} else if (origin == SeekOrigin.End) {
|
|
||||||
return _wrappedStream.Seek(offset, SeekOrigin.End) - _startOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPosition = _wrappedStream.Seek(offset, SeekOrigin.Current);
|
|
||||||
if (newPosition < _startOffset)
|
|
||||||
throw new ArgumentException("Cannot seak beyond the beginning of a stream");
|
|
||||||
|
|
||||||
return newPosition - _startOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SetLength(long value)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(byte[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
_wrappedStream.Write(buffer, offset, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Contracts;
|
using System.Diagnostics.Contracts;
|
||||||
@@ -8,8 +8,8 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Squirrel.NuGet;
|
using Squirrel.NuGet;
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel
|
namespace Squirrel
|
||||||
{
|
{
|
||||||
@@ -180,7 +180,7 @@ namespace Squirrel
|
|||||||
Contract.Requires(!String.IsNullOrEmpty(to));
|
Contract.Requires(!String.IsNullOrEmpty(to));
|
||||||
|
|
||||||
if (!File.Exists(from)) {
|
if (!File.Exists(from)) {
|
||||||
Log().Warn("The file {0} does not exist", from);
|
//Log().Warn("The file {0} does not exist", from);
|
||||||
|
|
||||||
// TODO: should we fail this operation?
|
// TODO: should we fail this operation?
|
||||||
return;
|
return;
|
||||||
@@ -198,7 +198,7 @@ namespace Squirrel
|
|||||||
}, retries, retryDelay);
|
}, retries, retryDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250)
|
public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250, ILogger logger = null)
|
||||||
{
|
{
|
||||||
Contract.Requires(retries > 0);
|
Contract.Requires(retries > 0);
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ namespace Squirrel
|
|||||||
return ret;
|
return ret;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
if (retries == 0) throw;
|
if (retries == 0) throw;
|
||||||
Log().Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
|
logger?.Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
|
||||||
retries--;
|
retries--;
|
||||||
Thread.Sleep(retryDelay);
|
Thread.Sleep(retryDelay);
|
||||||
}
|
}
|
||||||
@@ -223,14 +223,14 @@ namespace Squirrel
|
|||||||
}, retries, retryDelay);
|
}, retries, retryDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250)
|
public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250, ILogger logger = null)
|
||||||
{
|
{
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
return await block().ConfigureAwait(false);
|
return await block().ConfigureAwait(false);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
if (retries == 0) throw;
|
if (retries == 0) throw;
|
||||||
Log().Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
|
logger?.Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
|
||||||
retries--;
|
retries--;
|
||||||
await Task.Delay(retryDelay).ConfigureAwait(false);
|
await Task.Delay(retryDelay).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -346,14 +346,14 @@ namespace Squirrel
|
|||||||
/// <param name="throwOnFailure">Whether this function should throw if the delete fails.</param>
|
/// <param name="throwOnFailure">Whether this function should throw if the delete fails.</param>
|
||||||
/// <param name="renameFirst">Try to rename this object first before deleting. Can help prevent partial delete of folders.</param>
|
/// <param name="renameFirst">Try to rename this object first before deleting. Can help prevent partial delete of folders.</param>
|
||||||
/// <returns>True if the file system object was deleted, false otherwise.</returns>
|
/// <returns>True if the file system object was deleted, false otherwise.</returns>
|
||||||
public static bool DeleteFileOrDirectoryHard(string path, bool throwOnFailure = true, bool renameFirst = false)
|
public static bool DeleteFileOrDirectoryHard(string path, bool throwOnFailure = true, bool renameFirst = false, ILogger logger = null)
|
||||||
{
|
{
|
||||||
Contract.Requires(!String.IsNullOrEmpty(path));
|
Contract.Requires(!String.IsNullOrEmpty(path));
|
||||||
Log().Debug("Starting to delete: {0}", path);
|
logger?.Debug($"Starting to delete: {path}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (File.Exists(path)) {
|
if (File.Exists(path)) {
|
||||||
DeleteFsiVeryHard(new FileInfo(path));
|
DeleteFsiVeryHard(new FileInfo(path), logger);
|
||||||
} else if (Directory.Exists(path)) {
|
} else if (Directory.Exists(path)) {
|
||||||
if (renameFirst) {
|
if (renameFirst) {
|
||||||
// if there are locked files in a directory, we will not attempt to delte it
|
// if there are locked files in a directory, we will not attempt to delte it
|
||||||
@@ -362,26 +362,26 @@ namespace Squirrel
|
|||||||
path = oldPath;
|
path = oldPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteFsiTree(new DirectoryInfo(path));
|
DeleteFsiTree(new DirectoryInfo(path), logger);
|
||||||
} else {
|
} else {
|
||||||
if (throwOnFailure)
|
if (throwOnFailure)
|
||||||
Log().Warn($"Cannot delete '{path}' if it does not exist.");
|
logger?.Warn($"Cannot delete '{path}' if it does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Log().ErrorException($"Unable to delete '{path}'", ex);
|
logger?.Error(ex, $"Unable to delete '{path}'");
|
||||||
if (throwOnFailure)
|
if (throwOnFailure)
|
||||||
throw;
|
throw;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DeleteFsiTree(FileSystemInfo fileSystemInfo)
|
private static void DeleteFsiTree(FileSystemInfo fileSystemInfo, ILogger logger)
|
||||||
{
|
{
|
||||||
// if junction / symlink, don't iterate, just delete it.
|
// if junction / symlink, don't iterate, just delete it.
|
||||||
if (fileSystemInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) {
|
if (fileSystemInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) {
|
||||||
DeleteFsiVeryHard(fileSystemInfo);
|
DeleteFsiVeryHard(fileSystemInfo, logger);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,19 +390,19 @@ namespace Squirrel
|
|||||||
var directoryInfo = fileSystemInfo as DirectoryInfo;
|
var directoryInfo = fileSystemInfo as DirectoryInfo;
|
||||||
if (directoryInfo != null) {
|
if (directoryInfo != null) {
|
||||||
foreach (FileSystemInfo childInfo in directoryInfo.GetFileSystemInfos()) {
|
foreach (FileSystemInfo childInfo in directoryInfo.GetFileSystemInfos()) {
|
||||||
DeleteFsiTree(childInfo);
|
DeleteFsiTree(childInfo, logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Log().WarnException($"Unable to traverse children of '{fileSystemInfo.FullName}'", ex);
|
logger?.Warn(ex, $"Unable to traverse children of '{fileSystemInfo.FullName}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// finally, delete myself, we should try this even if deleting children failed
|
// finally, delete myself, we should try this even if deleting children failed
|
||||||
// because Directory.Delete can also be recursive
|
// because Directory.Delete can also be recursive
|
||||||
DeleteFsiVeryHard(fileSystemInfo);
|
DeleteFsiVeryHard(fileSystemInfo, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DeleteFsiVeryHard(FileSystemInfo fileSystemInfo)
|
private static void DeleteFsiVeryHard(FileSystemInfo fileSystemInfo, ILogger logger)
|
||||||
{
|
{
|
||||||
// don't try to delete the running process
|
// don't try to delete the running process
|
||||||
if (FullPathEquals(fileSystemInfo.FullName, SquirrelRuntimeInfo.EntryExePath))
|
if (FullPathEquals(fileSystemInfo.FullName, SquirrelRuntimeInfo.EntryExePath))
|
||||||
@@ -429,7 +429,7 @@ namespace Squirrel
|
|||||||
}
|
}
|
||||||
}, retries: 4, retryDelay: 50);
|
}, retries: 4, retryDelay: 50);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Log().WarnException($"Unable to delete child '{fileSystemInfo.FullName}'", ex);
|
logger?.Warn(ex, $"Unable to delete child '{fileSystemInfo.FullName}'");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,138 +539,6 @@ namespace Squirrel
|
|||||||
return relativePath.Split(Path.DirectorySeparatorChar).Length == 4;
|
return relativePath.Split(Path.DirectorySeparatorChar).Length == 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void LogIfThrows(this IFullLogger This, LogLevel level, string message, Action block)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
block();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
switch (level) {
|
|
||||||
case LogLevel.Debug:
|
|
||||||
This.DebugException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
case LogLevel.Info:
|
|
||||||
This.InfoException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
case LogLevel.Warn:
|
|
||||||
This.WarnException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
case LogLevel.Error:
|
|
||||||
This.ErrorException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task LogIfThrows(this IFullLogger This, LogLevel level, string message, Func<Task> block)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
await block().ConfigureAwait(false);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
switch (level) {
|
|
||||||
case LogLevel.Debug:
|
|
||||||
This.DebugException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
case LogLevel.Info:
|
|
||||||
This.InfoException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
case LogLevel.Warn:
|
|
||||||
This.WarnException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
case LogLevel.Error:
|
|
||||||
This.ErrorException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<T> LogIfThrows<T>(this IFullLogger This, LogLevel level, string message, Func<Task<T>> block)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return await block().ConfigureAwait(false);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
switch (level) {
|
|
||||||
case LogLevel.Debug:
|
|
||||||
This.DebugException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
case LogLevel.Info:
|
|
||||||
This.InfoException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
case LogLevel.Warn:
|
|
||||||
This.WarnException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
case LogLevel.Error:
|
|
||||||
This.ErrorException(message ?? "", ex);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WarnIfThrows(this IEnableLogger This, Action block, string message = null)
|
|
||||||
{
|
|
||||||
This.Log().LogIfThrows(LogLevel.Warn, message, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task WarnIfThrows(this IEnableLogger This, Func<Task> block, string message = null)
|
|
||||||
{
|
|
||||||
return This.Log().LogIfThrows(LogLevel.Warn, message, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<T> WarnIfThrows<T>(this IEnableLogger This, Func<Task<T>> block, string message = null)
|
|
||||||
{
|
|
||||||
return This.Log().LogIfThrows(LogLevel.Warn, message, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ErrorIfThrows(this IEnableLogger This, Action block, string message = null)
|
|
||||||
{
|
|
||||||
This.Log().LogIfThrows(LogLevel.Error, message, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task ErrorIfThrows(this IEnableLogger This, Func<Task> block, string message = null)
|
|
||||||
{
|
|
||||||
return This.Log().LogIfThrows(LogLevel.Error, message, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<T> ErrorIfThrows<T>(this IEnableLogger This, Func<Task<T>> block, string message = null)
|
|
||||||
{
|
|
||||||
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<Task> block, string message = null)
|
|
||||||
{
|
|
||||||
return This.LogIfThrows(LogLevel.Warn, message, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<T> WarnIfThrows<T>(this IFullLogger This, Func<Task<T>> 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<Task> block, string message = null)
|
|
||||||
{
|
|
||||||
return This.LogIfThrows(LogLevel.Error, message, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<T> ErrorIfThrows<T>(this IFullLogger This, Func<Task<T>> block, string message = null)
|
|
||||||
{
|
|
||||||
return This.LogIfThrows(LogLevel.Error, message, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ConsoleWriteWithColor(string text, ConsoleColor color)
|
public static void ConsoleWriteWithColor(string text, ConsoleColor color)
|
||||||
{
|
{
|
||||||
var fc = Console.ForegroundColor;
|
var fc = Console.ForegroundColor;
|
||||||
@@ -679,14 +547,6 @@ namespace Squirrel
|
|||||||
Console.ForegroundColor = fc;
|
Console.ForegroundColor = fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
static IFullLogger logger;
|
|
||||||
|
|
||||||
static IFullLogger Log()
|
|
||||||
{
|
|
||||||
return logger ??
|
|
||||||
(logger = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(Utility)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Guid CreateGuidFromHash(string text)
|
public static Guid CreateGuidFromHash(string text)
|
||||||
{
|
{
|
||||||
return CreateGuidFromHash(text, Utility.IsoOidNamespace);
|
return CreateGuidFromHash(text, Utility.IsoOidNamespace);
|
||||||
|
|||||||
@@ -1,401 +0,0 @@
|
|||||||
// File: RollThroughLibrary/CreateMaps/JunctionPoint.cs
|
|
||||||
// User: Adrian Hum/
|
|
||||||
//
|
|
||||||
// Created: 2017-11-19 2:46 PM
|
|
||||||
// Modified: 2017-11-19 6:10 PM
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.Win32.SafeHandles;
|
|
||||||
|
|
||||||
namespace Squirrel.Lib
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides access to NTFS junction points in .Net.
|
|
||||||
/// </summary>
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
internal static class JunctionPoint
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The file or directory is not a reparse point.
|
|
||||||
/// </summary>
|
|
||||||
private const int ERROR_NOT_A_REPARSE_POINT = 4390;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The reparse point attribute cannot be set because it conflicts with an existing attribute.
|
|
||||||
/// </summary>
|
|
||||||
private const int ERROR_REPARSE_ATTRIBUTE_CONFLICT = 4391;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The data present in the reparse point buffer is invalid.
|
|
||||||
/// </summary>
|
|
||||||
private const int ERROR_INVALID_REPARSE_DATA = 4392;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The tag present in the reparse point buffer is invalid.
|
|
||||||
/// </summary>
|
|
||||||
private const int ERROR_REPARSE_TAG_INVALID = 4393;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// There is a mismatch between the tag specified in the request and the tag present in the reparse point.
|
|
||||||
/// </summary>
|
|
||||||
private const int ERROR_REPARSE_TAG_MISMATCH = 4394;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command to set the reparse point data block.
|
|
||||||
/// </summary>
|
|
||||||
private const int FSCTL_SET_REPARSE_POINT = 0x000900A4;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command to get the reparse point data block.
|
|
||||||
/// </summary>
|
|
||||||
private const int FSCTL_GET_REPARSE_POINT = 0x000900A8;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command to delete the reparse point data base.
|
|
||||||
/// </summary>
|
|
||||||
private const int FSCTL_DELETE_REPARSE_POINT = 0x000900AC;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reparse point tag used to identify mount points and junction points.
|
|
||||||
/// </summary>
|
|
||||||
private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This prefix indicates to NTFS that the path is to be treated as a non-interpreted
|
|
||||||
/// path in the virtual file system.
|
|
||||||
/// </summary>
|
|
||||||
private const string NonInterpretedPathPrefix = @"\??\";
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
private static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
|
|
||||||
IntPtr InBuffer, int nInBufferSize,
|
|
||||||
IntPtr OutBuffer, int nOutBufferSize,
|
|
||||||
out int pBytesReturned, IntPtr lpOverlapped);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern IntPtr CreateFile(
|
|
||||||
string lpFileName,
|
|
||||||
EFileAccess dwDesiredAccess,
|
|
||||||
EFileShare dwShareMode,
|
|
||||||
IntPtr lpSecurityAttributes,
|
|
||||||
ECreationDisposition dwCreationDisposition,
|
|
||||||
EFileAttributes dwFlagsAndAttributes,
|
|
||||||
IntPtr hTemplateFile);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a junction point from the specified directory to the specified target directory.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Only works on NTFS.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="junctionPoint">The junction point path</param>
|
|
||||||
/// <param name="targetDir">The target directory</param>
|
|
||||||
/// <param name="overwrite">If true overwrites an existing reparse point or empty directory</param>
|
|
||||||
/// <exception cref="IOException">
|
|
||||||
/// Thrown when the junction point could not be created or when
|
|
||||||
/// an existing directory was found and <paramref name="overwrite" /> if false
|
|
||||||
/// </exception>
|
|
||||||
public static void Create(string junctionPoint, string targetDir, bool overwrite)
|
|
||||||
{
|
|
||||||
targetDir = Path.GetFullPath(targetDir);
|
|
||||||
|
|
||||||
if (!Directory.Exists(targetDir))
|
|
||||||
throw new IOException("Target path does not exist or is not a directory.");
|
|
||||||
|
|
||||||
if (Directory.Exists(junctionPoint)) {
|
|
||||||
if (!overwrite)
|
|
||||||
throw new IOException("Directory already exists and overwrite parameter is false.");
|
|
||||||
} else {
|
|
||||||
Directory.CreateDirectory(junctionPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite)) {
|
|
||||||
var targetDirBytes = Encoding.Unicode.GetBytes(NonInterpretedPathPrefix + Path.GetFullPath(targetDir));
|
|
||||||
|
|
||||||
var reparseDataBuffer =
|
|
||||||
new REPARSE_DATA_BUFFER {
|
|
||||||
ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
|
|
||||||
ReparseDataLength = (ushort) (targetDirBytes.Length + 12),
|
|
||||||
SubstituteNameOffset = 0,
|
|
||||||
SubstituteNameLength = (ushort) targetDirBytes.Length,
|
|
||||||
PrintNameOffset = (ushort) (targetDirBytes.Length + 2),
|
|
||||||
PrintNameLength = 0,
|
|
||||||
PathBuffer = new byte[0x3ff0]
|
|
||||||
};
|
|
||||||
|
|
||||||
Array.Copy(targetDirBytes, reparseDataBuffer.PathBuffer, targetDirBytes.Length);
|
|
||||||
|
|
||||||
var inBufferSize = Marshal.SizeOf(reparseDataBuffer);
|
|
||||||
var inBuffer = Marshal.AllocHGlobal(inBufferSize);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);
|
|
||||||
|
|
||||||
int bytesReturned;
|
|
||||||
var result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_SET_REPARSE_POINT,
|
|
||||||
inBuffer, targetDirBytes.Length + 20, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);
|
|
||||||
|
|
||||||
if (!result)
|
|
||||||
ThrowLastWin32Error("Unable to create junction point.");
|
|
||||||
} finally {
|
|
||||||
Marshal.FreeHGlobal(inBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes a junction point at the specified source directory along with the directory itself.
|
|
||||||
/// Does nothing if the junction point does not exist.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Only works on NTFS.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="junctionPoint">The junction point path</param>
|
|
||||||
public static void Delete(string junctionPoint)
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(junctionPoint)) {
|
|
||||||
if (File.Exists(junctionPoint))
|
|
||||||
throw new IOException("Path is not a junction point.");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite)) {
|
|
||||||
var reparseDataBuffer = new REPARSE_DATA_BUFFER {
|
|
||||||
ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
|
|
||||||
ReparseDataLength = 0,
|
|
||||||
PathBuffer = new byte[0x3ff0]
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
var inBufferSize = Marshal.SizeOf(reparseDataBuffer);
|
|
||||||
var inBuffer = Marshal.AllocHGlobal(inBufferSize);
|
|
||||||
try {
|
|
||||||
Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);
|
|
||||||
|
|
||||||
int bytesReturned;
|
|
||||||
var result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_DELETE_REPARSE_POINT,
|
|
||||||
inBuffer, 8, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);
|
|
||||||
|
|
||||||
if (!result)
|
|
||||||
ThrowLastWin32Error("Unable to delete junction point.");
|
|
||||||
} finally {
|
|
||||||
Marshal.FreeHGlobal(inBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Directory.Delete(junctionPoint);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new IOException("Unable to delete junction point.", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether the specified path exists and refers to a junction point.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The junction point path</param>
|
|
||||||
/// <returns>True if the specified path represents a junction point</returns>
|
|
||||||
/// <exception cref="IOException">
|
|
||||||
/// Thrown if the specified path is invalid
|
|
||||||
/// or some other error occurs
|
|
||||||
/// </exception>
|
|
||||||
public static bool Exists(string path)
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(path))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
using (var handle = OpenReparsePoint(path, EFileAccess.GenericRead)) {
|
|
||||||
var target = InternalGetTarget(handle);
|
|
||||||
return target != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the target of the specified junction point.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Only works on NTFS.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="junctionPoint">The junction point path</param>
|
|
||||||
/// <returns>The target of the junction point</returns>
|
|
||||||
/// <exception cref="IOException">
|
|
||||||
/// Thrown when the specified path does not
|
|
||||||
/// exist, is invalid, is not a junction point, or some other error occurs
|
|
||||||
/// </exception>
|
|
||||||
public static string GetTarget(string junctionPoint)
|
|
||||||
{
|
|
||||||
using (var handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericRead)) {
|
|
||||||
var target = InternalGetTarget(handle);
|
|
||||||
if (target == null)
|
|
||||||
throw new IOException("Path is not a junction point.");
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string InternalGetTarget(SafeFileHandle handle)
|
|
||||||
{
|
|
||||||
var outBufferSize = Marshal.SizeOf(typeof(REPARSE_DATA_BUFFER));
|
|
||||||
var outBuffer = Marshal.AllocHGlobal(outBufferSize);
|
|
||||||
|
|
||||||
try {
|
|
||||||
int bytesReturned;
|
|
||||||
var result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_GET_REPARSE_POINT,
|
|
||||||
IntPtr.Zero, 0, outBuffer, outBufferSize, out bytesReturned, IntPtr.Zero);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
var error = Marshal.GetLastWin32Error();
|
|
||||||
if (error == ERROR_NOT_A_REPARSE_POINT)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
ThrowLastWin32Error("Unable to get information about junction point.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var reparseDataBuffer = (REPARSE_DATA_BUFFER)
|
|
||||||
Marshal.PtrToStructure(outBuffer, typeof(REPARSE_DATA_BUFFER));
|
|
||||||
|
|
||||||
if (reparseDataBuffer.ReparseTag != IO_REPARSE_TAG_MOUNT_POINT)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer,
|
|
||||||
reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength);
|
|
||||||
|
|
||||||
if (targetDir.StartsWith(NonInterpretedPathPrefix))
|
|
||||||
targetDir = targetDir.Substring(NonInterpretedPathPrefix.Length);
|
|
||||||
|
|
||||||
return targetDir;
|
|
||||||
} finally {
|
|
||||||
Marshal.FreeHGlobal(outBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SafeFileHandle OpenReparsePoint(string reparsePoint, EFileAccess accessMode)
|
|
||||||
{
|
|
||||||
var reparsePointHandle = new SafeFileHandle(CreateFile(reparsePoint, accessMode,
|
|
||||||
EFileShare.Read | EFileShare.Write | EFileShare.Delete,
|
|
||||||
IntPtr.Zero, ECreationDisposition.OpenExisting,
|
|
||||||
EFileAttributes.BackupSemantics | EFileAttributes.OpenReparsePoint, IntPtr.Zero), true);
|
|
||||||
|
|
||||||
if (Marshal.GetLastWin32Error() != 0)
|
|
||||||
ThrowLastWin32Error("Unable to open reparse point.");
|
|
||||||
|
|
||||||
return reparsePointHandle;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ThrowLastWin32Error(string message)
|
|
||||||
{
|
|
||||||
throw new IOException(message, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
private enum EFileAccess : uint
|
|
||||||
{
|
|
||||||
GenericRead = 0x80000000,
|
|
||||||
GenericWrite = 0x40000000,
|
|
||||||
GenericExecute = 0x20000000,
|
|
||||||
GenericAll = 0x10000000
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
private enum EFileShare : uint
|
|
||||||
{
|
|
||||||
None = 0x00000000,
|
|
||||||
Read = 0x00000001,
|
|
||||||
Write = 0x00000002,
|
|
||||||
Delete = 0x00000004
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum ECreationDisposition : uint
|
|
||||||
{
|
|
||||||
New = 1,
|
|
||||||
CreateAlways = 2,
|
|
||||||
OpenExisting = 3,
|
|
||||||
OpenAlways = 4,
|
|
||||||
TruncateExisting = 5
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
private enum EFileAttributes : uint
|
|
||||||
{
|
|
||||||
Readonly = 0x00000001,
|
|
||||||
Hidden = 0x00000002,
|
|
||||||
System = 0x00000004,
|
|
||||||
Directory = 0x00000010,
|
|
||||||
Archive = 0x00000020,
|
|
||||||
Device = 0x00000040,
|
|
||||||
Normal = 0x00000080,
|
|
||||||
Temporary = 0x00000100,
|
|
||||||
SparseFile = 0x00000200,
|
|
||||||
ReparsePoint = 0x00000400,
|
|
||||||
Compressed = 0x00000800,
|
|
||||||
Offline = 0x00001000,
|
|
||||||
NotContentIndexed = 0x00002000,
|
|
||||||
Encrypted = 0x00004000,
|
|
||||||
Write_Through = 0x80000000,
|
|
||||||
Overlapped = 0x40000000,
|
|
||||||
NoBuffering = 0x20000000,
|
|
||||||
RandomAccess = 0x10000000,
|
|
||||||
SequentialScan = 0x08000000,
|
|
||||||
DeleteOnClose = 0x04000000,
|
|
||||||
BackupSemantics = 0x02000000,
|
|
||||||
PosixSemantics = 0x01000000,
|
|
||||||
OpenReparsePoint = 0x00200000,
|
|
||||||
OpenNoRecall = 0x00100000,
|
|
||||||
FirstPipeInstance = 0x00080000
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
private struct REPARSE_DATA_BUFFER
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Reparse point tag. Must be a Microsoft reparse point tag.
|
|
||||||
/// </summary>
|
|
||||||
public uint ReparseTag;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Size, in bytes, of the data after the Reserved member. This can be calculated by:
|
|
||||||
/// (4 * sizeof(ushort)) + SubstituteNameLength + PrintNameLength +
|
|
||||||
/// (namesAreNullTerminated ? 2 * sizeof(char) : 0);
|
|
||||||
/// </summary>
|
|
||||||
public ushort ReparseDataLength;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reserved; do not use.
|
|
||||||
/// </summary>
|
|
||||||
public ushort Reserved;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Offset, in bytes, of the substitute name string in the PathBuffer array.
|
|
||||||
/// </summary>
|
|
||||||
public ushort SubstituteNameOffset;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Length, in bytes, of the substitute name string. If this string is null-terminated,
|
|
||||||
/// SubstituteNameLength does not include space for the null character.
|
|
||||||
/// </summary>
|
|
||||||
public ushort SubstituteNameLength;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Offset, in bytes, of the print name string in the PathBuffer array.
|
|
||||||
/// </summary>
|
|
||||||
public ushort PrintNameOffset;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Length, in bytes, of the print name string. If this string is null-terminated,
|
|
||||||
/// PrintNameLength does not include space for the null character.
|
|
||||||
/// </summary>
|
|
||||||
public ushort PrintNameLength;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A buffer containing the unicode-encoded path string. The path string contains
|
|
||||||
/// the substitute name string and print name string.
|
|
||||||
/// </summary>
|
|
||||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3FF0)] public byte[] PathBuffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
54
src/Squirrel/LoggerExtensions.cs
Normal file
54
src/Squirrel/LoggerExtensions.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Squirrel
|
||||||
|
{
|
||||||
|
public static class LoggerExtensions
|
||||||
|
{
|
||||||
|
public static void Warn(this ILogger logger, string message)
|
||||||
|
{
|
||||||
|
logger.LogWarning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Warn(this ILogger logger, Exception ex, string message)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Warn(this ILogger logger, Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Info(this ILogger logger, string message)
|
||||||
|
{
|
||||||
|
logger.LogInformation(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Error(this ILogger logger, string message)
|
||||||
|
{
|
||||||
|
logger.LogError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Error(this ILogger logger, Exception ex, string message)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Error(this ILogger logger, Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Debug(this ILogger logger, string message)
|
||||||
|
{
|
||||||
|
logger.LogDebug(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Trace(this ILogger logger, string message)
|
||||||
|
{
|
||||||
|
logger.LogTrace(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/Squirrel/NuGet/ZipExtensions.cs
Normal file
15
src/Squirrel/NuGet/ZipExtensions.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
|
namespace Squirrel.NuGet
|
||||||
|
{
|
||||||
|
public static class ZipExtensions
|
||||||
|
{
|
||||||
|
public static bool IsDirectory(this ZipArchiveEntry entry)
|
||||||
|
{
|
||||||
|
return entry.FullName.EndsWith("/") || entry.FullName.EndsWith("\\") || String.IsNullOrEmpty(entry.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ using System.Linq;
|
|||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Squirrel.SimpleSplat;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Squirrel.NuGet
|
namespace Squirrel.NuGet
|
||||||
{
|
{
|
||||||
@@ -90,13 +90,13 @@ namespace Squirrel.NuGet
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task ExtractZipReleaseForInstall(string zipFilePath, string outFolder, string rootPackageFolder, Action<int> progress)
|
public static Task ExtractZipReleaseForInstall(ILogger logger, string zipFilePath, string outFolder, string rootPackageFolder, Action<int> progress)
|
||||||
{
|
{
|
||||||
if (SquirrelRuntimeInfo.IsWindows)
|
if (SquirrelRuntimeInfo.IsWindows)
|
||||||
return ExtractZipReleaseForInstallWindows(zipFilePath, outFolder, rootPackageFolder, progress);
|
return ExtractZipReleaseForInstallWindows(logger, zipFilePath, outFolder, rootPackageFolder, progress);
|
||||||
|
|
||||||
if (SquirrelRuntimeInfo.IsOSX)
|
if (SquirrelRuntimeInfo.IsOSX)
|
||||||
return ExtractZipReleaseForInstallOSX(zipFilePath, outFolder, progress);
|
return ExtractZipReleaseForInstallOSX(logger, zipFilePath, outFolder, progress);
|
||||||
|
|
||||||
throw new NotSupportedException("Platform not supported.");
|
throw new NotSupportedException("Platform not supported.");
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ namespace Squirrel.NuGet
|
|||||||
new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
[SupportedOSPlatform("macos")]
|
[SupportedOSPlatform("macos")]
|
||||||
public static Task ExtractZipReleaseForInstallOSX(string zipFilePath, string outFinalFolder, Action<int> progress)
|
public static Task ExtractZipReleaseForInstallOSX(ILogger logger, string zipFilePath, string outFinalFolder, Action<int> progress)
|
||||||
{
|
{
|
||||||
if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist");
|
if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist");
|
||||||
progress ??= ((_) => { });
|
progress ??= ((_) => { });
|
||||||
@@ -161,7 +161,7 @@ namespace Squirrel.NuGet
|
|||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
public static Task ExtractZipReleaseForInstallWindows(string zipFilePath, string outFinalFolder, string rootPackageFolder, Action<int> progress)
|
public static Task ExtractZipReleaseForInstallWindows(ILogger logger, string zipFilePath, string outFinalFolder, string rootPackageFolder, Action<int> progress)
|
||||||
{
|
{
|
||||||
if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist");
|
if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist");
|
||||||
progress ??= ((_) => { });
|
progress ??= ((_) => { });
|
||||||
@@ -198,13 +198,15 @@ namespace Squirrel.NuGet
|
|||||||
var failureIsOkay = false;
|
var failureIsOkay = false;
|
||||||
if (!entry.IsDirectory() && decoded.Contains("_ExecutionStub.exe")) {
|
if (!entry.IsDirectory() && decoded.Contains("_ExecutionStub.exe")) {
|
||||||
// NB: On upgrade, many of these stubs will be in-use, nbd tho.
|
// NB: On upgrade, many of these stubs will be in-use, nbd tho.
|
||||||
failureIsOkay = true;
|
//failureIsOkay = true;
|
||||||
|
|
||||||
fullTargetFile = Path.Combine(
|
//fullTargetFile = Path.Combine(
|
||||||
rootPackageFolder,
|
// rootPackageFolder,
|
||||||
Path.GetFileName(decoded).Replace("_ExecutionStub.exe", ".exe"));
|
// Path.GetFileName(decoded).Replace("_ExecutionStub.exe", ".exe"));
|
||||||
|
|
||||||
LogHost.Default.Info("Rigging execution stub for {0} to {1}", decoded, fullTargetFile);
|
//logger.Info($"Rigging execution stub for {decoded} to {fullTargetFile}");
|
||||||
|
logger.Info($"Skipping obsolete stub {decoded}");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utility.PathPartEquals(parts.Last(), "app.ico")) {
|
if (Utility.PathPartEquals(parts.Last(), "app.ico")) {
|
||||||
@@ -222,7 +224,7 @@ namespace Squirrel.NuGet
|
|||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!failureIsOkay) throw;
|
if (!failureIsOkay) throw;
|
||||||
LogHost.Default.WarnException("Can't write execution stub, probably in use", e);
|
logger.Warn(e, "Can't write execution stub, probably in use");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
[assembly: ComVisible(false)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel.Tests, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("SquirrelMac, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("SquirrelCli, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine.OSX, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine.Windows, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Update, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("UpdateMac, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Update.Windows, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("Update.OSX, PublicKey=" + SNK.SHA1)]
|
|
||||||
[assembly: InternalsVisibleTo("csq, PublicKey=" + SNK.SHA1)]
|
|
||||||
|
|
||||||
internal static class SNK
|
|
||||||
{
|
|
||||||
public const string SHA1 = "002400000480000094000000060200000024000052534131000400000100010061b199572531d267773d7783a077bc020aacb34a10d8c11407505a4a814284d4c953df3229ccf8f63d1a410a3395b7266e5e5cba8f1c0bc9ee10fc7ddafdae297431e2eef82eccd2ac8957bfc9119063f4a965d6ae3ccf53e1f4d8e9ce894a79ea1f681eb2067745c5253f6747cbc51eec640dd2edb4a67339b44f093e3ec5b0";
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NuGet.Versioning;
|
using NuGet.Versioning;
|
||||||
using Squirrel.NuGet;
|
using Squirrel.NuGet;
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel
|
namespace Squirrel
|
||||||
{
|
{
|
||||||
@@ -79,7 +78,7 @@ namespace Squirrel
|
|||||||
|
|
||||||
/// <inheritdoc cref="IReleaseEntry" />
|
/// <inheritdoc cref="IReleaseEntry" />
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public class ReleaseEntry : IEnableLogger, IReleaseEntry
|
public class ReleaseEntry : IReleaseEntry
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[DataMember] public string SHA1 { get; protected set; }
|
[DataMember] public string SHA1 { get; protected set; }
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel.Sources
|
namespace Squirrel.Sources
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A simple abstractable file downloader
|
/// A simple abstractable file downloader
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IFileDownloader : IEnableLogger
|
public interface IFileDownloader
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads a remote file to the specified local path
|
/// Downloads a remote file to the specified local path
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel.Sources
|
namespace Squirrel.Sources
|
||||||
{
|
{
|
||||||
@@ -9,7 +8,7 @@ namespace Squirrel.Sources
|
|||||||
/// An implementation may copy a file from a local repository, download from a web address,
|
/// An implementation may copy a file from a local repository, download from a web address,
|
||||||
/// or even use third party services and parse proprietary data to produce a package feed.
|
/// or even use third party services and parse proprietary data to produce a package feed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IUpdateSource : IEnableLogger
|
public interface IUpdateSource
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve the list of available remote releases from the package source. These releases
|
/// Retrieve the list of available remote releases from the package source. These releases
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Squirrel.SimpleSplat;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Squirrel.Sources
|
namespace Squirrel.Sources
|
||||||
{
|
{
|
||||||
@@ -13,13 +13,16 @@ namespace Squirrel.Sources
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SimpleFileSource : IUpdateSource
|
public class SimpleFileSource : IUpdateSource
|
||||||
{
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
/// <summary> The local directory containing packages to update to. </summary>
|
/// <summary> The local directory containing packages to update to. </summary>
|
||||||
public virtual DirectoryInfo BaseDirectory { get; }
|
public virtual DirectoryInfo BaseDirectory { get; }
|
||||||
|
|
||||||
/// <inheritdoc cref="SimpleFileSource" />
|
/// <inheritdoc cref="SimpleFileSource" />
|
||||||
/// <param name="baseDirectory">The directory where to search for packages.</param>
|
/// <param name="baseDirectory">The directory where to search for packages.</param>
|
||||||
public SimpleFileSource(DirectoryInfo baseDirectory)
|
public SimpleFileSource(ILogger logger, DirectoryInfo baseDirectory)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
BaseDirectory = baseDirectory;
|
BaseDirectory = baseDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +33,7 @@ namespace Squirrel.Sources
|
|||||||
throw new Exception($"The local update directory '{BaseDirectory.FullName}' does not exist.");
|
throw new Exception($"The local update directory '{BaseDirectory.FullName}' does not exist.");
|
||||||
|
|
||||||
var releasesPath = Path.Combine(BaseDirectory.FullName, "RELEASES");
|
var releasesPath = Path.Combine(BaseDirectory.FullName, "RELEASES");
|
||||||
this.Log().Info($"Reading RELEASES from '{releasesPath}'");
|
_logger.Info($"Reading RELEASES from '{releasesPath}'");
|
||||||
var fi = new FileInfo(releasesPath);
|
var fi = new FileInfo(releasesPath);
|
||||||
|
|
||||||
if (fi.Exists) {
|
if (fi.Exists) {
|
||||||
@@ -39,7 +42,7 @@ namespace Squirrel.Sources
|
|||||||
} else {
|
} else {
|
||||||
var packages = BaseDirectory.EnumerateFiles("*.nupkg");
|
var packages = BaseDirectory.EnumerateFiles("*.nupkg");
|
||||||
if (packages.Any()) {
|
if (packages.Any()) {
|
||||||
this.Log().Warn($"The file '{releasesPath}' does not exist but directory contains packages. " +
|
_logger.Warn($"The file '{releasesPath}' does not exist but directory contains packages. " +
|
||||||
$"This is not valid but attempting to proceed anyway by writing new file.");
|
$"This is not valid but attempting to proceed anyway by writing new file.");
|
||||||
return Task.FromResult(ReleaseEntry.BuildReleasesFile(BaseDirectory.FullName).ToArray());
|
return Task.FromResult(ReleaseEntry.BuildReleasesFile(BaseDirectory.FullName).ToArray());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Squirrel.SimpleSplat;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Squirrel.Sources
|
namespace Squirrel.Sources
|
||||||
{
|
{
|
||||||
@@ -13,6 +13,8 @@ namespace Squirrel.Sources
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SimpleWebSource : IUpdateSource
|
public class SimpleWebSource : IUpdateSource
|
||||||
{
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
/// <summary> The URL of the server hosting packages to update to. </summary>
|
/// <summary> The URL of the server hosting packages to update to. </summary>
|
||||||
public virtual Uri BaseUri { get; }
|
public virtual Uri BaseUri { get; }
|
||||||
|
|
||||||
@@ -20,13 +22,14 @@ namespace Squirrel.Sources
|
|||||||
public virtual IFileDownloader Downloader { get; }
|
public virtual IFileDownloader Downloader { get; }
|
||||||
|
|
||||||
/// <inheritdoc cref="SimpleWebSource" />
|
/// <inheritdoc cref="SimpleWebSource" />
|
||||||
public SimpleWebSource(string baseUrl, IFileDownloader downloader = null)
|
public SimpleWebSource(ILogger logger, string baseUrl, IFileDownloader downloader = null)
|
||||||
: this(new Uri(baseUrl), downloader)
|
: this(logger, new Uri(baseUrl), downloader)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
/// <inheritdoc cref="SimpleWebSource" />
|
/// <inheritdoc cref="SimpleWebSource" />
|
||||||
public SimpleWebSource(Uri baseUri, IFileDownloader downloader = null)
|
public SimpleWebSource(ILogger logger, Uri baseUri, IFileDownloader downloader = null)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
BaseUri = baseUri;
|
BaseUri = baseUri;
|
||||||
Downloader = downloader ?? Utility.CreateDefaultDownloader();
|
Downloader = downloader ?? Utility.CreateDefaultDownloader();
|
||||||
}
|
}
|
||||||
@@ -54,7 +57,7 @@ namespace Squirrel.Sources
|
|||||||
|
|
||||||
var uriAndQuery = Utility.AddQueryParamsToUri(uri, args);
|
var uriAndQuery = Utility.AddQueryParamsToUri(uri, args);
|
||||||
|
|
||||||
this.Log().Info($"Downloading RELEASES from '{uriAndQuery}'.");
|
_logger.Info($"Downloading RELEASES from '{uriAndQuery}'.");
|
||||||
|
|
||||||
var bytes = await Downloader.DownloadBytes(uriAndQuery.ToString()).ConfigureAwait(false);
|
var bytes = await Downloader.DownloadBytes(uriAndQuery.ToString()).ConfigureAwait(false);
|
||||||
var txt = Utility.RemoveByteOrderMarkerIfPresent(bytes);
|
var txt = Utility.RemoveByteOrderMarkerIfPresent(bytes);
|
||||||
@@ -83,7 +86,7 @@ namespace Squirrel.Sources
|
|||||||
? new Uri(sourceBaseUri, releaseUri).ToString()
|
? new Uri(sourceBaseUri, releaseUri).ToString()
|
||||||
: Utility.AppendPathToUri(sourceBaseUri, releaseUri).ToString();
|
: Utility.AppendPathToUri(sourceBaseUri, releaseUri).ToString();
|
||||||
|
|
||||||
this.Log().Info($"Downloading '{releaseEntry.Filename}' from '{source}'.");
|
_logger.Info($"Downloading '{releaseEntry.Filename}' from '{source}'.");
|
||||||
return Downloader.DownloadFile(source, localFile, progress);
|
return Downloader.DownloadFile(source, localFile, progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
|
|
||||||
namespace Squirrel.Sources
|
|
||||||
{
|
|
||||||
/// This class is obsolete. Use <see cref="HttpClientFileDownloader"/> instead.
|
|
||||||
[Obsolete("Use HttpClientFileDownloader")]
|
|
||||||
public class WebClientFileDownloader : IFileDownloader
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual async Task DownloadFile(string url, string targetFile, Action<int> progress, string authorization, string accept)
|
|
||||||
{
|
|
||||||
using (var wc = CreateWebClient(authorization, accept)) {
|
|
||||||
var failedUrl = default(string);
|
|
||||||
|
|
||||||
var lastSignalled = DateTime.MinValue;
|
|
||||||
wc.DownloadProgressChanged += (sender, args) => {
|
|
||||||
var now = DateTime.Now;
|
|
||||||
|
|
||||||
if (now - lastSignalled > TimeSpan.FromMilliseconds(500)) {
|
|
||||||
lastSignalled = now;
|
|
||||||
progress(args.ProgressPercentage);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
retry:
|
|
||||||
try {
|
|
||||||
this.Log().Info("Downloading file: " + (failedUrl ?? url));
|
|
||||||
|
|
||||||
await this.WarnIfThrows(
|
|
||||||
async () => {
|
|
||||||
await wc.DownloadFileTaskAsync(failedUrl ?? url, targetFile).ConfigureAwait(false);
|
|
||||||
progress(100);
|
|
||||||
},
|
|
||||||
"Failed downloading URL: " + (failedUrl ?? url)).ConfigureAwait(false);
|
|
||||||
} catch (Exception) {
|
|
||||||
// NB: Some super brain-dead services are case-sensitive yet
|
|
||||||
// corrupt case on upload. I can't even.
|
|
||||||
if (failedUrl != null) throw;
|
|
||||||
|
|
||||||
failedUrl = url.ToLower();
|
|
||||||
progress(0);
|
|
||||||
goto retry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual async Task<byte[]> DownloadBytes(string url, string authorization, string accept)
|
|
||||||
{
|
|
||||||
using (var wc = CreateWebClient(authorization, accept)) {
|
|
||||||
var failedUrl = default(string);
|
|
||||||
|
|
||||||
retry:
|
|
||||||
try {
|
|
||||||
this.Log().Info("Downloading url: " + (failedUrl ?? url));
|
|
||||||
|
|
||||||
return await this.WarnIfThrows(() => wc.DownloadDataTaskAsync(failedUrl ?? url),
|
|
||||||
"Failed to download url: " + (failedUrl ?? url)).ConfigureAwait(false);
|
|
||||||
} catch (Exception) {
|
|
||||||
// NB: Some super brain-dead services are case-sensitive yet
|
|
||||||
// corrupt case on upload. I can't even.
|
|
||||||
if (failedUrl != null) throw;
|
|
||||||
|
|
||||||
failedUrl = url.ToLower();
|
|
||||||
goto retry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual async Task<string> DownloadString(string url, string authorization, string accept)
|
|
||||||
{
|
|
||||||
using (var wc = CreateWebClient(authorization, accept)) {
|
|
||||||
var failedUrl = default(string);
|
|
||||||
|
|
||||||
retry:
|
|
||||||
try {
|
|
||||||
this.Log().Info("Downloading url: " + (failedUrl ?? url));
|
|
||||||
|
|
||||||
return await this.WarnIfThrows(() => wc.DownloadStringTaskAsync(failedUrl ?? url),
|
|
||||||
"Failed to download url: " + (failedUrl ?? url)).ConfigureAwait(false);
|
|
||||||
} catch (Exception) {
|
|
||||||
// NB: Some super brain-dead services are case-sensitive yet
|
|
||||||
// corrupt case on upload. I can't even.
|
|
||||||
if (failedUrl != null) throw;
|
|
||||||
|
|
||||||
failedUrl = url.ToLower();
|
|
||||||
goto retry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates and returns a new WebClient for every requst
|
|
||||||
/// </summary>
|
|
||||||
protected virtual WebClient CreateWebClient(string authorization, string accept)
|
|
||||||
{
|
|
||||||
var ret = new WebClient();
|
|
||||||
var wp = WebRequest.DefaultWebProxy;
|
|
||||||
if (wp != null) {
|
|
||||||
wp.Credentials = CredentialCache.DefaultCredentials;
|
|
||||||
ret.Proxy = wp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authorization != null)
|
|
||||||
ret.Headers.Add("Authorization", authorization);
|
|
||||||
|
|
||||||
if (accept != null)
|
|
||||||
ret.Headers.Add("Accept", accept);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user