diff --git a/src/dotnet/CommonLocalizableStrings.resx b/src/dotnet/CommonLocalizableStrings.resx
index 77847a864..236b2fd26 100644
--- a/src/dotnet/CommonLocalizableStrings.resx
+++ b/src/dotnet/CommonLocalizableStrings.resx
@@ -619,6 +619,9 @@ setx PATH "%PATH%;{0}"
Tool '{0}' (version '{1}') is already installed.
+
+ Failed to find staged tool package '{0}'.
+
Column maximum width must be greater than zero.
diff --git a/src/dotnet/ToolPackage/IToolPackage.cs b/src/dotnet/ToolPackage/IToolPackage.cs
index 3ee968cb9..c769b002d 100644
--- a/src/dotnet/ToolPackage/IToolPackage.cs
+++ b/src/dotnet/ToolPackage/IToolPackage.cs
@@ -4,14 +4,15 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.EnvironmentAbstractions;
+using NuGet.Versioning;
namespace Microsoft.DotNet.ToolPackage
{
internal interface IToolPackage
{
- string PackageId { get; }
+ PackageId Id { get; }
- string PackageVersion { get; }
+ NuGetVersion Version { get; }
DirectoryPath PackageDirectory { get; }
diff --git a/src/dotnet/ToolPackage/IToolPackageInstaller.cs b/src/dotnet/ToolPackage/IToolPackageInstaller.cs
index d8501d22d..c6ef964e1 100644
--- a/src/dotnet/ToolPackage/IToolPackageInstaller.cs
+++ b/src/dotnet/ToolPackage/IToolPackageInstaller.cs
@@ -4,14 +4,15 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.EnvironmentAbstractions;
+using NuGet.Versioning;
namespace Microsoft.DotNet.ToolPackage
{
internal interface IToolPackageInstaller
{
IToolPackage InstallPackage(
- string packageId,
- string packageVersion = null,
+ PackageId packageId,
+ VersionRange versionRange = null,
string targetFramework = null,
FilePath? nugetConfig = null,
string source = null,
diff --git a/src/dotnet/ToolPackage/IToolPackageStore.cs b/src/dotnet/ToolPackage/IToolPackageStore.cs
index 72105d24d..4e3e88c65 100644
--- a/src/dotnet/ToolPackage/IToolPackageStore.cs
+++ b/src/dotnet/ToolPackage/IToolPackageStore.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.EnvironmentAbstractions;
+using NuGet.Versioning;
namespace Microsoft.DotNet.ToolPackage
{
@@ -11,6 +12,18 @@ namespace Microsoft.DotNet.ToolPackage
{
DirectoryPath Root { get; }
- IEnumerable GetInstalledPackages(string packageId = null);
+ DirectoryPath GetRandomStagingDirectory();
+
+ NuGetVersion GetStagedPackageVersion(DirectoryPath stagingDirectory, PackageId packageId);
+
+ DirectoryPath GetRootPackageDirectory(PackageId packageId);
+
+ DirectoryPath GetPackageDirectory(PackageId packageId, NuGetVersion version);
+
+ IEnumerable EnumeratePackages();
+
+ IEnumerable EnumeratePackageVersions(PackageId packageId);
+
+ IToolPackage GetPackage(PackageId packageId, NuGetVersion version);
}
}
diff --git a/src/dotnet/ToolPackage/PackageId.cs b/src/dotnet/ToolPackage/PackageId.cs
new file mode 100644
index 000000000..07846bf7a
--- /dev/null
+++ b/src/dotnet/ToolPackage/PackageId.cs
@@ -0,0 +1,43 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using Microsoft.DotNet.InternalAbstractions;
+
+namespace Microsoft.DotNet.ToolPackage
+{
+ internal struct PackageId : IEquatable, IComparable
+ {
+ private string _id;
+
+ public PackageId(string id)
+ {
+ _id = id?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(id));
+ }
+
+ public bool Equals(PackageId other)
+ {
+ return ToString() == other.ToString();
+ }
+
+ public int CompareTo(PackageId other)
+ {
+ return string.Compare(ToString(), other.ToString(), StringComparison.Ordinal);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is PackageId id && Equals(id);
+ }
+
+ public override int GetHashCode()
+ {
+ return ToString().GetHashCode();
+ }
+
+ public override string ToString()
+ {
+ return _id ?? "";
+ }
+ }
+}
diff --git a/src/dotnet/ToolPackage/ToolPackageInstaller.cs b/src/dotnet/ToolPackage/ToolPackageInstaller.cs
index e30a4cc7e..cf655a301 100644
--- a/src/dotnet/ToolPackage/ToolPackageInstaller.cs
+++ b/src/dotnet/ToolPackage/ToolPackageInstaller.cs
@@ -7,13 +7,12 @@ using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Configurer;
using Microsoft.DotNet.Tools;
using Microsoft.Extensions.EnvironmentAbstractions;
+using NuGet.Versioning;
namespace Microsoft.DotNet.ToolPackage
{
internal class ToolPackageInstaller : IToolPackageInstaller
{
- public const string StagingDirectory = ".stage";
-
private readonly IToolPackageStore _store;
private readonly IProjectRestorer _projectRestorer;
private readonly FilePath? _tempProject;
@@ -32,33 +31,27 @@ namespace Microsoft.DotNet.ToolPackage
}
public IToolPackage InstallPackage(
- string packageId,
- string packageVersion = null,
+ PackageId packageId,
+ VersionRange versionRange = null,
string targetFramework = null,
FilePath? nugetConfig = null,
string source = null,
string verbosity = null)
{
- if (packageId == null)
- {
- throw new ArgumentNullException(nameof(packageId));
- }
-
- var packageRootDirectory = _store.Root.WithSubDirectories(packageId);
+ var packageRootDirectory = _store.GetRootPackageDirectory(packageId);
string rollbackDirectory = null;
return TransactionalAction.Run(
action: () => {
try
{
-
- var stageDirectory = _store.Root.WithSubDirectories(StagingDirectory, Path.GetRandomFileName());
+ var stageDirectory = _store.GetRandomStagingDirectory();
Directory.CreateDirectory(stageDirectory.Value);
rollbackDirectory = stageDirectory.Value;
var tempProject = CreateTempProject(
packageId: packageId,
- packageVersion: packageVersion,
+ versionRange: versionRange,
targetFramework: targetFramework ?? BundledTargetFramework.GetTargetFrameworkMoniker(),
restoreDirectory: stageDirectory);
@@ -76,29 +69,22 @@ namespace Microsoft.DotNet.ToolPackage
File.Delete(tempProject.Value);
}
- packageVersion = Path.GetFileName(
- Directory.EnumerateDirectories(
- stageDirectory.WithSubDirectories(packageId).Value).Single());
-
- var packageDirectory = packageRootDirectory.WithSubDirectories(packageVersion);
+ var version = _store.GetStagedPackageVersion(stageDirectory, packageId);
+ var packageDirectory = _store.GetPackageDirectory(packageId, version);
if (Directory.Exists(packageDirectory.Value))
{
throw new ToolPackageException(
string.Format(
CommonLocalizableStrings.ToolPackageConflictPackageId,
packageId,
- packageVersion));
+ version.ToNormalizedString()));
}
Directory.CreateDirectory(packageRootDirectory.Value);
Directory.Move(stageDirectory.Value, packageDirectory.Value);
rollbackDirectory = packageDirectory.Value;
- return new ToolPackageInstance(
- _store,
- packageId,
- packageVersion,
- packageDirectory);
+ return new ToolPackageInstance(_store, packageId, version, packageDirectory);
}
catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException)
{
@@ -126,8 +112,8 @@ namespace Microsoft.DotNet.ToolPackage
}
private FilePath CreateTempProject(
- string packageId,
- string packageVersion,
+ PackageId packageId,
+ VersionRange versionRange,
string targetFramework,
DirectoryPath restoreDirectory)
{
@@ -159,8 +145,9 @@ namespace Microsoft.DotNet.ToolPackage
new XElement("DisableImplicitNuGetFallbackFolder", "true")), // disable SDK side implicit NuGetFallbackFolder
new XElement("ItemGroup",
new XElement("PackageReference",
- new XAttribute("Include", packageId),
- new XAttribute("Version", packageVersion ?? "*") // nuget will restore * for latest
+ new XAttribute("Include", packageId.ToString()),
+ new XAttribute("Version",
+ versionRange?.ToString("S", new VersionRangeFormatter()) ?? "*") // nuget will restore latest stable for *
))
));
diff --git a/src/dotnet/ToolPackage/ToolPackageInstance.cs b/src/dotnet/ToolPackage/ToolPackageInstance.cs
index 5841c2ef6..74b092109 100644
--- a/src/dotnet/ToolPackage/ToolPackageInstance.cs
+++ b/src/dotnet/ToolPackage/ToolPackageInstance.cs
@@ -6,6 +6,7 @@ using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Tools;
using Microsoft.Extensions.EnvironmentAbstractions;
using NuGet.ProjectModel;
+using NuGet.Versioning;
namespace Microsoft.DotNet.ToolPackage
{
@@ -17,20 +18,21 @@ namespace Microsoft.DotNet.ToolPackage
public ToolPackageInstance(
IToolPackageStore store,
- string packageId,
- string packageVersion,
+ PackageId id,
+ NuGetVersion version,
DirectoryPath packageDirectory)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
- PackageId = packageId ?? throw new ArgumentNullException(nameof(packageId));
- PackageVersion = packageVersion ?? throw new ArgumentNullException(nameof(packageVersion));
- PackageDirectory = packageDirectory;
_commands = new Lazy>(GetCommands);
+
+ Id = id;
+ Version = version ?? throw new ArgumentNullException(nameof(version));
+ PackageDirectory = packageDirectory;
}
- public string PackageId { get; private set; }
+ public PackageId Id { get; private set; }
- public string PackageVersion { get; private set; }
+ public NuGetVersion Version { get; private set; }
public DirectoryPath PackageDirectory { get; private set; }
@@ -53,13 +55,9 @@ namespace Microsoft.DotNet.ToolPackage
{
if (Directory.Exists(PackageDirectory.Value))
{
- // Use the same staging directory for uninstall instead of temp
+ // Use the staging directory for uninstall
// This prevents cross-device moves when temp is mounted to a different device
- var tempPath = _store
- .Root
- .WithSubDirectories(ToolPackageInstaller.StagingDirectory)
- .WithFile(Path.GetRandomFileName())
- .Value;
+ var tempPath = _store.GetRandomStagingDirectory().Value;
Directory.Move(PackageDirectory.Value, tempPath);
tempPackageDirectory = tempPath;
}
@@ -75,7 +73,7 @@ namespace Microsoft.DotNet.ToolPackage
throw new ToolPackageException(
string.Format(
CommonLocalizableStrings.FailedToUninstallToolPackage,
- PackageId,
+ Id,
ex.Message),
ex);
}
@@ -116,7 +114,7 @@ namespace Microsoft.DotNet.ToolPackage
var toolConfigurationPath =
PackageDirectory
.WithSubDirectories(
- PackageId,
+ Id.ToString(),
library.Version.ToNormalizedString())
.WithFile(dotnetToolSettings.Path);
@@ -138,7 +136,7 @@ namespace Microsoft.DotNet.ToolPackage
"dotnet",
PackageDirectory
.WithSubDirectories(
- PackageId,
+ Id.ToString(),
library.Version.ToNormalizedString())
.WithFile(entryPointFromLockFile.Path)));
@@ -158,7 +156,8 @@ namespace Microsoft.DotNet.ToolPackage
{
return lockFile
?.Targets?.SingleOrDefault(t => t.RuntimeIdentifier != null)
- ?.Libraries?.SingleOrDefault(l => l.Name == PackageId);
+ ?.Libraries?.SingleOrDefault(l =>
+ string.Compare(l.Name, Id.ToString(), StringComparison.CurrentCultureIgnoreCase) == 0);
}
private static LockFileItem FindItemInTargetLibrary(LockFileTargetLibrary library, string targetRelativeFilePath)
diff --git a/src/dotnet/ToolPackage/ToolPackageStore.cs b/src/dotnet/ToolPackage/ToolPackageStore.cs
index f9575a9cc..f424b6062 100644
--- a/src/dotnet/ToolPackage/ToolPackageStore.cs
+++ b/src/dotnet/ToolPackage/ToolPackageStore.cs
@@ -2,12 +2,16 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Microsoft.DotNet.Tools;
using Microsoft.Extensions.EnvironmentAbstractions;
+using NuGet.Versioning;
namespace Microsoft.DotNet.ToolPackage
{
internal class ToolPackageStore : IToolPackageStore
{
+ public const string StagingDirectory = ".stage";
+
public ToolPackageStore(DirectoryPath root)
{
Root = root;
@@ -15,17 +19,45 @@ namespace Microsoft.DotNet.ToolPackage
public DirectoryPath Root { get; private set; }
- public IEnumerable GetInstalledPackages(string packageId = null)
+ public DirectoryPath GetRandomStagingDirectory()
{
- if (packageId != null)
- {
- return EnumerateVersions(packageId);
- }
-
- return EnumerateAllPackages().SelectMany(p => p);
+ return Root.WithSubDirectories(StagingDirectory, Path.GetRandomFileName());
}
- private IEnumerable> EnumerateAllPackages()
+ public NuGetVersion GetStagedPackageVersion(DirectoryPath stagingDirectory, PackageId packageId)
+ {
+ if (NuGetVersion.TryParse(
+ Path.GetFileName(
+ Directory.EnumerateDirectories(
+ stagingDirectory.WithSubDirectories(packageId.ToString()).Value).FirstOrDefault()),
+ out var version))
+ {
+ return version;
+ }
+
+ throw new ToolPackageException(
+ string.Format(
+ CommonLocalizableStrings.FailedToFindStagedToolPackage,
+ packageId));
+ }
+
+ public DirectoryPath GetRootPackageDirectory(PackageId packageId)
+ {
+ return Root.WithSubDirectories(packageId.ToString());
+ }
+
+ public DirectoryPath GetPackageDirectory(PackageId packageId, NuGetVersion version)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException(nameof(version));
+ }
+
+ return GetRootPackageDirectory(packageId)
+ .WithSubDirectories(version.ToNormalizedString().ToLowerInvariant());
+ }
+
+ public IEnumerable EnumeratePackages()
{
if (!Directory.Exists(Root.Value))
{
@@ -34,19 +66,25 @@ namespace Microsoft.DotNet.ToolPackage
foreach (var subdirectory in Directory.EnumerateDirectories(Root.Value))
{
- var packageId = Path.GetFileName(subdirectory);
- if (packageId == ToolPackageInstaller.StagingDirectory)
+ var name = Path.GetFileName(subdirectory);
+ var packageId = new PackageId(name);
+
+ // Ignore the staging directory and any directory that isn't the same as the package id
+ if (name == StagingDirectory || name != packageId.ToString())
{
continue;
}
- yield return EnumerateVersions(packageId);
+ foreach (var package in EnumeratePackageVersions(packageId))
+ {
+ yield return package;
+ }
}
}
- private IEnumerable EnumerateVersions(string packageId)
+ public IEnumerable EnumeratePackageVersions(PackageId packageId)
{
- var packageRootDirectory = Root.WithSubDirectories(packageId);
+ var packageRootDirectory = Root.WithSubDirectories(packageId.ToString());
if (!Directory.Exists(packageRootDirectory.Value))
{
yield break;
@@ -54,13 +92,27 @@ namespace Microsoft.DotNet.ToolPackage
foreach (var subdirectory in Directory.EnumerateDirectories(packageRootDirectory.Value))
{
- var version = Path.GetFileName(subdirectory);
yield return new ToolPackageInstance(
this,
packageId,
- version,
- packageRootDirectory.WithSubDirectories(version));
+ NuGetVersion.Parse(Path.GetFileName(subdirectory)),
+ new DirectoryPath(subdirectory));
}
}
+
+ public IToolPackage GetPackage(PackageId packageId, NuGetVersion version)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException(nameof(version));
+ }
+
+ var directory = GetPackageDirectory(packageId, version);
+ if (!Directory.Exists(directory.Value))
+ {
+ return null;
+ }
+ return new ToolPackageInstance(this, packageId, version, directory);
+ }
}
}
diff --git a/src/dotnet/commands/dotnet-install/dotnet-install-tool/InstallToolCommand.cs b/src/dotnet/commands/dotnet-install/dotnet-install-tool/InstallToolCommand.cs
index 881f81491..8459c2b55 100644
--- a/src/dotnet/commands/dotnet-install/dotnet-install-tool/InstallToolCommand.cs
+++ b/src/dotnet/commands/dotnet-install/dotnet-install-tool/InstallToolCommand.cs
@@ -13,6 +13,7 @@ using Microsoft.DotNet.Configurer;
using Microsoft.DotNet.ShellShim;
using Microsoft.DotNet.ToolPackage;
using Microsoft.Extensions.EnvironmentAbstractions;
+using NuGet.Versioning;
namespace Microsoft.DotNet.Tools.Install.Tool
{
@@ -25,7 +26,7 @@ namespace Microsoft.DotNet.Tools.Install.Tool
private readonly IReporter _reporter;
private readonly IReporter _errorReporter;
- private readonly string _packageId;
+ private readonly PackageId _packageId;
private readonly string _packageVersion;
private readonly string _configFilePath;
private readonly string _framework;
@@ -48,7 +49,7 @@ namespace Microsoft.DotNet.Tools.Install.Tool
throw new ArgumentNullException(nameof(appliedCommand));
}
- _packageId = appliedCommand.Arguments.Single();
+ _packageId = new PackageId(appliedCommand.Arguments.Single());
_packageVersion = appliedCommand.ValueOrDefault("version");
_configFilePath = appliedCommand.ValueOrDefault("configfile");
_framework = appliedCommand.ValueOrDefault("framework");
@@ -91,8 +92,16 @@ namespace Microsoft.DotNet.Tools.Install.Tool
Path.GetFullPath(_configFilePath)));
}
- // Prevent installation if any version of the package is installed
- if (_toolPackageStore.GetInstalledPackages(_packageId).FirstOrDefault() != null)
+ VersionRange versionRange = null;
+ if (!string.IsNullOrEmpty(_packageVersion) && !VersionRange.TryParse(_packageVersion, out versionRange))
+ {
+ throw new GracefulException(
+ string.Format(
+ LocalizableStrings.InvalidNuGetVersionRange,
+ _packageVersion));
+ }
+
+ if (_toolPackageStore.EnumeratePackageVersions(_packageId).FirstOrDefault() != null)
{
_errorReporter.WriteLine(string.Format(LocalizableStrings.ToolAlreadyInstalled, _packageId).Red());
return 1;
@@ -113,7 +122,7 @@ namespace Microsoft.DotNet.Tools.Install.Tool
{
package = _toolPackageInstaller.InstallPackage(
packageId: _packageId,
- packageVersion: _packageVersion,
+ versionRange: versionRange,
targetFramework: _framework,
nugetConfig: configFile,
source: _source,
@@ -133,8 +142,8 @@ namespace Microsoft.DotNet.Tools.Install.Tool
string.Format(
LocalizableStrings.InstallationSucceeded,
string.Join(", ", package.Commands.Select(c => c.Name)),
- package.PackageId,
- package.PackageVersion).Green());
+ package.Id,
+ package.Version.ToNormalizedString()).Green());
return 0;
}
catch (ToolPackageException ex)
diff --git a/src/dotnet/commands/dotnet-install/dotnet-install-tool/LocalizableStrings.resx b/src/dotnet/commands/dotnet-install/dotnet-install-tool/LocalizableStrings.resx
index 8d366537c..2b463ae96 100644
--- a/src/dotnet/commands/dotnet-install/dotnet-install-tool/LocalizableStrings.resx
+++ b/src/dotnet/commands/dotnet-install/dotnet-install-tool/LocalizableStrings.resx
@@ -178,4 +178,7 @@ Tool '{1}' (version '{2}') was successfully installed.
Failed to create shell shim for tool '{0}': {1}
+
+ Specified version '{0}' is not a valid NuGet version range.
+
\ No newline at end of file
diff --git a/src/dotnet/commands/dotnet-install/dotnet-install-tool/xlf/LocalizableStrings.cs.xlf b/src/dotnet/commands/dotnet-install/dotnet-install-tool/xlf/LocalizableStrings.cs.xlf
index eb8dae6d6..894f345b3 100644
--- a/src/dotnet/commands/dotnet-install/dotnet-install-tool/xlf/LocalizableStrings.cs.xlf
+++ b/src/dotnet/commands/dotnet-install/dotnet-install-tool/xlf/LocalizableStrings.cs.xlf
@@ -104,6 +104,11 @@ Instalace byla úspěšná. Pokud nejsou další pokyny, můžete přímo do já
Balíček nástroje nebylo možné obnovit.
+
+ Specified version '{0}' is not a valid NuGet version range.
+ Specified version '{0}' is not a valid NuGet version range.
+
+