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 version.json = version.json
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine", "src\Squirrel.CommandLine\Squirrel.CommandLine.csproj", "{352C15EA-622F-4132-80D8-9B6E3C83404E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Packaging", "src\Squirrel.Packaging\Squirrel.Packaging.csproj", "{352C15EA-622F-4132-80D8-9B6E3C83404E}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Tool", "src\Squirrel.Tool\Squirrel.Tool.csproj", "{9E769C7E-A54C-4844-8362-727D37BB1578}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Csq", "src\Squirrel.Csq\Squirrel.Csq.csproj", "{9E769C7E-A54C-4844-8362-727D37BB1578}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine.Tests", "test\Squirrel.CommandLine.Tests\Squirrel.CommandLine.Tests.csproj", "{519EAB50-47B8-425F-8B20-AB9548F220B4}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine.Tests", "test\Squirrel.CommandLine.Tests\Squirrel.CommandLine.Tests.csproj", "{519EAB50-47B8-425F-8B20-AB9548F220B4}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7AC3A776-B582-4B65-9D03-BD52332B5CA3}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7AC3A776-B582-4B65-9D03-BD52332B5CA3}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Packaging.Windows", "src\Squirrel.Packaging.Windows\Squirrel.Packaging.Windows.csproj", "{E35039C8-1F98-48EB-B7D5-08E33DF061A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Packaging.OSX", "src\Squirrel.Packaging.OSX\Squirrel.Packaging.OSX.csproj", "{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Deployment", "src\Squirrel.Deployment\Squirrel.Deployment.csproj", "{D19EA72C-E7AE-4A7B-924A-E7550901A49C}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -52,6 +58,18 @@ Global
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {519EAB50-47B8-425F-8B20-AB9548F220B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.Build.0 = Release|Any CPU {519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.Build.0 = Release|Any CPU
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Release|Any CPU.Build.0 = Release|Any CPU
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Release|Any CPU.Build.0 = Release|Any CPU
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

8
nuget.config Normal file
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> <AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<LangVersion>9</LangVersion> <LangVersion>latest</LangVersion>
<SignAssembly>True</SignAssembly> <SignAssembly>True</SignAssembly>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<AssemblyOriginatorKeyFile>..\..\Squirrel.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>..\..\Squirrel.snk</AssemblyOriginatorKeyFile>

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.Collections.Generic;
using System.CommandLine; using System.CommandLine;
using System.CommandLine.Builder; using System.CommandLine.Builder;
@@ -12,8 +13,9 @@ using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.Build.Construction; using Microsoft.Build.Construction;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Squirrel.CommandLine; using Squirrel.CommandLine;
using LogLevel = Squirrel.SimpleSplat.LogLevel;
namespace Squirrel.Tool namespace Squirrel.Tool
{ {
@@ -23,18 +25,26 @@ namespace Squirrel.Tool
private static ConsoleLogger _logger; private static ConsoleLogger _logger;
private static Option<string> CsqVersion { get; } private static CliOption<string> CsqVersion { get; }
= new Option<string>("--csq-version"); = new CliOption<string>("--csq-version");
private static Option<FileSystemInfo> CsqSolutionPath { get; } private static CliOption<FileSystemInfo> CsqSolutionPath { get; }
= new Option<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly(); = new CliOption<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly();
private static Option<bool> Verbose { get; } private static CliOption<bool> Verbose { get; }
= new Option<bool>("--verbose"); = new CliOption<bool>("--verbose");
static Task<int> Main(string[] inargs) static Task<int> Main(string[] args)
{ {
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
builder.Configuration.AddCommandLine(args);
_logger = ConsoleLogger.RegisterLogger(); _logger = ConsoleLogger.RegisterLogger();
RootCommand rootCommand = new RootCommand() { System.CommandLine.Hosting.HostingExtensions.
CliRootCommand rootCommand = new CliRootCommand() {
CsqVersion, CsqVersion,
CsqSolutionPath, CsqSolutionPath,
Verbose Verbose

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> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFrameworks>net6.0</TargetFrameworks>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<AssemblyName>csq</AssemblyName> <AssemblyName>csq</AssemblyName>
<PackageId>csq</PackageId> <PackageId>csq</PackageId>
@@ -13,21 +13,20 @@
<PackAsTool>true</PackAsTool> <PackAsTool>true</PackAsTool>
<Description>A .NET Core Tool that uses the Squirrel framework to create installers and update packages for dotnet applications.</Description> <Description>A .NET Core Tool that uses the Squirrel framework to create installers and update packages for dotnet applications.</Description>
<PackageIcon>Clowd_200.png</PackageIcon> <PackageIcon>Clowd_200.png</PackageIcon>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="..\..\docs\artwork\Clowd_200.png" Pack="true" PackagePath="\" /> <None Include="..\..\docs\artwork\Clowd_200.png" Pack="true" PackagePath="\" />
<Compile Include="..\Squirrel.CommandLine\ConsoleLogger.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Build" Version="17.3.2" /> <PackageReference Include="Microsoft.Build" Version="17.3.2" />
<PackageReference Include="NuGet.Protocol" Version="6.7.0" /> <PackageReference Include="NuGet.Protocol" Version="6.7.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup> <PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<ItemGroup> <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.23407.1" />
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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

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;
using System.IO.Compression; using System.IO.Compression;
using System.Threading; using System.Threading;
// Adapted from https://github.com/LogosBible/bsdiff.net/blob/master/src/bsdiff/BinaryPatchUtility.cs // Adapted from https://github.com/LogosBible/bsdiff.net/blob/master/src/bsdiff/BinaryPatchUtility.cs
namespace Squirrel.Bsdiff namespace Squirrel.Compression
{ {
/* /*
The original bsdiff.c source code (http://www.daemonology.net/bsdiff/) is The original bsdiff.c source code (http://www.daemonology.net/bsdiff/) is
@@ -35,7 +36,7 @@ namespace Squirrel.Bsdiff
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE. POSSIBILITY OF SUCH DAMAGE.
*/ */
class BinaryPatchUtility public class BinaryPatchUtility
{ {
/// <summary> /// <summary>
/// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used /// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used

View File

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

View File

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

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

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.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squirrel.SimpleSplat; using Microsoft.Extensions.Logging;
namespace Squirrel namespace Squirrel
{ {
internal static class PlatformUtil internal static class PlatformUtil
{ {
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(PlatformUtil));
private const string OSX_CSTD_LIB = "libSystem.dylib"; private const string OSX_CSTD_LIB = "libSystem.dylib";
private const string NIX_CSTD_LIB = "libc"; private const string NIX_CSTD_LIB = "libc";
private const string WIN_KERNEL32 = "kernel32.dll"; private const string WIN_KERNEL32 = "kernel32.dll";
@@ -24,84 +22,6 @@ namespace Squirrel
private const string WIN_NTDLL = "NTDLL.DLL"; private const string WIN_NTDLL = "NTDLL.DLL";
private const string WIN_PSAPI = "psapi.dll"; private const string WIN_PSAPI = "psapi.dll";
[SupportedOSPlatform("linux")]
[DllImport(NIX_CSTD_LIB, EntryPoint = "getppid")]
private static extern int nix_getppid();
[SupportedOSPlatform("osx")]
[DllImport(OSX_CSTD_LIB, EntryPoint = "getppid")]
private static extern int osx_getppid();
[SupportedOSPlatform("windows")]
[DllImport(WIN_KERNEL32)]
private static extern IntPtr GetCurrentProcess();
[SupportedOSPlatform("windows")]
[DllImport(WIN_NTDLL, SetLastError = true)]
private static extern int NtQueryInformationProcess(IntPtr hProcess, int pic, ref PROCESS_BASIC_INFORMATION pbi, int cb, out int pSize);
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct PROCESS_BASIC_INFORMATION
{
public nint ExitStatus;
public nint PebBaseAddress;
public nint AffinityMask;
public nint BasePriority;
public nuint UniqueProcessId;
public nint InheritedFromUniqueProcessId;
}
public static Process GetParentProcess()
{
int parentId;
if (SquirrelRuntimeInfo.IsWindows) {
var pbi = new PROCESS_BASIC_INFORMATION();
NtQueryInformationProcess(GetCurrentProcess(), 0, ref pbi, Marshal.SizeOf(typeof(PROCESS_BASIC_INFORMATION)), out _);
parentId = (int) pbi.InheritedFromUniqueProcessId;
} else if (SquirrelRuntimeInfo.IsLinux) {
parentId = nix_getppid();
} else if (SquirrelRuntimeInfo.IsOSX) {
parentId = osx_getppid();
} else {
throw new PlatformNotSupportedException();
}
// the parent process has exited (nix/osx)
if (parentId <= 1)
return null;
try {
var p = Process.GetProcessById(parentId);
// the retrieved process is not our parent, the pid has been reused
if (p.StartTime > Process.GetCurrentProcess().StartTime)
return null;
return p;
} catch (ArgumentException) {
// the process has exited (windows)
return null;
}
}
public static void WaitForParentProcessToExit()
{
var p = GetParentProcess();
if (p == null) {
Log.Warn("Will not wait. Parent process has already exited.");
return;
}
Log.Info($"Waiting for PID {p.Id} to exit (60s timeout)...");
var exited = p.WaitForExit(60_000);
if (!exited) {
throw new Exception("Parent wait timed out.");
}
Log.Info($"PID {p.Id} has exited.");
}
[SupportedOSPlatform("osx")] [SupportedOSPlatform("osx")]
[DllImport(OSX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)] [DllImport(OSX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)]
private static extern int osx_chmod(string pathname, int mode); private static extern int osx_chmod(string pathname, int mode);
@@ -257,14 +177,14 @@ namespace Squirrel
.ToList(); .ToList();
} }
public static void KillProcessesInDirectory(string directoryToKill) public static void KillProcessesInDirectory(ILogger logger, string directoryToKill)
{ {
Log.Info("Killing all processes in " + directoryToKill); logger.Info("Killing all processes in " + directoryToKill);
var myPid = Process.GetCurrentProcess().Id; var myPid = Process.GetCurrentProcess().Id;
int c = 0; int c = 0;
foreach (var x in GetRunningProcessesInDirectory(directoryToKill)) { foreach (var x in GetRunningProcessesInDirectory(directoryToKill)) {
if (myPid == x.ProcessId) { if (myPid == x.ProcessId) {
Log.Info($"Skipping '{x.ProcessExePath}' (is current process)"); logger.Info($"Skipping '{x.ProcessExePath}' (is current process)");
continue; continue;
} }
@@ -272,11 +192,11 @@ namespace Squirrel
Process.GetProcessById(x.ProcessId).Kill(); Process.GetProcessById(x.ProcessId).Kill();
c++; c++;
} catch (Exception ex) { } catch (Exception ex) {
Log.WarnException($"Unable to terminate process (pid.{x.ProcessId})", ex); logger.Warn(ex, $"Unable to terminate process (pid.{x.ProcessId})");
} }
} }
Log.Info($"Terminated {c} processes successfully."); logger.Info($"Terminated {c} processes successfully.");
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]

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.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
@@ -8,8 +8,8 @@ using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Squirrel.NuGet; using Squirrel.NuGet;
using Squirrel.SimpleSplat;
namespace Squirrel namespace Squirrel
{ {
@@ -180,7 +180,7 @@ namespace Squirrel
Contract.Requires(!String.IsNullOrEmpty(to)); Contract.Requires(!String.IsNullOrEmpty(to));
if (!File.Exists(from)) { if (!File.Exists(from)) {
Log().Warn("The file {0} does not exist", from); //Log().Warn("The file {0} does not exist", from);
// TODO: should we fail this operation? // TODO: should we fail this operation?
return; return;
@@ -198,7 +198,7 @@ namespace Squirrel
}, retries, retryDelay); }, retries, retryDelay);
} }
public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250) public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250, ILogger logger = null)
{ {
Contract.Requires(retries > 0); Contract.Requires(retries > 0);
@@ -208,7 +208,7 @@ namespace Squirrel
return ret; return ret;
} catch (Exception ex) { } catch (Exception ex) {
if (retries == 0) throw; if (retries == 0) throw;
Log().Warn($"Operation failed ({ex.Message}). Retrying {retries} more times..."); logger?.Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
retries--; retries--;
Thread.Sleep(retryDelay); Thread.Sleep(retryDelay);
} }
@@ -223,14 +223,14 @@ namespace Squirrel
}, retries, retryDelay); }, retries, retryDelay);
} }
public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250) public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250, ILogger logger = null)
{ {
while (true) { while (true) {
try { try {
return await block().ConfigureAwait(false); return await block().ConfigureAwait(false);
} catch (Exception ex) { } catch (Exception ex) {
if (retries == 0) throw; if (retries == 0) throw;
Log().Warn($"Operation failed ({ex.Message}). Retrying {retries} more times..."); logger?.Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
retries--; retries--;
await Task.Delay(retryDelay).ConfigureAwait(false); await Task.Delay(retryDelay).ConfigureAwait(false);
} }
@@ -346,14 +346,14 @@ namespace Squirrel
/// <param name="throwOnFailure">Whether this function should throw if the delete fails.</param> /// <param name="throwOnFailure">Whether this function should throw if the delete fails.</param>
/// <param name="renameFirst">Try to rename this object first before deleting. Can help prevent partial delete of folders.</param> /// <param name="renameFirst">Try to rename this object first before deleting. Can help prevent partial delete of folders.</param>
/// <returns>True if the file system object was deleted, false otherwise.</returns> /// <returns>True if the file system object was deleted, false otherwise.</returns>
public static bool DeleteFileOrDirectoryHard(string path, bool throwOnFailure = true, bool renameFirst = false) public static bool DeleteFileOrDirectoryHard(string path, bool throwOnFailure = true, bool renameFirst = false, ILogger logger = null)
{ {
Contract.Requires(!String.IsNullOrEmpty(path)); Contract.Requires(!String.IsNullOrEmpty(path));
Log().Debug("Starting to delete: {0}", path); logger?.Debug($"Starting to delete: {path}");
try { try {
if (File.Exists(path)) { if (File.Exists(path)) {
DeleteFsiVeryHard(new FileInfo(path)); DeleteFsiVeryHard(new FileInfo(path), logger);
} else if (Directory.Exists(path)) { } else if (Directory.Exists(path)) {
if (renameFirst) { if (renameFirst) {
// if there are locked files in a directory, we will not attempt to delte it // if there are locked files in a directory, we will not attempt to delte it
@@ -362,26 +362,26 @@ namespace Squirrel
path = oldPath; path = oldPath;
} }
DeleteFsiTree(new DirectoryInfo(path)); DeleteFsiTree(new DirectoryInfo(path), logger);
} else { } else {
if (throwOnFailure) if (throwOnFailure)
Log().Warn($"Cannot delete '{path}' if it does not exist."); logger?.Warn($"Cannot delete '{path}' if it does not exist.");
} }
return true; return true;
} catch (Exception ex) { } catch (Exception ex) {
Log().ErrorException($"Unable to delete '{path}'", ex); logger?.Error(ex, $"Unable to delete '{path}'");
if (throwOnFailure) if (throwOnFailure)
throw; throw;
return false; return false;
} }
} }
private static void DeleteFsiTree(FileSystemInfo fileSystemInfo) private static void DeleteFsiTree(FileSystemInfo fileSystemInfo, ILogger logger)
{ {
// if junction / symlink, don't iterate, just delete it. // if junction / symlink, don't iterate, just delete it.
if (fileSystemInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) { if (fileSystemInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) {
DeleteFsiVeryHard(fileSystemInfo); DeleteFsiVeryHard(fileSystemInfo, logger);
return; return;
} }
@@ -390,19 +390,19 @@ namespace Squirrel
var directoryInfo = fileSystemInfo as DirectoryInfo; var directoryInfo = fileSystemInfo as DirectoryInfo;
if (directoryInfo != null) { if (directoryInfo != null) {
foreach (FileSystemInfo childInfo in directoryInfo.GetFileSystemInfos()) { foreach (FileSystemInfo childInfo in directoryInfo.GetFileSystemInfos()) {
DeleteFsiTree(childInfo); DeleteFsiTree(childInfo, logger);
} }
} }
} catch (Exception ex) { } catch (Exception ex) {
Log().WarnException($"Unable to traverse children of '{fileSystemInfo.FullName}'", ex); logger?.Warn(ex, $"Unable to traverse children of '{fileSystemInfo.FullName}'");
} }
// finally, delete myself, we should try this even if deleting children failed // finally, delete myself, we should try this even if deleting children failed
// because Directory.Delete can also be recursive // because Directory.Delete can also be recursive
DeleteFsiVeryHard(fileSystemInfo); DeleteFsiVeryHard(fileSystemInfo, logger);
} }
private static void DeleteFsiVeryHard(FileSystemInfo fileSystemInfo) private static void DeleteFsiVeryHard(FileSystemInfo fileSystemInfo, ILogger logger)
{ {
// don't try to delete the running process // don't try to delete the running process
if (FullPathEquals(fileSystemInfo.FullName, SquirrelRuntimeInfo.EntryExePath)) if (FullPathEquals(fileSystemInfo.FullName, SquirrelRuntimeInfo.EntryExePath))
@@ -429,7 +429,7 @@ namespace Squirrel
} }
}, retries: 4, retryDelay: 50); }, retries: 4, retryDelay: 50);
} catch (Exception ex) { } catch (Exception ex) {
Log().WarnException($"Unable to delete child '{fileSystemInfo.FullName}'", ex); logger?.Warn(ex, $"Unable to delete child '{fileSystemInfo.FullName}'");
throw; throw;
} }
} }
@@ -539,138 +539,6 @@ namespace Squirrel
return relativePath.Split(Path.DirectorySeparatorChar).Length == 4; return relativePath.Split(Path.DirectorySeparatorChar).Length == 4;
} }
public static void LogIfThrows(this IFullLogger This, LogLevel level, string message, Action block)
{
try {
block();
} catch (Exception ex) {
switch (level) {
case LogLevel.Debug:
This.DebugException(message ?? "", ex);
break;
case LogLevel.Info:
This.InfoException(message ?? "", ex);
break;
case LogLevel.Warn:
This.WarnException(message ?? "", ex);
break;
case LogLevel.Error:
This.ErrorException(message ?? "", ex);
break;
}
throw;
}
}
public static async Task LogIfThrows(this IFullLogger This, LogLevel level, string message, Func<Task> block)
{
try {
await block().ConfigureAwait(false);
} catch (Exception ex) {
switch (level) {
case LogLevel.Debug:
This.DebugException(message ?? "", ex);
break;
case LogLevel.Info:
This.InfoException(message ?? "", ex);
break;
case LogLevel.Warn:
This.WarnException(message ?? "", ex);
break;
case LogLevel.Error:
This.ErrorException(message ?? "", ex);
break;
}
throw;
}
}
public static async Task<T> LogIfThrows<T>(this IFullLogger This, LogLevel level, string message, Func<Task<T>> block)
{
try {
return await block().ConfigureAwait(false);
} catch (Exception ex) {
switch (level) {
case LogLevel.Debug:
This.DebugException(message ?? "", ex);
break;
case LogLevel.Info:
This.InfoException(message ?? "", ex);
break;
case LogLevel.Warn:
This.WarnException(message ?? "", ex);
break;
case LogLevel.Error:
This.ErrorException(message ?? "", ex);
break;
}
throw;
}
}
public static void WarnIfThrows(this IEnableLogger This, Action block, string message = null)
{
This.Log().LogIfThrows(LogLevel.Warn, message, block);
}
public static Task WarnIfThrows(this IEnableLogger This, Func<Task> block, string message = null)
{
return This.Log().LogIfThrows(LogLevel.Warn, message, block);
}
public static Task<T> WarnIfThrows<T>(this IEnableLogger This, Func<Task<T>> block, string message = null)
{
return This.Log().LogIfThrows(LogLevel.Warn, message, block);
}
public static void ErrorIfThrows(this IEnableLogger This, Action block, string message = null)
{
This.Log().LogIfThrows(LogLevel.Error, message, block);
}
public static Task ErrorIfThrows(this IEnableLogger This, Func<Task> block, string message = null)
{
return This.Log().LogIfThrows(LogLevel.Error, message, block);
}
public static Task<T> ErrorIfThrows<T>(this IEnableLogger This, Func<Task<T>> block, string message = null)
{
return This.Log().LogIfThrows(LogLevel.Error, message, block);
}
public static void WarnIfThrows(this IFullLogger This, Action block, string message = null)
{
This.LogIfThrows(LogLevel.Warn, message, block);
}
public static Task WarnIfThrows(this IFullLogger This, Func<Task> block, string message = null)
{
return This.LogIfThrows(LogLevel.Warn, message, block);
}
public static Task<T> WarnIfThrows<T>(this IFullLogger This, Func<Task<T>> block, string message = null)
{
return This.LogIfThrows(LogLevel.Warn, message, block);
}
public static void ErrorIfThrows(this IFullLogger This, Action block, string message = null)
{
This.LogIfThrows(LogLevel.Error, message, block);
}
public static Task ErrorIfThrows(this IFullLogger This, Func<Task> block, string message = null)
{
return This.LogIfThrows(LogLevel.Error, message, block);
}
public static Task<T> ErrorIfThrows<T>(this IFullLogger This, Func<Task<T>> block, string message = null)
{
return This.LogIfThrows(LogLevel.Error, message, block);
}
public static void ConsoleWriteWithColor(string text, ConsoleColor color) public static void ConsoleWriteWithColor(string text, ConsoleColor color)
{ {
var fc = Console.ForegroundColor; var fc = Console.ForegroundColor;
@@ -679,14 +547,6 @@ namespace Squirrel
Console.ForegroundColor = fc; Console.ForegroundColor = fc;
} }
static IFullLogger logger;
static IFullLogger Log()
{
return logger ??
(logger = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(Utility)));
}
public static Guid CreateGuidFromHash(string text) public static Guid CreateGuidFromHash(string text)
{ {
return CreateGuidFromHash(text, Utility.IsoOidNamespace); return CreateGuidFromHash(text, Utility.IsoOidNamespace);

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.Runtime.Versioning;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squirrel.SimpleSplat; using Microsoft.Extensions.Logging;
namespace Squirrel.NuGet namespace Squirrel.NuGet
{ {
@@ -90,13 +90,13 @@ namespace Squirrel.NuGet
.ToArray(); .ToArray();
} }
public static Task ExtractZipReleaseForInstall(string zipFilePath, string outFolder, string rootPackageFolder, Action<int> progress) public static Task ExtractZipReleaseForInstall(ILogger logger, string zipFilePath, string outFolder, string rootPackageFolder, Action<int> progress)
{ {
if (SquirrelRuntimeInfo.IsWindows) if (SquirrelRuntimeInfo.IsWindows)
return ExtractZipReleaseForInstallWindows(zipFilePath, outFolder, rootPackageFolder, progress); return ExtractZipReleaseForInstallWindows(logger, zipFilePath, outFolder, rootPackageFolder, progress);
if (SquirrelRuntimeInfo.IsOSX) if (SquirrelRuntimeInfo.IsOSX)
return ExtractZipReleaseForInstallOSX(zipFilePath, outFolder, progress); return ExtractZipReleaseForInstallOSX(logger, zipFilePath, outFolder, progress);
throw new NotSupportedException("Platform not supported."); throw new NotSupportedException("Platform not supported.");
} }
@@ -105,7 +105,7 @@ namespace Squirrel.NuGet
new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled); new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
public static Task ExtractZipReleaseForInstallOSX(string zipFilePath, string outFinalFolder, Action<int> progress) public static Task ExtractZipReleaseForInstallOSX(ILogger logger, string zipFilePath, string outFinalFolder, Action<int> progress)
{ {
if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist"); if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist");
progress ??= ((_) => { }); progress ??= ((_) => { });
@@ -161,7 +161,7 @@ namespace Squirrel.NuGet
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
public static Task ExtractZipReleaseForInstallWindows(string zipFilePath, string outFinalFolder, string rootPackageFolder, Action<int> progress) public static Task ExtractZipReleaseForInstallWindows(ILogger logger, string zipFilePath, string outFinalFolder, string rootPackageFolder, Action<int> progress)
{ {
if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist"); if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist");
progress ??= ((_) => { }); progress ??= ((_) => { });
@@ -198,13 +198,15 @@ namespace Squirrel.NuGet
var failureIsOkay = false; var failureIsOkay = false;
if (!entry.IsDirectory() && decoded.Contains("_ExecutionStub.exe")) { if (!entry.IsDirectory() && decoded.Contains("_ExecutionStub.exe")) {
// NB: On upgrade, many of these stubs will be in-use, nbd tho. // NB: On upgrade, many of these stubs will be in-use, nbd tho.
failureIsOkay = true; //failureIsOkay = true;
fullTargetFile = Path.Combine( //fullTargetFile = Path.Combine(
rootPackageFolder, // rootPackageFolder,
Path.GetFileName(decoded).Replace("_ExecutionStub.exe", ".exe")); // Path.GetFileName(decoded).Replace("_ExecutionStub.exe", ".exe"));
LogHost.Default.Info("Rigging execution stub for {0} to {1}", decoded, fullTargetFile); //logger.Info($"Rigging execution stub for {decoded} to {fullTargetFile}");
logger.Info($"Skipping obsolete stub {decoded}");
continue;
} }
if (Utility.PathPartEquals(parts.Last(), "app.ico")) { if (Utility.PathPartEquals(parts.Last(), "app.ico")) {
@@ -222,7 +224,7 @@ namespace Squirrel.NuGet
}); });
} catch (Exception e) { } catch (Exception e) {
if (!failureIsOkay) throw; if (!failureIsOkay) throw;
LogHost.Default.WarnException("Can't write execution stub, probably in use", e); logger.Warn(e, "Can't write execution stub, probably in use");
} }
} }

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

View File

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

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squirrel.SimpleSplat;
namespace Squirrel.Sources namespace Squirrel.Sources
{ {
@@ -9,7 +8,7 @@ namespace Squirrel.Sources
/// An implementation may copy a file from a local repository, download from a web address, /// An implementation may copy a file from a local repository, download from a web address,
/// or even use third party services and parse proprietary data to produce a package feed. /// or even use third party services and parse proprietary data to produce a package feed.
/// </summary> /// </summary>
public interface IUpdateSource : IEnableLogger public interface IUpdateSource
{ {
/// <summary> /// <summary>
/// Retrieve the list of available remote releases from the package source. These releases /// Retrieve the list of available remote releases from the package source. These releases

View File

@@ -3,7 +3,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squirrel.SimpleSplat; using Microsoft.Extensions.Logging;
namespace Squirrel.Sources namespace Squirrel.Sources
{ {
@@ -13,13 +13,16 @@ namespace Squirrel.Sources
/// </summary> /// </summary>
public class SimpleFileSource : IUpdateSource public class SimpleFileSource : IUpdateSource
{ {
private readonly ILogger _logger;
/// <summary> The local directory containing packages to update to. </summary> /// <summary> The local directory containing packages to update to. </summary>
public virtual DirectoryInfo BaseDirectory { get; } public virtual DirectoryInfo BaseDirectory { get; }
/// <inheritdoc cref="SimpleFileSource" /> /// <inheritdoc cref="SimpleFileSource" />
/// <param name="baseDirectory">The directory where to search for packages.</param> /// <param name="baseDirectory">The directory where to search for packages.</param>
public SimpleFileSource(DirectoryInfo baseDirectory) public SimpleFileSource(ILogger logger, DirectoryInfo baseDirectory)
{ {
_logger = logger;
BaseDirectory = baseDirectory; BaseDirectory = baseDirectory;
} }
@@ -30,7 +33,7 @@ namespace Squirrel.Sources
throw new Exception($"The local update directory '{BaseDirectory.FullName}' does not exist."); throw new Exception($"The local update directory '{BaseDirectory.FullName}' does not exist.");
var releasesPath = Path.Combine(BaseDirectory.FullName, "RELEASES"); var releasesPath = Path.Combine(BaseDirectory.FullName, "RELEASES");
this.Log().Info($"Reading RELEASES from '{releasesPath}'"); _logger.Info($"Reading RELEASES from '{releasesPath}'");
var fi = new FileInfo(releasesPath); var fi = new FileInfo(releasesPath);
if (fi.Exists) { if (fi.Exists) {
@@ -39,7 +42,7 @@ namespace Squirrel.Sources
} else { } else {
var packages = BaseDirectory.EnumerateFiles("*.nupkg"); var packages = BaseDirectory.EnumerateFiles("*.nupkg");
if (packages.Any()) { if (packages.Any()) {
this.Log().Warn($"The file '{releasesPath}' does not exist but directory contains packages. " + _logger.Warn($"The file '{releasesPath}' does not exist but directory contains packages. " +
$"This is not valid but attempting to proceed anyway by writing new file."); $"This is not valid but attempting to proceed anyway by writing new file.");
return Task.FromResult(ReleaseEntry.BuildReleasesFile(BaseDirectory.FullName).ToArray()); return Task.FromResult(ReleaseEntry.BuildReleasesFile(BaseDirectory.FullName).ToArray());
} else { } else {

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squirrel.SimpleSplat; using Microsoft.Extensions.Logging;
namespace Squirrel.Sources namespace Squirrel.Sources
{ {
@@ -13,6 +13,8 @@ namespace Squirrel.Sources
/// </summary> /// </summary>
public class SimpleWebSource : IUpdateSource public class SimpleWebSource : IUpdateSource
{ {
private readonly ILogger _logger;
/// <summary> The URL of the server hosting packages to update to. </summary> /// <summary> The URL of the server hosting packages to update to. </summary>
public virtual Uri BaseUri { get; } public virtual Uri BaseUri { get; }
@@ -20,13 +22,14 @@ namespace Squirrel.Sources
public virtual IFileDownloader Downloader { get; } public virtual IFileDownloader Downloader { get; }
/// <inheritdoc cref="SimpleWebSource" /> /// <inheritdoc cref="SimpleWebSource" />
public SimpleWebSource(string baseUrl, IFileDownloader downloader = null) public SimpleWebSource(ILogger logger, string baseUrl, IFileDownloader downloader = null)
: this(new Uri(baseUrl), downloader) : this(logger, new Uri(baseUrl), downloader)
{ } { }
/// <inheritdoc cref="SimpleWebSource" /> /// <inheritdoc cref="SimpleWebSource" />
public SimpleWebSource(Uri baseUri, IFileDownloader downloader = null) public SimpleWebSource(ILogger logger, Uri baseUri, IFileDownloader downloader = null)
{ {
_logger = logger;
BaseUri = baseUri; BaseUri = baseUri;
Downloader = downloader ?? Utility.CreateDefaultDownloader(); Downloader = downloader ?? Utility.CreateDefaultDownloader();
} }
@@ -54,7 +57,7 @@ namespace Squirrel.Sources
var uriAndQuery = Utility.AddQueryParamsToUri(uri, args); var uriAndQuery = Utility.AddQueryParamsToUri(uri, args);
this.Log().Info($"Downloading RELEASES from '{uriAndQuery}'."); _logger.Info($"Downloading RELEASES from '{uriAndQuery}'.");
var bytes = await Downloader.DownloadBytes(uriAndQuery.ToString()).ConfigureAwait(false); var bytes = await Downloader.DownloadBytes(uriAndQuery.ToString()).ConfigureAwait(false);
var txt = Utility.RemoveByteOrderMarkerIfPresent(bytes); var txt = Utility.RemoveByteOrderMarkerIfPresent(bytes);
@@ -83,7 +86,7 @@ namespace Squirrel.Sources
? new Uri(sourceBaseUri, releaseUri).ToString() ? new Uri(sourceBaseUri, releaseUri).ToString()
: Utility.AppendPathToUri(sourceBaseUri, releaseUri).ToString(); : Utility.AppendPathToUri(sourceBaseUri, releaseUri).ToString();
this.Log().Info($"Downloading '{releaseEntry.Filename}' from '{source}'."); _logger.Info($"Downloading '{releaseEntry.Filename}' from '{source}'.");
return Downloader.DownloadFile(source, localFile, progress); return Downloader.DownloadFile(source, localFile, progress);
} }
} }

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