Skip to content
Merged
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
10 changes: 9 additions & 1 deletion src/dnvm/CommandLineArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,17 @@ public sealed partial record PruneArgs : DnvmSubCommand
}

[Command("restore", Summary = "Restore the SDK listed in the global.json file.",
Description = "Downloads the SDK in the global.json in or above the current directory to the .dotnet folder in the same directory.")]
Description = "Downloads the SDK in the global.json in or above the current directory.")]
public sealed partial record RestoreArgs : DnvmSubCommand
{
[CommandOption("-l|--local", Description = "Install the sdk into the .dotnet folder in the same directory as global.json.")]
public bool? Local { get; init; } = null;

[CommandOption("-f|--force", Description = "Force install the SDK, even if already installed.")]
public bool? Force { get; init; } = null;

[CommandOption("-v|--verbose", Description = "Print extra debugging info to the console.")]
public bool? Verbose { get; init; } = null;
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/dnvm/InstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public static async Task<Result> Run(DnvmEnv env, Logger logger, Options options
var sdkVersion = options.SdkVersion;
var channel = new Channel.VersionedMajorMinor(sdkVersion.Major, sdkVersion.Minor);

if (!options.Force && manifest.InstalledSdks.Any(s => s.SdkVersion == sdkVersion && s.SdkDirName == sdkDir))
if (!options.Force && ManifestUtils.IsSdkInstalled(manifest, sdkVersion, sdkDir))
{
logger.Log($"Version {sdkVersion} is already installed in directory '{sdkDir.Name}'." +
" Skipping installation. To install anyway, pass --force.");
Expand Down Expand Up @@ -241,7 +241,7 @@ internal static async Task<Result<Manifest, InstallError>> InstallSdk(
var result = JsonSerializer.Serialize(manifest);
logger.Info("Existing manifest: " + result);

if (!manifest.InstalledSdks.Any(s => s.SdkVersion == sdkVersion && s.SdkDirName == sdkDir))
if (!ManifestUtils.IsSdkInstalled(manifest, sdkVersion, sdkDir))
{
manifest = manifest with
{
Expand Down
5 changes: 5 additions & 0 deletions src/dnvm/ManifestUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ public static Manifest AddSdk(this Manifest manifest,
};
}

public static bool IsSdkInstalled(Manifest manifest, SemVersion version, SdkDirName dirName)
{
return manifest.InstalledSdks.Any(s => s.SdkVersion == version && s.SdkDirName == dirName);
}

/// <summary>
/// Either reads a manifest in the current format, or reads a
/// manifest in the old format and converts it to the new format.
Expand Down
2 changes: 1 addition & 1 deletion src/dnvm/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal static async Task<int> Dnvm(DnvmEnv env, Logger logger, DnvmArgs args)
DnvmSubCommand.UntrackArgs a => await UntrackCommand.Run(env, logger, a.Channel),
DnvmSubCommand.UninstallArgs a => await UninstallCommand.Run(env, logger, a.SdkVersion, a.SdkDir),
DnvmSubCommand.PruneArgs a => await PruneCommand.Run(env, logger, a),
DnvmSubCommand.RestoreArgs => await RestoreCommand.Run(env, logger) switch {
DnvmSubCommand.RestoreArgs a => await RestoreCommand.Run(env, logger, a) switch {
Result<SemVersion, RestoreCommand.Error>.Ok => 0,
Result<SemVersion, RestoreCommand.Error>.Err x => (int)x.Value,
},
Expand Down
138 changes: 96 additions & 42 deletions src/dnvm/RestoreCommand.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Semver;
using Serde;
using Serde.Json;
using Spectre.Console;
using StaticCs;
using Zio;
using static Dnvm.InstallCommand;
using RollForwardOptions = Dnvm.GlobalJsonSubset.SdkSubset.RollForwardOptions;

namespace Dnvm;
Expand Down Expand Up @@ -126,9 +124,30 @@ public enum Error
NoVersion = 4,
CouldntFetchReleaseIndex = 5,
CantFindRequestedVersion = 6,
ManifestFileCorrupted = 7,
ManifestIOError = 8,
}

public static async Task<Result<SemVersion, Error>> Run(DnvmEnv env, Logger logger)
public sealed record Options
{
public bool Local { get; init; }

public bool Force { get; init; }

public bool Verbose { get; init; }
}

public static Task<Result<SemVersion, Error>> Run(DnvmEnv env, Logger logger, DnvmSubCommand.RestoreArgs args)
{
return Run(env, logger, new Options
{
Local = args.Local ?? false,
Force = args.Force ?? false,
Verbose = args.Verbose ?? false,
});
}

public static async Task<Result<SemVersion, Error>> Run(DnvmEnv env, Logger logger, Options options)
{
UPath? globalJsonPathOpt = null;
UPath cwd = env.Cwd;
Expand Down Expand Up @@ -196,18 +215,18 @@ public static async Task<Result<SemVersion, Error>> Run(DnvmEnv env, Logger logg

var installDir = globalJsonPath.GetDirectory() / ".dotnet";

var sdks = await GetSortedSdks(env.HttpClient, versionIndex, allowPrerelease, sdk.Version.ToMajorMinor());
var releases = await GetSortedReleases(env.HttpClient, versionIndex, allowPrerelease, sdk.Version.ToMajorMinor());

ChannelReleaseIndex.Component? Search(Comparison<SemVersion> comparer, bool preferExact)
ChannelReleaseIndex.Release? Search(Comparison<SemVersion> comparer, bool preferExact)
{
if (preferExact && BinarySearchLatest(sdks, version, SemVersion.CompareSortOrder) is { } exactMatch)
if (preferExact && BinarySearchLatest(releases, version, SemVersion.CompareSortOrder) is { } exactMatch)
{
return exactMatch;
}
return BinarySearchLatest(sdks, version, comparer);
return BinarySearchLatest(releases, version, comparer);
}

ChannelReleaseIndex.Component? component = rollForward switch
ChannelReleaseIndex.Release? release = rollForward switch
{
RollForwardOptions.Patch => Search(LatestPatchComparison, preferExact: true),
RollForwardOptions.Feature => Search(LatestFeatureComparison, preferExact: true),
Expand All @@ -220,30 +239,65 @@ public static async Task<Result<SemVersion, Error>> Run(DnvmEnv env, Logger logg
RollForwardOptions.Disable => Search(SemVersion.CompareSortOrder, preferExact: false),
};

if (component is null)
if (release is null)
{
logger.Error("No SDK found that matches the requested version.");
return Error.CantFindRequestedVersion;
}

var downloadUrl = component.Files.Single(f => f.Rid == Utilities.CurrentRID.ToString() && f.Url.EndsWith(Utilities.ZipSuffix)).Url;

var error = await InstallCommand.InstallSdkToDir(
curMuxerVersion: null, // we shouldn't have a muxer yet, and we can overwrite it if we do
component.Version,
env.HttpClient,
downloadUrl,
env.CwdFs,
installDir,
env.TempFs,
logger
);
if (error is not null)
logger.Log($"Found version {sdk.Version} in global.json. Selected version {release.Sdk.Version} based on roll forward rules.");
var downloadUrl = release.Sdk.Files.Single(f => f.Rid == Utilities.CurrentRID.ToString() && f.Url.EndsWith(Utilities.ZipSuffix)).Url;

if (options.Local)
{
return Error.IoError;
var error = await InstallCommand.InstallSdkToDir(
curMuxerVersion: null, // we shouldn't have a muxer yet, and we can overwrite it if we do
release.Sdk.Version,
env.HttpClient,
downloadUrl,
env.CwdFs,
installDir,
env.TempFs,
logger
);
if (error is not null)
{
return Error.IoError;
}
}
else
{
Manifest manifest;
try
{
manifest = await ManifestUtils.ReadOrCreateManifest(env);
}
catch (InvalidDataException)
{
logger.Error("Manifest file corrupted");
return Error.ManifestFileCorrupted;
}
catch (Exception e) when (e is not OperationCanceledException)
{
logger.Error("Error reading manifest file: " + e.Message);
return Error.ManifestIOError;
}

if (!options.Force && ManifestUtils.IsSdkInstalled(manifest, release.Sdk.Version, manifest.CurrentSdkDir))
{
logger.Log($"Version {release.Sdk.Version} is already installed in directory '{manifest.CurrentSdkDir.Name}'." +
" Skipping installation. To install anyway, pass --force.");
return release.Sdk.Version;
}

var error = await InstallCommand.InstallSdk(env, manifest, release.Sdk, release, manifest.CurrentSdkDir, logger);
if (error is not Result<Manifest, InstallError>.Ok)
{
return Error.IoError;
}
}

return component.Version;
return release.Sdk.Version;
}

/// <summary>
Expand Down Expand Up @@ -334,25 +388,25 @@ private static int LatestMajorComparison(SemVersion a, SemVersion b)
}

/// <summary>
/// Finds the component with the given version. If multiple components have the same version,
/// the one with the newest version is returned.
/// Finds the release with the given SDK version. If multiple releases have the same SDK version,
/// the one with the newest SDK version is returned.
/// </summary>
/// <param name="sdks">A list of the components in descending sort order.</param>
/// <param name="version">Version of the component to find.</param>
/// <param name="releases">A list of the releases in descending sort order.</param>
/// <param name="version">Version of the SDK to find.</param>
/// <param name="comparer">Custom comparison for versions.</param>
/// <returns>The component with the newest matching version, or null if no matching version is found.</returns>
private static ChannelReleaseIndex.Component? BinarySearchLatest(
List<ChannelReleaseIndex.Component> sdks,
/// <returns>The SDK with the newest matching version, or null if no matching version is found.</returns>
private static ChannelReleaseIndex.Release? BinarySearchLatest(
List<ChannelReleaseIndex.Release> releases,
SemVersion version,
Comparison<SemVersion> comparer)
{
// Note: the list is sorted in descending order.
int left = 0;
int right = sdks.Count;
int right = releases.Count;
while (left < right)
{
int mid = left + (right - left) / 2;
if (comparer(sdks[mid].Version, version) > 0)
if (comparer(releases[mid].Sdk.Version, version) > 0)
{
left = mid + 1;
}
Expand All @@ -361,20 +415,20 @@ private static int LatestMajorComparison(SemVersion a, SemVersion b)
right = mid;
}
}
return left < sdks.Count && comparer(sdks[left].Version, version) == 0 ? sdks[left] : null;
return left < releases.Count && comparer(releases[left].Sdk.Version, version) == 0 ? releases[left] : null;
}

/// <summary>
/// Returns a sorted (descending) list of SDks. If <paramref name="minMajorMinor"/> is not
/// null, only SDKs for that major+minor version are returned.
/// Returns list of releases sorted (descending) by SDK version. If <paramref name="minMajorMinor"/> is not
/// null, only releases for that major+minor version are returned.
/// </summary>
private static async Task<List<ChannelReleaseIndex.Component>> GetSortedSdks(
private static async Task<List<ChannelReleaseIndex.Release>> GetSortedReleases(
ScopedHttpClient httpClient,
DotnetReleasesIndex versionIndex,
bool allowPrerelease,
string minMajorMinor)
{
var sdks = new List<ChannelReleaseIndex.Component>();
var releases = new List<ChannelReleaseIndex.Release>();
foreach (var releaseIndex in versionIndex.ChannelIndices)
{
if (minMajorMinor is not null && double.Parse(minMajorMinor) > double.Parse(releaseIndex.MajorMinorVersion))
Expand All @@ -388,13 +442,13 @@ private static int LatestMajorComparison(SemVersion a, SemVersion b)
{
if (allowPrerelease || !sdk.Version.IsPrerelease)
{
sdks.Add(sdk);
releases.Add(release);
}
}
}
}
// Sort in descending order.
sdks.Sort((a, b) => b.Version.CompareSortOrderTo(a.Version));
return sdks;
releases.Sort((a, b) => b.Sdk.Version.CompareSortOrderTo(a.Sdk.Version));
return releases;
}
}
9 changes: 6 additions & 3 deletions test/UnitTests/CommandLineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -448,12 +448,15 @@ public void RestoreHelp(string param)
console,
[ "restore", param ]));
Assert.Equal("""
usage: dnvm restore [-h | --help]
usage: dnvm restore [-l | --local] [-f | --force] [-v | --verbose] [-h | --help]

Downloads the SDK in the global.json in or above the current directory to the
.dotnet folder in the same directory.
Downloads the SDK in the global.json in or above the current directory.

Options:
-l, --local Install the sdk into the .dotnet folder in the same directory
as global.json.
-f, --force Force install the SDK, even if already installed.
-v, --verbose Print extra debugging info to the console.
-h, --help Show help information.


Expand Down
Loading