Skip to content

Commit

Permalink
Permit workload restore to work with new global.json (#42606)
Browse files Browse the repository at this point in the history
Fixes #42582

Specifying a workload set version in the global.json file is a supported scenario. If that workload set has not yet been installed, and the user tries to take an action that relies on workloads, we should error and indicate that they should make sure that workload set is installed first via update, install, or restore. We do that correctly, but when the user subsequently restores, we do not escape from the error case and instead direct the user to run restore. We should handle it properly, essentially installing the workload set before we discover which workloads the user needs and installing those.
  • Loading branch information
Forgind committed Aug 28, 2024
1 parent 6821c0d commit b35db9c
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 132 deletions.
2 changes: 1 addition & 1 deletion src/Cli/dotnet/commands/InstallingWorkloadCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ protected void UpdateWorkloadManifests(WorkloadHistoryRecorder recorder, ITransa
_workloadInstaller.UpdateInstallMode(_sdkFeatureBand, true);
}

if (SpecifiedWorkloadSetVersionInGlobalJson)
if (SpecifiedWorkloadSetVersionInGlobalJson && recorder is not null)
{
recorder.HistoryRecord.GlobalJsonVersion = _workloadSetVersionFromGlobalJson;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ public WorkloadInstallCommand(
INuGetPackageDownloader nugetPackageDownloader = null,
IWorkloadManifestUpdater workloadManifestUpdater = null,
string tempDirPath = null,
IReadOnlyCollection<string> workloadIds = null)
IReadOnlyCollection<string> workloadIds = null,
bool? skipWorkloadManifestUpdate = null)
: base(parseResult, reporter: reporter, workloadResolverFactory: workloadResolverFactory, workloadInstaller: workloadInstaller,
nugetPackageDownloader: nugetPackageDownloader, workloadManifestUpdater: workloadManifestUpdater,
tempDirPath: tempDirPath)
{
_skipManifestUpdate = parseResult.GetValue(WorkloadInstallCommandParser.SkipManifestUpdateOption);
_skipManifestUpdate = skipWorkloadManifestUpdate ?? parseResult.GetValue(WorkloadInstallCommandParser.SkipManifestUpdateOption);
_workloadIds = workloadIds ?? parseResult.GetValue(WorkloadInstallCommandParser.WorkloadIdArgument).ToList().AsReadOnly();
var resolvedReporter = _printDownloadLinkOnly ? NullReporter.Instance : Reporter;

Expand Down Expand Up @@ -126,91 +127,22 @@ public override int Execute()
}
else
{
WorkloadHistoryRecorder recorder = new WorkloadHistoryRecorder(_workloadResolver, _workloadInstaller, () => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, null));
recorder.HistoryRecord.CommandName = IsRunningRestore ? "restore" : "install";

try
{
recorder.Run(() =>
if (!IsRunningRestore)
{
// Normally we want to validate that the workload IDs specified were valid. However, if there is a global.json file with a workload
// set version specified, and we might install that workload version, then we don't do that check here, because we might not have the right
// workload set installed yet, and trying to list the available workloads would throw an error
if (_skipManifestUpdate || string.IsNullOrEmpty(_workloadSetVersionFromGlobalJson))
{
ValidateWorkloadIdsInput();
}
WorkloadHistoryRecorder recorder = new(_workloadResolver, _workloadInstaller, () => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, null));
recorder.HistoryRecord.CommandName = "install";

Reporter.WriteLine();
DirectoryPath? offlineCache = string.IsNullOrWhiteSpace(_fromCacheOption) ? null : new DirectoryPath(_fromCacheOption);
if (!_skipManifestUpdate)
recorder.Run(() =>
{
var installStateFilePath = Path.Combine(WorkloadInstallType.GetInstallStateFolder(_sdkFeatureBand, _workloadRootDir), "default.json");
if (string.IsNullOrWhiteSpace(_fromRollbackDefinition) &&
!SpecifiedWorkloadSetVersionOnCommandLine &&
!SpecifiedWorkloadSetVersionInGlobalJson &&
InstallStateContents.FromPath(installStateFilePath) is InstallStateContents installState &&
(installState.Manifests != null || installState.WorkloadVersion != null))
{
// If the workload version is pinned in the install state, then we don't want to automatically update workloads when a workload is installed
// To update to a new version, the user would need to run "dotnet workload update"
_skipManifestUpdate = true;
}
}
RunInNewTransaction(context =>
{
if (!_skipManifestUpdate)
{
if (Verbosity != VerbosityOptions.quiet && Verbosity != VerbosityOptions.q)
{
Reporter.WriteLine(LocalizableStrings.CheckForUpdatedWorkloadManifests);
}
UpdateWorkloadManifests(recorder, context, offlineCache);
}
// Add workload Ids that already exist to our collection to later trigger an update in those installed workloads
var workloadIds = _workloadIds.Select(id => new WorkloadId(id));
var installedWorkloads = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetInstalledWorkloads(_sdkFeatureBand);
var previouslyInstalledWorkloads = installedWorkloads.Intersect(workloadIds);
if (previouslyInstalledWorkloads.Any())
{
Reporter.WriteLine(string.Format(LocalizableStrings.WorkloadAlreadyInstalled, string.Join(" ", previouslyInstalledWorkloads)).Yellow());
}
workloadIds = workloadIds.Concat(installedWorkloads).Distinct();
workloadIds = WriteSDKInstallRecordsForVSWorkloads(workloadIds);
_workloadInstaller.InstallWorkloads(workloadIds, _sdkFeatureBand, context, offlineCache);
// Write workload installation records
var recordRepo = _workloadInstaller.GetWorkloadInstallationRecordRepository();
var newWorkloadInstallRecords = workloadIds.Except(recordRepo.GetInstalledWorkloads(_sdkFeatureBand));
context.Run(
action: () =>
{
foreach (var workloadId in newWorkloadInstallRecords)
{
recordRepo.WriteWorkloadInstallationRecord(workloadId, _sdkFeatureBand);
}
},
rollback: () =>
{
foreach (var workloadId in newWorkloadInstallRecords)
{
recordRepo.DeleteWorkloadInstallationRecord(workloadId, _sdkFeatureBand);
}
});
TryRunGarbageCollection(_workloadInstaller, Reporter, Verbosity, workloadSetVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadSetVersion), offlineCache);
Reporter.WriteLine();
Reporter.WriteLine(string.Format(LocalizableStrings.InstallationSucceeded, string.Join(" ", newWorkloadInstallRecords)));
Reporter.WriteLine();
InstallWorkloads(recorder);
});
});
}
else
{
InstallWorkloads(null);
}
}
catch (Exception e)
{
Expand All @@ -225,6 +157,87 @@ public override int Execute()
return _workloadInstaller.ExitCode;
}

private void InstallWorkloads(WorkloadHistoryRecorder recorder)
{
// Normally we want to validate that the workload IDs specified were valid. However, if there is a global.json file with a workload
// set version specified, and we might install that workload version, then we don't do that check here, because we might not have the right
// workload set installed yet, and trying to list the available workloads would throw an error
if (_skipManifestUpdate || string.IsNullOrEmpty(_workloadSetVersionFromGlobalJson))
{
ValidateWorkloadIdsInput();
}

Reporter.WriteLine();

DirectoryPath? offlineCache = string.IsNullOrWhiteSpace(_fromCacheOption) ? null : new DirectoryPath(_fromCacheOption);

if (!_skipManifestUpdate)
{
var installStateFilePath = Path.Combine(WorkloadInstallType.GetInstallStateFolder(_sdkFeatureBand, _workloadRootDir), "default.json");
if (string.IsNullOrWhiteSpace(_fromRollbackDefinition) &&
!SpecifiedWorkloadSetVersionOnCommandLine &&
!SpecifiedWorkloadSetVersionInGlobalJson &&
InstallStateContents.FromPath(installStateFilePath) is InstallStateContents installState &&
(installState.Manifests != null || installState.WorkloadVersion != null))
{
// If the workload version is pinned in the install state, then we don't want to automatically update workloads when a workload is installed
// To update to a new version, the user would need to run "dotnet workload update"
_skipManifestUpdate = true;
}
}

RunInNewTransaction(context =>
{
if (!_skipManifestUpdate)
{
if (Verbosity != VerbosityOptions.quiet && Verbosity != VerbosityOptions.q)
{
Reporter.WriteLine(LocalizableStrings.CheckForUpdatedWorkloadManifests);
}
UpdateWorkloadManifests(recorder, context, offlineCache);
}
// Add workload Ids that already exist to our collection to later trigger an update in those installed workloads
var workloadIds = _workloadIds.Select(id => new WorkloadId(id));
var installedWorkloads = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetInstalledWorkloads(_sdkFeatureBand);
var previouslyInstalledWorkloads = installedWorkloads.Intersect(workloadIds);
if (previouslyInstalledWorkloads.Any())
{
Reporter.WriteLine(string.Format(LocalizableStrings.WorkloadAlreadyInstalled, string.Join(" ", previouslyInstalledWorkloads)).Yellow());
}
workloadIds = workloadIds.Concat(installedWorkloads).Distinct();
workloadIds = WriteSDKInstallRecordsForVSWorkloads(workloadIds);
_workloadInstaller.InstallWorkloads(workloadIds, _sdkFeatureBand, context, offlineCache);
// Write workload installation records
var recordRepo = _workloadInstaller.GetWorkloadInstallationRecordRepository();
var newWorkloadInstallRecords = workloadIds.Except(recordRepo.GetInstalledWorkloads(_sdkFeatureBand));
context.Run(
action: () =>
{
foreach (var workloadId in newWorkloadInstallRecords)
{
recordRepo.WriteWorkloadInstallationRecord(workloadId, _sdkFeatureBand);
}
},
rollback: () =>
{
foreach (var workloadId in newWorkloadInstallRecords)
{
recordRepo.DeleteWorkloadInstallationRecord(workloadId, _sdkFeatureBand);
}
});
TryRunGarbageCollection(_workloadInstaller, Reporter, Verbosity, workloadSetVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadSetVersion), offlineCache);
Reporter.WriteLine();
Reporter.WriteLine(string.Format(LocalizableStrings.InstallationSucceeded, string.Join(" ", newWorkloadInstallRecords)));
Reporter.WriteLine();
});
}

internal static void TryRunGarbageCollection(IInstaller workloadInstaller, IReporter reporter, VerbosityOptions verbosity, Func<string, IWorkloadResolver> getResolverForWorkloadSet, DirectoryPath? offlineCache = null)
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
using Microsoft.Build.Execution;
using Microsoft.Build.Logging;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Workloads.Workload.Install;
using Microsoft.DotNet.Workloads.Workload.Update;
using Microsoft.Extensions.EnvironmentAbstractions;
using Microsoft.NET.Sdk.WorkloadManifestReader;

Expand All @@ -31,15 +33,38 @@ public WorkloadRestoreCommand(

public override int Execute()
{
var allProjects = DiscoverAllProjects(Directory.GetCurrentDirectory(), _slnOrProjectArgument).Distinct();
List<WorkloadId> allWorkloadId = RunTargetToGetWorkloadIds(allProjects);
Reporter.WriteLine(string.Format(LocalizableStrings.InstallingWorkloads, string.Join(" ", allWorkloadId)));
var workloadResolverFactory = new WorkloadResolverFactory();
var creationResult = workloadResolverFactory.Create();
var workloadInstaller = WorkloadInstallerFactory.GetWorkloadInstaller(NullReporter.Instance, new SdkFeatureBand(creationResult.SdkVersion),
creationResult.WorkloadResolver, Verbosity, creationResult.UserProfileDir, VerifySignatures, PackageDownloader,
creationResult.DotnetPath, TempDirectoryPath, null, RestoreActionConfiguration, elevationRequired: true);
var recorder = new WorkloadHistoryRecorder(
creationResult.WorkloadResolver,
workloadInstaller,
() => workloadResolverFactory.CreateForWorkloadSet(
creationResult.DotnetPath,
creationResult.SdkVersion.ToString(),
creationResult.UserProfileDir,
null));
recorder.HistoryRecord.CommandName = "restore";

recorder.Run(() =>
{
// First update manifests and install a workload set as necessary
new WorkloadUpdateCommand(_result, recorder: recorder).Execute();
var workloadInstallCommand = new WorkloadInstallCommand(_result,
workloadIds: allWorkloadId.Select(a => a.ToString()).ToList().AsReadOnly());
workloadInstallCommand.IsRunningRestore = true;
var allProjects = DiscoverAllProjects(Directory.GetCurrentDirectory(), _slnOrProjectArgument).Distinct();
List<WorkloadId> allWorkloadId = RunTargetToGetWorkloadIds(allProjects);
Reporter.WriteLine(string.Format(LocalizableStrings.InstallingWorkloads, string.Join(" ", allWorkloadId)));
workloadInstallCommand.Execute();
new WorkloadInstallCommand(_result,
workloadIds: allWorkloadId.Select(a => a.ToString()).ToList().AsReadOnly(),
skipWorkloadManifestUpdate: true)
{
IsRunningRestore = true
}.Execute();
});

return 0;
}

Expand Down
Loading

0 comments on commit b35db9c

Please sign in to comment.