Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
5 changes: 5 additions & 0 deletions docs/content/bootstrapper.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 6 additions & 2 deletions docs/content/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.

Expand Down
9 changes: 7 additions & 2 deletions docs/content/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
12 changes: 11 additions & 1 deletion src/Paket.Bootstrapper/BootstrapperHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@ namespace Paket.Bootstrapper
{
internal static class BootstrapperHelper
{
public static string HelpText = @"The paket.bootstrapper downloads the latest version of paket.
/// <summary>
/// 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.
/// </summary>
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|<version>]

Expand Down
47 changes: 46 additions & 1 deletion src/Paket.Bootstrapper/DownloadStrategies/DownloadStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,55 @@ public abstract class DownloadStrategy : IDownloadStrategy
public abstract string Name { get; }
public abstract bool CanDownloadHashFile { get; }

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

/// <summary>
/// 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.
/// </summary>
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/Paket.Bootstrapper/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
4 changes: 4 additions & 0 deletions src/Paket/Paket.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<IsPackable>true</IsPackable>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net461' ">
<!-- This build is the paket.exe the bootstrapper downloads; it is deprecated. -->
<DefineConstants>PAKET_LEGACY_EXE;$(DefineConstants)</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(PackAsTool)' == 'true' ">
<!-- .net tools support netcoreapp only -->
<TargetFrameworks>
Expand Down
16 changes: 16 additions & 0 deletions src/Paket/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ public void GetLatestVersion()
mockWebProxy.Verify();
}

[Test]
public void GetLatestVersion_CapsAVersionWeCanNoLongerDownload()
{
//arrange
mockWebProxy.Setup(x => x.DownloadString(GitHubDownloadStrategy.Constants.PaketReleasesLatestUrl)).Returns("<title>Release 12.0.0 · fsprojects/Paket</title>").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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>())).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()
{
Expand Down Expand Up @@ -238,13 +254,13 @@ public void NugetFolder_DownloadVersion_NoVersionSpecified_GetsLatestVersion()
CreateSystemUnderTestWithNugetFolder();
mockFileProxy.Setup(
x => x.EnumerateFiles(It.IsAny<string>(), "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<string>(s => s.StartsWith("anyNugetFolder") && s.EndsWith("paket.111.nupkg")), It.Is<string>(s => s.StartsWith("folder") && s.EndsWith("paket.latest.nupkg")), false));
mockFileProxy.Verify(x => x.CopyFile(It.Is<string>(s => s.StartsWith("anyNugetFolder") && s.EndsWith("paket.1.1.1.nupkg")), It.Is<string>(s => s.StartsWith("folder") && s.EndsWith("paket.latest.nupkg")), false));

mockFileProxy.Verify(x => x.ExtractToDirectory(It.Is<string>(s => s.StartsWith("folder") && s.EndsWith("paket.latest.nupkg")), It.IsAny<string>()));
mockFileProxy.Verify(x => x.CopyFile(It.Is<string>(s => s.StartsWith("folder") && s.EndsWith("paket.exe")), "paket", true));
Expand Down
Loading