WIP: basic restructuring and renames

This commit is contained in:
Caelan Sayler
2023-12-14 14:16:06 +00:00
parent 9ab5c56c9f
commit e4c0aa2200
112 changed files with 1452 additions and 5759 deletions

View File

@@ -18,14 +18,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionLevel", "SolutionLe
version.json = version.json
EndProjectSection
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
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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine.Tests", "test\Squirrel.CommandLine.Tests\Squirrel.CommandLine.Tests.csproj", "{519EAB50-47B8-425F-8B20-AB9548F220B4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7AC3A776-B582-4B65-9D03-BD52332B5CA3}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

8
nuget.config Normal file
View 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>

View File

@@ -9,7 +9,7 @@
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<LangVersion>9</LangVersion>
<LangVersion>latest</LangVersion>
<SignAssembly>True</SignAssembly>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<AssemblyOriginatorKeyFile>..\..\Squirrel.snk</AssemblyOriginatorKeyFile>

View File

@@ -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; }
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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");
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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
}
}

View File

@@ -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)]

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}
}
}

View 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; }
}

View 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();
}
}

View 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();
}
}

View 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");
}
}
}

View 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();
}
}

View 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;
}
}
}
}

View 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();
}
}

View File

@@ -1,4 +1,5 @@
using System;
#if false
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Builder;
@@ -12,8 +13,9 @@ using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Build.Construction;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Squirrel.CommandLine;
using LogLevel = Squirrel.SimpleSplat.LogLevel;
namespace Squirrel.Tool
{
@@ -23,18 +25,26 @@ namespace Squirrel.Tool
private static ConsoleLogger _logger;
private static Option<string> CsqVersion { get; }
= new Option<string>("--csq-version");
private static Option<FileSystemInfo> CsqSolutionPath { get; }
= new Option<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly();
private static Option<bool> Verbose { get; }
= new Option<bool>("--verbose");
private static CliOption<string> CsqVersion { get; }
= new CliOption<string>("--csq-version");
private static CliOption<FileSystemInfo> CsqSolutionPath { get; }
= new CliOption<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly();
private static CliOption<bool> Verbose { get; }
= 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();
RootCommand rootCommand = new RootCommand() {
System.CommandLine.Hosting.HostingExtensions.
CliRootCommand rootCommand = new CliRootCommand() {
CsqVersion,
CsqSolutionPath,
Verbose

View 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;

View 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;
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFrameworks>net6.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<AssemblyName>csq</AssemblyName>
<PackageId>csq</PackageId>
@@ -13,21 +13,20 @@
<PackAsTool>true</PackAsTool>
<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>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\docs\artwork\Clowd_200.png" Pack="true" PackagePath="\" />
<Compile Include="..\Squirrel.CommandLine\ConsoleLogger.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build" Version="17.3.2" />
<PackageReference Include="NuGet.Protocol" Version="6.7.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.23407.1" />
</ItemGroup>
</Project>

View 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);
}
}

View 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>

View File

@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -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);
}
}
}
}

View File

@@ -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.Compression;
namespace Squirrel
namespace Squirrel.Compression
{
internal sealed class BZip2Stream : Stream
public sealed class BZip2Stream : Stream
{
private readonly Stream stream;
private bool isDisposed;

View File

@@ -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.Compression;
using System.Threading;
// 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
@@ -35,7 +36,7 @@ namespace Squirrel.Bsdiff
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
class BinaryPatchUtility
public class BinaryPatchUtility
{
/// <summary>
/// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used

View File

@@ -1,6 +1,6 @@
using System;
namespace Squirrel
namespace Squirrel.Compression
{
/// <summary>
/// Represents an error that occurs when a package does not match it's expected SHA checksum

View File

@@ -1,21 +1,22 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Squirrel.Bsdiff;
using Squirrel.SimpleSplat;
using Microsoft.Extensions.Logging;
namespace Squirrel
namespace Squirrel.Compression
{
internal class DeltaPackage : IEnableLogger
public class DeltaPackage
{
private readonly ILogger _log;
private readonly string _baseTempDir;
public DeltaPackage(string baseTempDir = null)
public DeltaPackage(ILogger logger, string baseTempDir = null)
{
_log = logger;
_baseTempDir = baseTempDir ?? Utility.GetDefaultTempBaseDirectory();
}
@@ -23,15 +24,16 @@ namespace Squirrel
{
progress = progress ?? (x => { });
Contract.Requires(deltaPackageZip != null);
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
if (deltaPackageZip is null) throw new ArgumentNullException(nameof(deltaPackageZip));
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 workingPath, _baseTempDir)) {
EasyZip.ExtractZipToDirectory(deltaPackageZip, deltaPath);
EasyZip.ExtractZipToDirectory(_log, deltaPackageZip, deltaPath);
progress(25);
EasyZip.ExtractZipToDirectory(basePackageZip, workingPath);
EasyZip.ExtractZipToDirectory(_log, basePackageZip, workingPath);
progress(50);
var pathsVisited = new List<string>();
@@ -59,7 +61,7 @@ namespace Squirrel
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(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));
});
@@ -70,13 +72,13 @@ namespace Squirrel
deltaPathRelativePaths
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
.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);
});
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);
}
@@ -88,10 +90,10 @@ namespace Squirrel
{
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);
EasyZip.ExtractZipToDirectory(deltaPackageZip, deltaPath);
EasyZip.ExtractZipToDirectory(_log, deltaPackageZip, deltaPath);
progress(10);
var pathsVisited = new List<string>();
@@ -112,8 +114,8 @@ namespace Squirrel
var file = files[index];
pathsVisited.Add(Regex.Replace(file, @"\.(bs)?diff$", "").ToLowerInvariant());
applyDiffToFile(deltaPath, file, workingPath);
var perc = (index + 1) / (double)files.Length * 100;
Utility.CalculateProgress((int)perc, 10, 90);
var perc = (index + 1) / (double) files.Length * 100;
Utility.CalculateProgress((int) perc, 10, 90);
}
progress(90);
@@ -124,7 +126,7 @@ namespace Squirrel
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(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));
});
@@ -135,7 +137,7 @@ namespace Squirrel
deltaPathRelativePaths
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
.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);
});
@@ -151,20 +153,20 @@ namespace Squirrel
// NB: Zero-length diffs indicate the file hasn't actually changed
if (new FileInfo(inputFile).Length == 0) {
this.Log().Info("{0} exists unchanged, skipping", relativeFilePath);
_log.Info($"{relativeFilePath} exists unchanged, skipping");
return;
}
if (relativeFilePath.EndsWith(".bsdiff", StringComparison.InvariantCultureIgnoreCase)) {
using (var of = File.OpenWrite(tempTargetFile))
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);
}
verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile);
} else if (relativeFilePath.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase)) {
this.Log().Info("Applying msdiff to {0}", relativeFilePath);
_log.Info($"Applying msdiff to {relativeFilePath}");
if (SquirrelRuntimeInfo.IsWindows) {
MsDeltaCompression.ApplyDelta(inputFile, finalTarget, tempTargetFile);
@@ -176,7 +178,7 @@ namespace Squirrel
} else {
using (var of = File.OpenWrite(tempTargetFile))
using (var inf = File.OpenRead(inputFile)) {
this.Log().Info("Adding new file: {0}", relativeFilePath);
_log.Info($"Adding new file: {relativeFilePath}");
inf.CopyTo(of);
}
}
@@ -196,14 +198,12 @@ namespace Squirrel
var actualReleaseEntry = ReleaseEntry.GenerateFromFile(tempTargetFile);
if (expectedReleaseEntry.Filesize != actualReleaseEntry.Filesize) {
this.Log().Warn("Patched file {0} has incorrect size, expected {1}, got {2}", relativeFilePath,
expectedReleaseEntry.Filesize, actualReleaseEntry.Filesize);
_log.Warn($"Patched file {relativeFilePath} has incorrect size, expected {expectedReleaseEntry.Filesize}, got {actualReleaseEntry.Filesize}");
throw new ChecksumFailedException() { Filename = relativeFilePath };
}
if (expectedReleaseEntry.SHA1 != actualReleaseEntry.SHA1) {
this.Log().Warn("Patched file {0} has incorrect SHA1, expected {1}, got {2}", relativeFilePath,
expectedReleaseEntry.SHA1, actualReleaseEntry.SHA1);
_log.Warn($"Patched file {relativeFilePath} has incorrect SHA1, expected {expectedReleaseEntry.SHA1}, got {actualReleaseEntry.SHA1}");
throw new ChecksumFailedException() { Filename = relativeFilePath };
}
}

View 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);
// }
// }
//}
}
}

View File

@@ -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.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Squirrel
namespace Squirrel.Compression
{
[SupportedOSPlatform("windows")]
internal class MsDeltaCompression
public class MsDeltaCompression
{
/// <summary>
/// The ApplyDelta function use the specified delta and source files to create a new copy of the target file.

View 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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -9,14 +9,12 @@ using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Squirrel.SimpleSplat;
using Microsoft.Extensions.Logging;
namespace Squirrel
{
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 NIX_CSTD_LIB = "libc";
private const string WIN_KERNEL32 = "kernel32.dll";
@@ -24,84 +22,6 @@ namespace Squirrel
private const string WIN_NTDLL = "NTDLL.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")]
[DllImport(OSX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)]
private static extern int osx_chmod(string pathname, int mode);
@@ -257,14 +177,14 @@ namespace Squirrel
.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;
int c = 0;
foreach (var x in GetRunningProcessesInDirectory(directoryToKill)) {
if (myPid == x.ProcessId) {
Log.Info($"Skipping '{x.ProcessExePath}' (is current process)");
logger.Info($"Skipping '{x.ProcessExePath}' (is current process)");
continue;
}
@@ -272,11 +192,11 @@ namespace Squirrel
Process.GetProcessById(x.ProcessId).Kill();
c++;
} 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")]

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
@@ -8,8 +8,8 @@ using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Squirrel.NuGet;
using Squirrel.SimpleSplat;
namespace Squirrel
{
@@ -180,7 +180,7 @@ namespace Squirrel
Contract.Requires(!String.IsNullOrEmpty(to));
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?
return;
@@ -198,7 +198,7 @@ namespace Squirrel
}, 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);
@@ -208,7 +208,7 @@ namespace Squirrel
return ret;
} catch (Exception ex) {
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--;
Thread.Sleep(retryDelay);
}
@@ -223,14 +223,14 @@ namespace Squirrel
}, 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) {
try {
return await block().ConfigureAwait(false);
} catch (Exception ex) {
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--;
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="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>
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));
Log().Debug("Starting to delete: {0}", path);
logger?.Debug($"Starting to delete: {path}");
try {
if (File.Exists(path)) {
DeleteFsiVeryHard(new FileInfo(path));
DeleteFsiVeryHard(new FileInfo(path), logger);
} else if (Directory.Exists(path)) {
if (renameFirst) {
// if there are locked files in a directory, we will not attempt to delte it
@@ -362,26 +362,26 @@ namespace Squirrel
path = oldPath;
}
DeleteFsiTree(new DirectoryInfo(path));
DeleteFsiTree(new DirectoryInfo(path), logger);
} else {
if (throwOnFailure)
Log().Warn($"Cannot delete '{path}' if it does not exist.");
logger?.Warn($"Cannot delete '{path}' if it does not exist.");
}
return true;
} catch (Exception ex) {
Log().ErrorException($"Unable to delete '{path}'", ex);
logger?.Error(ex, $"Unable to delete '{path}'");
if (throwOnFailure)
throw;
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 (fileSystemInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) {
DeleteFsiVeryHard(fileSystemInfo);
DeleteFsiVeryHard(fileSystemInfo, logger);
return;
}
@@ -390,19 +390,19 @@ namespace Squirrel
var directoryInfo = fileSystemInfo as DirectoryInfo;
if (directoryInfo != null) {
foreach (FileSystemInfo childInfo in directoryInfo.GetFileSystemInfos()) {
DeleteFsiTree(childInfo);
DeleteFsiTree(childInfo, logger);
}
}
} 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
// 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
if (FullPathEquals(fileSystemInfo.FullName, SquirrelRuntimeInfo.EntryExePath))
@@ -429,7 +429,7 @@ namespace Squirrel
}
}, retries: 4, retryDelay: 50);
} catch (Exception ex) {
Log().WarnException($"Unable to delete child '{fileSystemInfo.FullName}'", ex);
logger?.Warn(ex, $"Unable to delete child '{fileSystemInfo.FullName}'");
throw;
}
}
@@ -539,138 +539,6 @@ namespace Squirrel
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)
{
var fc = Console.ForegroundColor;
@@ -679,14 +547,6 @@ namespace Squirrel
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)
{
return CreateGuidFromHash(text, Utility.IsoOidNamespace);

View File

@@ -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

View 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);
}
}
}

View 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);
}
}
}

View File

@@ -6,7 +6,7 @@ using System.Linq;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Squirrel.SimpleSplat;
using Microsoft.Extensions.Logging;
namespace Squirrel.NuGet
{
@@ -90,13 +90,13 @@ namespace Squirrel.NuGet
.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)
return ExtractZipReleaseForInstallWindows(zipFilePath, outFolder, rootPackageFolder, progress);
return ExtractZipReleaseForInstallWindows(logger, zipFilePath, outFolder, rootPackageFolder, progress);
if (SquirrelRuntimeInfo.IsOSX)
return ExtractZipReleaseForInstallOSX(zipFilePath, outFolder, progress);
return ExtractZipReleaseForInstallOSX(logger, zipFilePath, outFolder, progress);
throw new NotSupportedException("Platform not supported.");
}
@@ -105,7 +105,7 @@ namespace Squirrel.NuGet
new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);
[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");
progress ??= ((_) => { });
@@ -161,7 +161,7 @@ namespace Squirrel.NuGet
}
[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");
progress ??= ((_) => { });
@@ -198,13 +198,15 @@ namespace Squirrel.NuGet
var failureIsOkay = false;
if (!entry.IsDirectory() && decoded.Contains("_ExecutionStub.exe")) {
// NB: On upgrade, many of these stubs will be in-use, nbd tho.
failureIsOkay = true;
//failureIsOkay = true;
fullTargetFile = Path.Combine(
rootPackageFolder,
Path.GetFileName(decoded).Replace("_ExecutionStub.exe", ".exe"));
//fullTargetFile = Path.Combine(
// rootPackageFolder,
// 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")) {
@@ -222,7 +224,7 @@ namespace Squirrel.NuGet
});
} catch (Exception e) {
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");
}
}

View File

@@ -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";
}

View File

@@ -10,7 +10,6 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NuGet.Versioning;
using Squirrel.NuGet;
using Squirrel.SimpleSplat;
namespace Squirrel
{
@@ -79,7 +78,7 @@ namespace Squirrel
/// <inheritdoc cref="IReleaseEntry" />
[DataContract]
public class ReleaseEntry : IEnableLogger, IReleaseEntry
public class ReleaseEntry : IReleaseEntry
{
/// <inheritdoc />
[DataMember] public string SHA1 { get; protected set; }

View File

@@ -1,13 +1,12 @@
using System;
using System.Threading.Tasks;
using Squirrel.SimpleSplat;
namespace Squirrel.Sources
{
/// <summary>
/// A simple abstractable file downloader
/// </summary>
public interface IFileDownloader : IEnableLogger
public interface IFileDownloader
{
/// <summary>
/// Downloads a remote file to the specified local path

View File

@@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using Squirrel.SimpleSplat;
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,
/// or even use third party services and parse proprietary data to produce a package feed.
/// </summary>
public interface IUpdateSource : IEnableLogger
public interface IUpdateSource
{
/// <summary>
/// Retrieve the list of available remote releases from the package source. These releases

View File

@@ -3,7 +3,7 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Squirrel.SimpleSplat;
using Microsoft.Extensions.Logging;
namespace Squirrel.Sources
{
@@ -13,13 +13,16 @@ namespace Squirrel.Sources
/// </summary>
public class SimpleFileSource : IUpdateSource
{
private readonly ILogger _logger;
/// <summary> The local directory containing packages to update to. </summary>
public virtual DirectoryInfo BaseDirectory { get; }
/// <inheritdoc cref="SimpleFileSource" />
/// <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;
}
@@ -30,7 +33,7 @@ namespace Squirrel.Sources
throw new Exception($"The local update directory '{BaseDirectory.FullName}' does not exist.");
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);
if (fi.Exists) {
@@ -39,7 +42,7 @@ namespace Squirrel.Sources
} else {
var packages = BaseDirectory.EnumerateFiles("*.nupkg");
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.");
return Task.FromResult(ReleaseEntry.BuildReleasesFile(BaseDirectory.FullName).ToArray());
} else {

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squirrel.SimpleSplat;
using Microsoft.Extensions.Logging;
namespace Squirrel.Sources
{
@@ -13,6 +13,8 @@ namespace Squirrel.Sources
/// </summary>
public class SimpleWebSource : IUpdateSource
{
private readonly ILogger _logger;
/// <summary> The URL of the server hosting packages to update to. </summary>
public virtual Uri BaseUri { get; }
@@ -20,13 +22,14 @@ namespace Squirrel.Sources
public virtual IFileDownloader Downloader { get; }
/// <inheritdoc cref="SimpleWebSource" />
public SimpleWebSource(string baseUrl, IFileDownloader downloader = null)
: this(new Uri(baseUrl), downloader)
public SimpleWebSource(ILogger logger, string baseUrl, IFileDownloader downloader = null)
: this(logger, new Uri(baseUrl), downloader)
{ }
/// <inheritdoc cref="SimpleWebSource" />
public SimpleWebSource(Uri baseUri, IFileDownloader downloader = null)
public SimpleWebSource(ILogger logger, Uri baseUri, IFileDownloader downloader = null)
{
_logger = logger;
BaseUri = baseUri;
Downloader = downloader ?? Utility.CreateDefaultDownloader();
}
@@ -54,7 +57,7 @@ namespace Squirrel.Sources
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 txt = Utility.RemoveByteOrderMarkerIfPresent(bytes);
@@ -83,7 +86,7 @@ namespace Squirrel.Sources
? new Uri(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);
}
}

View File

@@ -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