diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2d687e158e..26dcbf81d2 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,8 @@ #### Unreleased +* DEPRECATION: `paket.exe` and `paket.bootstrapper.exe` are deprecated and will no longer be published from Paket 12.0 onwards. The `paket` .NET tool becomes the only supported way to run Paket: `dotnet tool install paket`. Both binaries now print a notice on every run unless `-s` is passed. +* The bootstrapper shipped with this release never resolves a version past 11.x, so it keeps working once 12.0 ships without a `paket.exe` asset instead of failing the build. An explicitly pinned version is unaffected. + #### 11.0.0-alpha003 - 2026-09-27 * Attempt to republish now package permissions are in place diff --git a/docs/content/bootstrapper.md b/docs/content/bootstrapper.md index 5443baaa47..ede70791d8 100644 --- a/docs/content/bootstrapper.md +++ b/docs/content/bootstrapper.md @@ -1,5 +1,10 @@ # The Paket bootstrapper (paket.bootstrapper.exe) +> **Deprecated.** `paket.bootstrapper.exe` and `paket.exe` will no longer be published from Paket +> 12.0 onwards, and the bootstrapper shipped with Paket 11 will never resolve past the 11.x +> releases. Install Paket as a [.NET tool](installation.html#Installation-on-NET-Core) instead: +> `dotnet tool install paket`. + The bootstrapper downloads the latest stable `paket.exe`. By default, the bootstrapper caches downloaded versions of `paket.exe` for the current user across all projects. If the requested version is not present in the cache, it is diff --git a/docs/content/get-started.md b/docs/content/get-started.md index baae9785ad..8c1049d6ea 100644 --- a/docs/content/get-started.md +++ b/docs/content/get-started.md @@ -3,7 +3,7 @@ This guide shows how to get started with Paket in various ways, depending on your scenario: * [Get started with .NET Core (preferred)](#net-core-preferred) -* [Get started with the paket bootstrapper (legacy)](#install-the-paket-bootstrapper-legacy) +* [Get started with the paket bootstrapper (deprecated)](#install-the-paket-bootstrapper-deprecated) * [Convert from legacy NuGet](#convert-from-nuget) ## .NET Core (preferred) @@ -49,7 +49,11 @@ paket-files/ Next, [learn how to use Paket](learn-how-to-use-paket.html) -## Install the Paket bootstrapper (legacy) +## Install the Paket bootstrapper (deprecated) + +> **Deprecated.** `paket.bootstrapper.exe` and `paket.exe` will no longer be published from Paket +> 12.0 onwards, and the bootstrapper shipped with Paket 11 will never resolve past the 11.x +> releases. Prefer the .NET tool above; `dotnet tool install paket` is the supported route. If you're not using .NET Core, or you're stuck on .NET Core 2.2 or lower, you can use the paket bootstrapper. diff --git a/docs/content/installation.md b/docs/content/installation.md index 3e394dfb80..eaf24c4841 100644 --- a/docs/content/installation.md +++ b/docs/content/installation.md @@ -40,7 +40,11 @@ dotnet paket --help ## Installation per repository -The most common use of Paket is as a command line tool inside your project +> **Deprecated.** This route relies on `paket.bootstrapper.exe` and `paket.exe`, neither of which +> will be published from Paket 12.0 onwards. Use the [local tool](#Local-tool) instead, which gives +> you the same per-repository pinning. + +Paket used to be most commonly used as a command line tool inside your project repository. 1. Create a `.paket` directory in the root of your solution. @@ -112,7 +116,8 @@ utility. ### Installation on Windows -Please [install per repository](installation.html#Installation-per-repository). +Please install the [.NET tool](#Installation-on-NET-Core). The +[per-repository](#Installation-per-repository) route still works, but it is deprecated. ### Post installation diff --git a/src/Paket.Bootstrapper/BootstrapperHelper.cs b/src/Paket.Bootstrapper/BootstrapperHelper.cs index bbf1e9d69a..230e768826 100644 --- a/src/Paket.Bootstrapper/BootstrapperHelper.cs +++ b/src/Paket.Bootstrapper/BootstrapperHelper.cs @@ -10,7 +10,17 @@ namespace Paket.Bootstrapper { internal static class BootstrapperHelper { - public static string HelpText = @"The paket.bootstrapper downloads the latest version of paket. + /// + /// Printed on every run that isn't silent. Paket 12.0 and later ship the .NET tool + /// only, and their releases carry no paket.exe asset for the bootstrapper to download. + /// + public const string DeprecationNotice = + "paket.bootstrapper.exe and paket.exe are deprecated and will no longer be published from Paket 12.0 onwards. " + + "The .NET tool is now the only supported way to run Paket: dotnet tool install paket"; + + public static string HelpText = DeprecationNotice + @" + +The paket.bootstrapper downloads the latest version of paket. Usage for paket bootstrapper: paket.bootstrapper [OPTIONS] [prerelease|] diff --git a/src/Paket.Bootstrapper/DownloadStrategies/DownloadStrategy.cs b/src/Paket.Bootstrapper/DownloadStrategies/DownloadStrategy.cs index d4d098b7f5..24d75d32c3 100644 --- a/src/Paket.Bootstrapper/DownloadStrategies/DownloadStrategy.cs +++ b/src/Paket.Bootstrapper/DownloadStrategies/DownloadStrategy.cs @@ -8,10 +8,55 @@ public abstract class DownloadStrategy : IDownloadStrategy public abstract string Name { get; } public abstract bool CanDownloadHashFile { get; } + /// + /// Paket 12.0 and later are published as a .NET tool only: their releases carry no + /// paket.exe asset and their NuGet package no longer holds tools/paket.exe. Resolving one + /// would end in a 404, then a FileNotFoundException on the NuGet fallback, and an exit + /// code of 1 wherever paket.exe isn't already on disk. Stay on the last release we can + /// actually download instead. + /// + internal const int LastSupportedMajorVersion = 11; + + internal const string LastSupportedVersion = "11.0.0"; + public IDownloadStrategy FallbackStrategy { get; set; } public string GetLatestVersion(bool ignorePrerelease) { - return Wrap(() => GetLatestVersionCore(ignorePrerelease), "GetLatestVersion"); + var version = Wrap(() => GetLatestVersionCore(ignorePrerelease), "GetLatestVersion"); + return CapToLastSupportedVersion(version); + } + + /// + /// Applied here rather than in the individual strategies because this method is the single + /// non-virtual entry point every strategy goes through, and the decorating strategies call + /// it on the strategy they wrap. That also covers the version sources that never touch the + /// network: the on-disk cache, the --max-file-age fast path and a local NuGet folder. + /// + internal static string CapToLastSupportedVersion(string version) + { + if (String.IsNullOrWhiteSpace(version)) + return version; + + SemVer parsed; + try + { + parsed = SemVer.Create(version); + } + catch (Exception) + { + // Never let an unparseable version break the bootstrapper; let the caller deal + // with it as it did before. + return version; + } + + if (parsed.Major <= LastSupportedMajorVersion) + return version; + + ConsoleImpl.WriteWarning( + "Paket {0} is available, but it is published as a .NET tool only and cannot be downloaded by the bootstrapper. Staying on {1}. To move on, run: dotnet tool install paket", + version, LastSupportedVersion); + + return LastSupportedVersion; } public void DownloadVersion(string latestVersion, string target, PaketHashFile hashfile) diff --git a/src/Paket.Bootstrapper/DownloadStrategies/NugetDownloadStrategy.cs b/src/Paket.Bootstrapper/DownloadStrategies/NugetDownloadStrategy.cs index 3f81702915..c0e9f8a374 100644 --- a/src/Paket.Bootstrapper/DownloadStrategies/NugetDownloadStrategy.cs +++ b/src/Paket.Bootstrapper/DownloadStrategies/NugetDownloadStrategy.cs @@ -102,6 +102,10 @@ protected override string GetLatestVersionCore(bool ignorePrerelease) var latestVersion = allVersions. Select(SemVer.Create). Where(x => !ignorePrerelease || (x.PreRelease == null)). + // This is the only strategy that sees every published version, so it can pick + // the best one we still support instead of falling back to the constant the + // base class clamps to. + Where(x => x.Major <= LastSupportedMajorVersion). OrderBy(x => x). LastOrDefault(x => !String.IsNullOrWhiteSpace(x.Original)); return latestVersion != null ? latestVersion.Original : String.Empty; diff --git a/src/Paket.Bootstrapper/Program.cs b/src/Paket.Bootstrapper/Program.cs index 97306193bf..852d84772e 100644 --- a/src/Paket.Bootstrapper/Program.cs +++ b/src/Paket.Bootstrapper/Program.cs @@ -60,6 +60,12 @@ static void Main(string[] args) } ConsoleImpl.Verbosity = options.Verbosity; + + // WriteWarning, not WriteAlways: it goes to stdout, and under '-s' (which transparent + // magic mode adds on its own) that stream is parsed by Paket.Restore.targets + // ('show-conditions -s'), where any extra line breaks the build. + ConsoleImpl.WriteWarning(BootstrapperHelper.DeprecationNotice); + if (options.UnprocessedCommandArgs.Any()) ConsoleImpl.WriteWarning("Ignoring the following unknown argument(s): {0}", String.Join(", ", options.UnprocessedCommandArgs)); diff --git a/src/Paket/Paket.fsproj b/src/Paket/Paket.fsproj index efc7ce8ec7..22a095e494 100644 --- a/src/Paket/Paket.fsproj +++ b/src/Paket/Paket.fsproj @@ -8,6 +8,10 @@ true true + + + PAKET_LEGACY_EXE;$(DefineConstants) + diff --git a/src/Paket/Program.fs b/src/Paket/Program.fs index ffbba31f47..dada8e42b4 100644 --- a/src/Paket/Program.fs +++ b/src/Paket/Program.fs @@ -934,6 +934,22 @@ let main() = Environment.SetEnvironmentVariable ("PAKET_DISABLE_RUNTIME_RESOLUTION", "true") use consoleTrace = Logging.event.Publish |> Observable.subscribe Logging.traceToConsole +#if PAKET_LEGACY_EXE + // Only the .NET Framework build carries this: it is the paket.exe the bootstrapper downloads, + // and the one committed into .paket for magic mode. Users of the .NET tool are already on the + // supported path and must not see it. + // It has to sit here: earlier and the trace event has no subscriber yet, later and the restore + // and install fast routes below would skip it. + // Skipped under -s/--silent: Paket.Restore.targets runs `show-conditions -s` and parses every + // stdout line as an MSBuild condition, so the notice must not be written there. The arguments + // are scanned by hand because Argu only parses them further down. + let isSilent = + Environment.GetCommandLineArgs() + |> Array.exists (fun a -> a = "-s" || a = "--silent") + if not isSilent then + traceWarnfn "paket.exe is deprecated and will no longer be published from Paket 12.0 onwards. The .NET tool is now the only supported way to run Paket: dotnet tool install paket" +#endif + try let args = Environment.GetCommandLineArgs() match args with diff --git a/tests/Paket.Bootstrapper.Tests/DownloadStrategies/DownloadStrategyVersionCapTests.cs b/tests/Paket.Bootstrapper.Tests/DownloadStrategies/DownloadStrategyVersionCapTests.cs new file mode 100644 index 0000000000..e514ccd35a --- /dev/null +++ b/tests/Paket.Bootstrapper.Tests/DownloadStrategies/DownloadStrategyVersionCapTests.cs @@ -0,0 +1,124 @@ +using NUnit.Framework; +using Paket.Bootstrapper.DownloadStrategies; + +namespace Paket.Bootstrapper.Tests.DownloadStrategies +{ + [TestFixture] + public class DownloadStrategyVersionCapTests + { + private FakeStrategy sut; + + [SetUp] + public void Setup() + { + sut = new FakeStrategy(); + } + + [Test] + public void GetLatestVersion_CapsAVersionWeCanNoLongerDownload() + { + //arrange + sut.LatestVersion = "12.0.0"; + + //act + var result = sut.GetLatestVersion(true); + + //assert + Assert.That(result, Is.EqualTo(DownloadStrategy.LastSupportedVersion)); + } + + [Test] + public void GetLatestVersion_CapsAPrereleaseOfAnUnsupportedMajor() + { + //arrange + sut.LatestVersion = "12.0.0-alpha001"; + + //act + var result = sut.GetLatestVersion(false); + + //assert + Assert.That(result, Is.EqualTo(DownloadStrategy.LastSupportedVersion)); + } + + [Test] + public void GetLatestVersion_LeavesTheLastSupportedMajorAlone() + { + //arrange + sut.LatestVersion = "11.2.3"; + + //act + var result = sut.GetLatestVersion(true); + + //assert + Assert.That(result, Is.EqualTo("11.2.3")); + } + + [Test] + public void GetLatestVersion_LeavesAnOlderVersionAlone() + { + //arrange + sut.LatestVersion = "10.3.1"; + + //act + var result = sut.GetLatestVersion(true); + + //assert + Assert.That(result, Is.EqualTo("10.3.1")); + } + + [Test] + public void GetLatestVersion_PassesAnEmptyVersionThrough() + { + //arrange + // An empty string is what the strategies return when they find nothing at all, and + // SemVer.Create would throw on it. + sut.LatestVersion = ""; + + //act + var result = sut.GetLatestVersion(true); + + //assert + Assert.That(result, Is.EqualTo("")); + } + + [Test] + public void GetLatestVersion_PassesAnUnparseableVersionThrough() + { + //arrange + sut.LatestVersion = "not a version"; + + //act + var result = sut.GetLatestVersion(true); + + //assert + Assert.That(result, Is.EqualTo("not a version")); + } + + private class FakeStrategy : DownloadStrategy + { + public string LatestVersion { get; set; } + + public override string Name { get { return "Fake"; } } + + public override bool CanDownloadHashFile { get { return false; } } + + protected override string GetLatestVersionCore(bool ignorePrerelease) + { + return LatestVersion; + } + + protected override void DownloadVersionCore(string latestVersion, string target, PaketHashFile hashfile) + { + } + + protected override void SelfUpdateCore(string latestVersion) + { + } + + protected override PaketHashFile DownloadHashFileCore(string latestVersion) + { + return null; + } + } + } +} diff --git a/tests/Paket.Bootstrapper.Tests/DownloadStrategies/GitHubDownloadStrategyTest.cs b/tests/Paket.Bootstrapper.Tests/DownloadStrategies/GitHubDownloadStrategyTest.cs index 76d73d66d5..00565ed627 100644 --- a/tests/Paket.Bootstrapper.Tests/DownloadStrategies/GitHubDownloadStrategyTest.cs +++ b/tests/Paket.Bootstrapper.Tests/DownloadStrategies/GitHubDownloadStrategyTest.cs @@ -39,6 +39,22 @@ public void GetLatestVersion() mockWebProxy.Verify(); } + [Test] + public void GetLatestVersion_CapsAVersionWeCanNoLongerDownload() + { + //arrange + mockWebProxy.Setup(x => x.DownloadString(GitHubDownloadStrategy.Constants.PaketReleasesLatestUrl)).Returns("Release 12.0.0 ยท fsprojects/Paket").Verifiable(); + + //act + var result = sut.GetLatestVersion(true); + + //assert + // The releases page only ever yields the single newest version, so there is no older + // one to fall back to here; the base class clamps to the constant instead. + Assert.That(result, Is.EqualTo(DownloadStrategy.LastSupportedVersion)); + mockWebProxy.Verify(); + } + [Test] public void GetLatestVersion_Prerelease() { diff --git a/tests/Paket.Bootstrapper.Tests/DownloadStrategies/NugetDownloadStrategyTests.cs b/tests/Paket.Bootstrapper.Tests/DownloadStrategies/NugetDownloadStrategyTests.cs index b84c3d37bf..7611fd2e47 100644 --- a/tests/Paket.Bootstrapper.Tests/DownloadStrategies/NugetDownloadStrategyTests.cs +++ b/tests/Paket.Bootstrapper.Tests/DownloadStrategies/NugetDownloadStrategyTests.cs @@ -47,6 +47,22 @@ public void DefaultApi_GetLatestVersion_NoPrerelease() Assert.That(version, Is.EqualTo("2.57.1")); } + [Test] + public void DefaultApi_GetLatestVersion_SkipsVersionsPastTheLastSupportedMajor() + { + //arrange + CreateSystemUnderTestWithDefaultApi(); + mockWebRequestProxy.Setup(x => x.DownloadString(It.IsAny())).Returns("[\"12.0.0\",\"11.1.0\",\"10.3.1\"]"); + + //act + var version = sut.GetLatestVersion(true); + + //assert + // This strategy sees the whole list, so it picks the best supported version rather + // than the constant the base class clamps to. + Assert.That(version, Is.EqualTo("11.1.0")); + } + [Test] public void DefaultApi_GetLatestVersion_WithPrerelease_ChoosePrelease() { @@ -238,13 +254,13 @@ public void NugetFolder_DownloadVersion_NoVersionSpecified_GetsLatestVersion() CreateSystemUnderTestWithNugetFolder(); mockFileProxy.Setup( x => x.EnumerateFiles(It.IsAny(), "paket.*.nupkg", SearchOption.TopDirectoryOnly)) - .Returns(new[] { "paket.111.nupkg" }); + .Returns(new[] { "paket.1.1.1.nupkg" }); //act sut.DownloadVersion(null, "paket", null); //assert - mockFileProxy.Verify(x => x.CopyFile(It.Is(s => s.StartsWith("anyNugetFolder") && s.EndsWith("paket.111.nupkg")), It.Is(s => s.StartsWith("folder") && s.EndsWith("paket.latest.nupkg")), false)); + mockFileProxy.Verify(x => x.CopyFile(It.Is(s => s.StartsWith("anyNugetFolder") && s.EndsWith("paket.1.1.1.nupkg")), It.Is(s => s.StartsWith("folder") && s.EndsWith("paket.latest.nupkg")), false)); mockFileProxy.Verify(x => x.ExtractToDirectory(It.Is(s => s.StartsWith("folder") && s.EndsWith("paket.latest.nupkg")), It.IsAny())); mockFileProxy.Verify(x => x.CopyFile(It.Is(s => s.StartsWith("folder") && s.EndsWith("paket.exe")), "paket", true));