From 7dee4ca1bf24f398857fd587976b6eee73d9cc6b Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Fri, 19 Aug 2022 09:01:22 +0100 Subject: [PATCH] csq supports running any squirrel version from nuget instead of containing an embedded version --- build.ps1 | 14 +- .../Squirrel.CommandLine.csproj | 2 + src/Squirrel.Tool/NugetDownloader.cs | 75 +++++ src/Squirrel.Tool/Program.cs | 266 ++++++++++++------ src/Squirrel.Tool/Squirrel.Tool.csproj | 7 +- src/Squirrel/SquirrelRuntimeInfo.cs | 3 +- version.json | 2 +- 7 files changed, 271 insertions(+), 98 deletions(-) create mode 100644 src/Squirrel.Tool/NugetDownloader.cs diff --git a/build.ps1 b/build.ps1 index b9b587e4d..bac608b7f 100644 --- a/build.ps1 +++ b/build.ps1 @@ -35,30 +35,24 @@ msbuild /verbosity:minimal /restore /p:Configuration=Release # Build single-exe packaged projects and drop into nupkg Write-Host "Extracting Generated Packages" -ForegroundColor Magenta Set-Location "$PSScriptRoot\build\Release" -seven x csq*.nupkg -ocsq seven x Clowd.Squirrel*.nupkg -osquirrel -Remove-Item *.nupkg +Remove-Item Clowd.Squirrel*.nupkg Write-Host "Publishing SingleFile Projects" -ForegroundColor Magenta -$ToolsDir = "csq\tools\net6.0\any" +$ToolsDir = "squirrel\tools" +dotnet publish -v minimal --no-build -c Release --no-self-contained "$PSScriptRoot\src\Squirrel.CommandLine\Squirrel.CommandLine.csproj" -o "$ToolsDir" dotnet publish -v minimal --no-build -c Release --self-contained "$PSScriptRoot\src\Update.Windows\Update.Windows.csproj" -o "$ToolsDir" dotnet publish -v minimal --no-build -c Release --self-contained "$PSScriptRoot\src\Update.OSX\Update.OSX.csproj" -o "$ToolsDir" Write-Host "Copying Tools" -ForegroundColor Magenta -# First, copy all the tools into the 'csq' package +# Copy all the tools into the 'csq' package Copy-Item -Path "$PSScriptRoot\vendor\*" -Destination $ToolsDir -Recurse Copy-Item -Path "Win32\*" -Destination $ToolsDir Copy-Item -Path "$PSScriptRoot\Squirrel.entitlements" -Destination "$ToolsDir" Remove-Item "$ToolsDir\*.pdb" Remove-Item "$ToolsDir\7za.exe" -# Second, copy all the csq files into the 'squirrel' package -New-Item -Path "squirrel" -Name "tools" -ItemType "directory" -Copy-Item -Path "$ToolsDir\*" -Destination "squirrel\tools" -Recurse -Remove-Item "squirrel\tools\*.xml" - Write-Host "Re-assembling Packages" -ForegroundColor Magenta -seven a "csq.$version.nupkg" -tzip -mx9 "$PSScriptRoot\build\Release\csq\*" seven a "Clowd.Squirrel.$version.nupkg" -tzip -mx9 "$PSScriptRoot\build\Release\squirrel\*" Write-Host "Done." -ForegroundColor Magenta diff --git a/src/Squirrel.CommandLine/Squirrel.CommandLine.csproj b/src/Squirrel.CommandLine/Squirrel.CommandLine.csproj index c0042b1bb..bb4954d6f 100644 --- a/src/Squirrel.CommandLine/Squirrel.CommandLine.csproj +++ b/src/Squirrel.CommandLine/Squirrel.CommandLine.csproj @@ -2,6 +2,8 @@ net6.0 + Exe + false true $(NoWarn);CA2007 diff --git a/src/Squirrel.Tool/NugetDownloader.cs b/src/Squirrel.Tool/NugetDownloader.cs new file mode 100644 index 000000000..bcd9f2479 --- /dev/null +++ b/src/Squirrel.Tool/NugetDownloader.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace Squirrel.Tool +{ + public class NugetDownloader + { + private readonly ILogger _logger; + private readonly PackageSource _packageSource; + private readonly SourceRepository _sourceRepository; + private readonly SourceCacheContext _sourceCacheContext; + + public NugetDownloader(ILogger logger) + { + _logger = logger; + _packageSource = new PackageSource("https://api.nuget.org/v3/index.json", "NuGet.org"); + _sourceRepository = new SourceRepository(_packageSource, Repository.Provider.GetCoreV3()); + _sourceCacheContext = new SourceCacheContext(); + } + + public IPackageSearchMetadata GetPackageMetadata(string packageName, string version) + { + PackageMetadataResource packageMetadataResource = _sourceRepository.GetResource(); + FindPackageByIdResource packageByIdResource = _sourceRepository.GetResource(); + IPackageSearchMetadata package = null; + + var prerelease = version?.Equals("pre", StringComparison.InvariantCultureIgnoreCase) == true; + if (version == null || version.Equals("latest", StringComparison.InvariantCultureIgnoreCase) || prerelease) { + // get latest (or prerelease) version + IEnumerable metadata = packageMetadataResource + .GetMetadataAsync(packageName, true, true, _sourceCacheContext, _logger, CancellationToken.None) + .GetAwaiter().GetResult(); + package = metadata + .Where(x => x.IsListed) + .Where(x => prerelease || !x.Identity.Version.IsPrerelease) + .OrderByDescending(x => x.Identity.Version) + .FirstOrDefault(); + } else { + // resolve version ranges and wildcards + var versions = packageByIdResource.GetAllVersionsAsync(packageName, _sourceCacheContext, _logger, CancellationToken.None) + .GetAwaiter().GetResult(); + var resolved = versions.FindBestMatch(VersionRange.Parse(version), version => version); + + // get exact version + var packageIdentity = new PackageIdentity(packageName, resolved); + package = packageMetadataResource + .GetMetadataAsync(packageIdentity, _sourceCacheContext, _logger, CancellationToken.None) + .GetAwaiter().GetResult(); + } + + if (package == null) { + throw new Exception($"Unable to locate {packageName} {version} on NuGet.org"); + } + + return package; + } + + public void DownloadPackageToStream(IPackageSearchMetadata package, Stream targetStream) + { + FindPackageByIdResource packageByIdResource = _sourceRepository.GetResource(); + packageByIdResource + .CopyNupkgToStreamAsync(package.Identity.Id, package.Identity.Version, targetStream, _sourceCacheContext, _logger, CancellationToken.None) + .GetAwaiter().GetResult(); + } + } +} \ No newline at end of file diff --git a/src/Squirrel.Tool/Program.cs b/src/Squirrel.Tool/Program.cs index 2b472f023..6ef4d6288 100644 --- a/src/Squirrel.Tool/Program.cs +++ b/src/Squirrel.Tool/Program.cs @@ -3,18 +3,28 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; using Microsoft.Build.Construction; using Mono.Options; +using NuGet.Common; using NuGet.Versioning; namespace Squirrel.Tool { - class Program + class Program : ILogger { private static bool Verbose { get; set; } +#pragma warning disable CS0436 + public static string SquirrelDisplayVersion => ThisAssembly.AssemblyInformationalVersion + (ThisAssembly.IsPublicRelease ? "" : " (prerelease)"); + public static NuGetVersion SquirrelNugetVersion => NuGetVersion.Parse(ThisAssembly.AssemblyInformationalVersion); +#pragma warning restore CS0436 + + const string CLOWD_PACKAGE_NAME = "Clowd.Squirrel"; + static int Main(string[] inargs) { try { @@ -27,132 +37,162 @@ static int Main(string[] inargs) static int MainInner(string[] inargs) { - string explicitSquirrelPath = null; string explicitSolutionPath = null; - bool useEmbedded = false; - + string explicitSquirrelVersion = null; var toolOptions = new OptionSet() { - { "q|csq-embedded", _ => useEmbedded = true }, - { "csq-path=", v => explicitSquirrelPath = v }, + { "csq-version=", v => explicitSquirrelVersion = v }, { "csq-sln=", v => explicitSolutionPath = v }, { "csq-verbose", _ => Verbose = true }, }; var restArgs = toolOptions.Parse(inargs).ToArray(); - Write(SquirrelRuntimeInfo.SquirrelDisplayVersion, true); + Console.WriteLine($"Squirrel Locator 'csq' {SquirrelDisplayVersion}"); + Write($"Entry EXE: {SquirrelRuntimeInfo.EntryExePath}", true); + + CheckForUpdates(); + + var solutionDir = FindSolutionDirectory(explicitSolutionPath); + var nugetPackagesDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); + var cacheDir = Path.GetFullPath(solutionDir == null ? ".squirrel" : Path.Combine(solutionDir, ".squirrel")); + + Dictionary packageSearchPaths = new(); + packageSearchPaths.Add("nuget user profile cache", Path.Combine(nugetPackagesDir, CLOWD_PACKAGE_NAME.ToLower(), "{0}", "tools")); + if (solutionDir != null) + packageSearchPaths.Add("visual studio packages cache", Path.Combine(solutionDir, "packages", CLOWD_PACKAGE_NAME + ".{0}", "tools")); + packageSearchPaths.Add("squirrel cache", Path.Combine(cacheDir, "{0}", "tools")); + + int runSquirrel(string version) + { + foreach (var kvp in packageSearchPaths) { + var path = String.Format(kvp.Value, version); + if (Directory.Exists(path)) { + Write($"Running {CLOWD_PACKAGE_NAME} {version} from {kvp.Key}", false); + return RunCsqFromPath(path, restArgs); + } + } + + // we did not find it locally on first pass, search for the package online + var dl = new NugetDownloader(new Program()); + var package = dl.GetPackageMetadata(CLOWD_PACKAGE_NAME, version); + + // search one more time now that we've potentially resolved the nuget version + foreach (var kvp in packageSearchPaths) { + var path = String.Format(kvp.Value, package.Identity.Version); + if (Directory.Exists(path)) { + Write($"Running {CLOWD_PACKAGE_NAME} {package.Identity.Version} from {kvp.Key}", false); + return RunCsqFromPath(path, restArgs); + } + } + + // let's try to download it from NuGet.org + var versionDir = Path.Combine(cacheDir, package.Identity.Version.ToString()); + if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir); + if (!Directory.Exists(versionDir)) Directory.CreateDirectory(versionDir); - // explicitly told to execute embedded version - if (useEmbedded) { - Write("using embedded (--csq-embedded)", true); - return CommandLine.SquirrelHost.Main(restArgs); + Write($"Downloading {package.Identity} from NuGet.", false); + + var filePath = Path.Combine(versionDir, package.Identity + ".nupkg"); + using (var fs = File.Create(filePath)) + dl.DownloadPackageToStream(package, fs); + + EasyZip.ExtractZipToDirectory(filePath, versionDir); + + var toolsPath = Path.Combine(versionDir, "tools"); + return RunCsqFromPath(toolsPath, restArgs); } - // explicitly told to use specific version at this directory - if (explicitSquirrelPath != null) { - return RunCsqFromPath(explicitSquirrelPath, restArgs); + if (explicitSquirrelVersion != null) { + return runSquirrel(explicitSquirrelVersion); } - // try to find the solution directory from cwd - string slnDir; - if (File.Exists(explicitSolutionPath) && explicitSolutionPath.EndsWith(".sln", StringComparison.InvariantCultureIgnoreCase)) { - slnDir = Path.GetDirectoryName(Path.GetFullPath(explicitSolutionPath)); - } else { - var cwd = Environment.CurrentDirectory; - var slnSearchDirs = new string[] { - cwd, - Path.Combine(cwd, ".."), - Path.Combine(cwd, "..", ".."), - }; - - slnDir = slnSearchDirs.FirstOrDefault(d => Directory.EnumerateFiles(d, "*.sln").Any()); - if (slnDir == null) { - throw new Exception("Could not find '.sln'. Specify solution file with '--csq-sln=', provide " + - "Squirrel tools path with '--csq-path=' argument, or use embedded version with '--csq-embedded'."); - } + if (solutionDir == null) { + throw new Exception("Could not find '.sln'. Specify solution with '--csq-sln=', or specify version of squirrel to use with '--csq-version='."); } - slnDir = Path.GetFullPath(slnDir); - Write("solution dir " + slnDir, true); + Write("Solution dir found at: " + solutionDir, true); - const string packageName = "Clowd.Squirrel"; - var dependencies = GetPackageVersionsFromDir(slnDir, packageName).Distinct().ToArray(); + // TODO actually read the SLN file rather than just searching for all .csproj files + var dependencies = GetPackageVersionsFromDir(solutionDir, CLOWD_PACKAGE_NAME).Distinct().ToArray(); if (dependencies.Length == 0) { - throw new Exception("Clowd.Squirrel nuget package was not found in solution."); + throw new Exception("Clowd.Squirrel nuget package was not found installed in solution."); } if (dependencies.Length > 1) { - throw new Exception("Found multiple versions of Clowd.Squirrel installed in solution. " + - "Please consolidate the following to a single version: " + string.Join(", ", dependencies)); + throw new Exception($"Found multiple versions of Clowd.Squirrel installed in solution ({string.Join(", ", dependencies)}). " + + "Please consolidate the following to a single version, or specify the version to use with '--csq-version='"); } var targetVersion = dependencies.Single(); - var toolsDir = GetToolPathFromUserCache(targetVersion, packageName); - - var localpath = Path.Combine(slnDir, "packages", packageName + "." + targetVersion, "tools"); - if (Directory.Exists(localpath)) - toolsDir = localpath; - if (!Directory.Exists(toolsDir)) { - throw new Exception($"Unable to find Squirrel tools for '{targetVersion}'. " + - $"Please specify path to tools directory with '--csq-path=' argument, " + - $"or use embedded version with '--csq-embedded'."); - } - - return RunCsqFromPath(toolsDir, restArgs); + return runSquirrel(targetVersion); } - static string GetToolPathFromUserCache(string targetVersion, string packageName) + static void CheckForUpdates() { - var packages = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); - var toolRootPath = Path.Combine(packages, packageName.ToLower(), targetVersion, "tools"); - if (Directory.Exists(toolRootPath)) - return toolRootPath; + try { + var myVer = SquirrelNugetVersion; + var dl = new NugetDownloader(new Program()); + var package = dl.GetPackageMetadata("csq", (myVer.IsPrerelease || myVer.HasMetadata) ? "pre" : "latest"); + if (package.Identity.Version > myVer) + Write($"There is a new version of csq available ({package.Identity.Version})", false); + } catch { ; } + } - // resolve wildcards. we should probably rely on the dotnet tooling for this in the future - // so we can be more certain we are using precisely the same version as dotnet. - if (targetVersion.Contains("*")) { - Write($"Project version is '{targetVersion}'. Attempting to resolve wildcard...", false); - var packageDir = Path.Combine(packages, packageName.ToLower()); - var vdir = Directory.EnumerateDirectories(packageDir, targetVersion, SearchOption.TopDirectoryOnly) - .Select(d => new DirectoryInfo(d).Name) - .Select(NuGetVersion.Parse) - .Max(); + static string FindSolutionDirectory(string slnArgument) + { + if (!String.IsNullOrWhiteSpace(slnArgument)) { + if (File.Exists(slnArgument) && slnArgument.EndsWith(".sln", StringComparison.InvariantCultureIgnoreCase)) { + // we were given a sln file as argument + return Path.GetDirectoryName(Path.GetFullPath(slnArgument)); + } - if (vdir != null) { - return Path.Combine(packageDir, vdir.OriginalVersion, "tools"); + if (Directory.Exists(slnArgument) && Directory.EnumerateFiles(slnArgument, "*.sln").Any()) { + return Path.GetFullPath(slnArgument); } } - return null; + // try to find the solution directory from cwd + var cwd = Environment.CurrentDirectory; + var slnSearchDirs = new string[] { + cwd, + Path.Combine(cwd, ".."), + Path.Combine(cwd, "..", ".."), + }; + + return slnSearchDirs.FirstOrDefault(d => Directory.EnumerateFiles(d, "*.sln").Any()); } static int RunCsqFromPath(string toolRootPath, string[] args) { - var dllName = "csq.dll"; - var exeName = "Squirrel.exe"; - var toolDllPath = Path.Combine(toolRootPath, dllName); - var toolExePath = Path.Combine(toolRootPath, exeName); - - Process p; + // > v3.0.170 + if (File.Exists(Path.Combine(toolRootPath, "Squirrel.CommandLine.runtimeconfig.json"))) { + var cliPath = Path.Combine(toolRootPath, "Squirrel.CommandLine.dll"); + var dnargs = new[] { cliPath }.Concat(args).ToArray(); + Write("running dotnet " + String.Join(" ", dnargs), true); + return PlatformUtil.InvokeProcess("dotnet", dnargs, Environment.CurrentDirectory, CancellationToken.None).ExitCode; + } + // v3.0 - v3.0.170 + var toolDllPath = Path.Combine(toolRootPath, "csq.dll"); if (File.Exists(toolDllPath)) { var dnargs = new[] { toolDllPath, "--csq-embedded" }.Concat(args).ToArray(); Write("running dotnet " + String.Join(" ", dnargs), true); - p = Process.Start("dotnet", dnargs); - } else if (File.Exists(toolExePath)) { - if (!OperatingSystem.IsWindows()) + return PlatformUtil.InvokeProcess("dotnet", dnargs, Environment.CurrentDirectory, CancellationToken.None).ExitCode; + } + + // < v3.0 + var toolExePath = Path.Combine(toolRootPath, "Squirrel.exe"); + if (File.Exists(toolExePath)) { + if (!SquirrelRuntimeInfo.IsWindows) throw new NotSupportedException( $"Squirrel at '{toolRootPath}' does not support this operating system. Please update the package version to >= 3.0"); Write("running " + toolExePath + " " + String.Join(" ", args), true); - p = Process.Start(toolExePath, args); - } else { - throw new Exception("Unable to locate Squirrel at: " + toolRootPath); + return PlatformUtil.InvokeProcess(toolExePath, args, Environment.CurrentDirectory, CancellationToken.None).ExitCode; } - p.WaitForExit(); - return p.ExitCode; + throw new Exception("Unable to locate Squirrel at: " + toolRootPath); } static IEnumerable GetPackageVersionsFromDir(string rootDir, string packageName) @@ -171,7 +211,7 @@ static IEnumerable GetPackageVersionsFromDir(string rootDir, string pack if (ver.Value.Contains("*")) throw new Exception( - "Wildcard versions are not supported in packages.config. Remove wildcard or upgrade csproj format to use PackageReference."); + $"Wildcard versions are not supported in packages.config. Remove wildcard or upgrade csproj format to use PackageReference."); yield return ver.Value; } @@ -211,8 +251,70 @@ static IEnumerable EnumerateFilesUntilSpecificDepth(string rootPath, str static void Write(string message, bool isDebugMessage) { + // TODO use Squirrel logging proper... if (Verbose || !isDebugMessage) Console.WriteLine("csq: " + message); } + + #region NuGet.Common.ILogger + + public void LogDebug(string data) + { + Write(data, true); + } + + public void LogVerbose(string data) + { + Write(data, true); + } + + public void LogInformation(string data) + { + Write(data, true); + } + + public void LogMinimal(string data) + { + Write(data, false); + } + + public void LogWarning(string data) + { + Write(data, false); + } + + public void LogError(string data) + { + Write(data, false); + } + + public void LogInformationSummary(string data) + { + Write(data, true); + } + + public void Log(LogLevel level, string data) + { + Write(data, level <= LogLevel.Information); + } + + public Task LogAsync(LogLevel level, string data) + { + Write(data, level <= LogLevel.Information); + return Task.CompletedTask; + } + + public void Log(ILogMessage message) + { + Write(message.Message, message.Level <= LogLevel.Information); + } + + public Task LogAsync(ILogMessage message) + { + Write(message.Message, message.Level <= LogLevel.Information); + return Task.CompletedTask; + } + + #endregion } } \ No newline at end of file diff --git a/src/Squirrel.Tool/Squirrel.Tool.csproj b/src/Squirrel.Tool/Squirrel.Tool.csproj index e5f10750d..970924c09 100644 --- a/src/Squirrel.Tool/Squirrel.Tool.csproj +++ b/src/Squirrel.Tool/Squirrel.Tool.csproj @@ -20,11 +20,12 @@ - + + - + - + diff --git a/src/Squirrel/SquirrelRuntimeInfo.cs b/src/Squirrel/SquirrelRuntimeInfo.cs index c85662bc5..00acb3025 100644 --- a/src/Squirrel/SquirrelRuntimeInfo.cs +++ b/src/Squirrel/SquirrelRuntimeInfo.cs @@ -111,8 +111,7 @@ public static class SquirrelRuntimeInfo static SquirrelRuntimeInfo() { - var entryProcess = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; - EntryExePath = Path.GetFileNameWithoutExtension(entryProcess) == "dotnet" ? "csq" : entryProcess; + EntryExePath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; BaseDirectory = AppContext.BaseDirectory; // if Assembly.Location does not exist, we're almost certainly bundled into a dotnet SingleFile diff --git a/version.json b/version.json index 63cc4fb7f..8718ce6ac 100644 --- a/version.json +++ b/version.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", "version": "3.0", - "gitCommitIdShortFixedLength": 6, + "gitCommitIdShortFixedLength": 7, "publicReleaseRefSpec": [ "^refs/heads/master$" ]