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
4 changes: 4 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2691,4 +2691,8 @@ Proceed?</value>
<data name="DurationColon" xml:space="preserve">
<value>duration:</value>
</data>
<data name="WorkloadSetHasMissingManifests" xml:space="preserve">
<value>Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this.</value>
<comment>{0} is the workload set version. {Locked="dotnet workload repair"}</comment>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.DotNet.Cli.Commands.Workload;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using Microsoft.DotNet.Cli.Commands.Workload; looks unused in this file (types are either in the current namespace subtree or referenced via other usings). With warnings-as-errors enabled, this may fail the build; please remove the unnecessary using.

Suggested change
using Microsoft.DotNet.Cli.Commands.Workload;

Copilot uses AI. Check for mistakes.
using Microsoft.DotNet.Cli.Commands.Workload.Config;
using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords;
using Microsoft.DotNet.Cli.Extensions;
Expand Down Expand Up @@ -642,7 +643,7 @@ public IEnumerable<WorkloadHistoryRecord> GetWorkloadHistoryRecords(string sdkFe
public void Shutdown()
{
// Perform any additional cleanup here that's intended to run at the end of the command, regardless
// of success or failure. For file based installs, there shouldn't be any additional work to
// of success or failure. For file based installs, there shouldn't be any additional work to
// perform.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable disable

using Microsoft.DotNet.Cli.Commands.Workload;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer;
Expand Down Expand Up @@ -53,7 +54,7 @@ public static IInstaller GetWorkloadInstaller(

userProfileDir ??= CliFolderPathCalculator.DotnetUserProfileFolderPath;

return new FileBasedInstaller(
var installer = new FileBasedInstaller(
reporter,
sdkFeatureBand,
workloadResolver,
Expand All @@ -64,6 +65,25 @@ public static IInstaller GetWorkloadInstaller(
verbosity: verbosity,
packageSourceLocation: packageSourceLocation,
restoreActionConfig: restoreActionConfig);

// Attach corruption repairer to recover from corrupt workload sets
if (nugetPackageDownloader is not null &&
workloadResolver?.GetWorkloadManifestProvider() is SdkDirectoryWorkloadManifestProvider sdkProvider &&
sdkProvider.CorruptionRepairer is null)
{
sdkProvider.CorruptionRepairer = new WorkloadManifestCorruptionRepairer(
reporter,
installer,
workloadResolver,
sdkFeatureBand,
dotnetDir,
userProfileDir,
nugetPackageDownloader,
packageSourceLocation,
verbosity);
}

return installer;
}

private static bool CanWriteToDotnetRoot(string dotnetDir = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public override int Execute()
{
Reporter.WriteLine();

var workloadIds = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetInstalledWorkloads(new SdkFeatureBand(_sdkVersion));
var sdkFeatureBand = new SdkFeatureBand(_sdkVersion);
var workloadIds = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetInstalledWorkloads(sdkFeatureBand);

if (!workloadIds.Any())
{
Expand All @@ -80,7 +81,7 @@ public override int Execute()

Reporter.WriteLine(string.Format(CliCommandStrings.RepairingWorkloads, string.Join(" ", workloadIds)));

ReinstallWorkloadsBasedOnCurrentManifests(workloadIds, new SdkFeatureBand(_sdkVersion));
ReinstallWorkloadsBasedOnCurrentManifests(workloadIds, sdkFeatureBand);

WorkloadInstallCommand.TryRunGarbageCollection(_workloadInstaller, Reporter, Verbosity, workloadSetVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadSetVersion));

Expand All @@ -106,4 +107,5 @@ private void ReinstallWorkloadsBasedOnCurrentManifests(IEnumerable<WorkloadId> w
{
_workloadInstaller.RepairWorkloads(workloadIds, sdkFeatureBand);
}

}
4 changes: 4 additions & 0 deletions src/Cli/dotnet/Commands/Workload/WorkloadHistoryRecorder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ public void Run(Action workloadAction)
private WorkloadHistoryState GetWorkloadState()
{
var resolver = _workloadResolverFunc();
if (resolver.GetWorkloadManifestProvider() is SdkDirectoryWorkloadManifestProvider sdkProvider)
{
sdkProvider.CorruptionFailureMode = ManifestCorruptionFailureMode.Ignore;
}
var currentWorkloadVersion = resolver.GetWorkloadVersion().Version;
return new WorkloadHistoryState()
{
Expand Down
118 changes: 118 additions & 0 deletions src/Cli/dotnet/Commands/Workload/WorkloadManifestCorruptionRepairer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Commands.Workload.Install;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.NET.Sdk.WorkloadManifestReader;

namespace Microsoft.DotNet.Cli.Commands.Workload;

internal sealed class WorkloadManifestCorruptionRepairer : IWorkloadManifestCorruptionRepairer
{
private readonly IReporter _reporter;
private readonly IInstaller _workloadInstaller;
private readonly IWorkloadResolver _workloadResolver;
private readonly SdkFeatureBand _sdkFeatureBand;
private readonly string _dotnetPath;
private readonly string _userProfileDir;
private readonly INuGetPackageDownloader? _packageDownloader;
private readonly PackageSourceLocation? _packageSourceLocation;
private readonly VerbosityOptions _verbosity;

Comment on lines +16 to +24
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_sdkFeatureBand and _dotnetPath are assigned in the constructor but never used, which will generate warnings (likely treated as errors in this repo). Either remove these fields/constructor parameters, or use them (e.g., to scope the repair/diagnostics) so the build stays clean.

Copilot uses AI. Check for mistakes.
private bool _checked;

public WorkloadManifestCorruptionRepairer(
IReporter reporter,
IInstaller workloadInstaller,
IWorkloadResolver workloadResolver,
SdkFeatureBand sdkFeatureBand,
string dotnetPath,
string userProfileDir,
INuGetPackageDownloader? packageDownloader,
PackageSourceLocation? packageSourceLocation,
VerbosityOptions verbosity)
{
_reporter = reporter ?? NullReporter.Instance;
_workloadInstaller = workloadInstaller;
_workloadResolver = workloadResolver;
_sdkFeatureBand = sdkFeatureBand;
_dotnetPath = dotnetPath;
_userProfileDir = userProfileDir;
_packageDownloader = packageDownloader;
_packageSourceLocation = packageSourceLocation;
_verbosity = verbosity;
}

public void EnsureManifestsHealthy(ManifestCorruptionFailureMode failureMode)
{
if (_checked)
{
return;
}

_checked = true;

if (failureMode == ManifestCorruptionFailureMode.Ignore)
{
return;
}

Comment on lines +56 to +62
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_checked is set to true before the Ignore early-return. If the first call uses ManifestCorruptionFailureMode.Ignore, subsequent calls (e.g., later switching to Repair/Throw) will never re-check or repair. Consider moving the Ignore check before setting _checked, or track the strongest requested failure mode so later calls can still perform repair/throw.

Suggested change
_checked = true;
if (failureMode == ManifestCorruptionFailureMode.Ignore)
{
return;
}
if (failureMode == ManifestCorruptionFailureMode.Ignore)
{
return;
}
_checked = true;

Copilot uses AI. Check for mistakes.
// Get the workload set directly from the provider - it was already resolved during construction
// and doesn't require reading the install state file again
var provider = _workloadResolver.GetWorkloadManifestProvider() as SdkDirectoryWorkloadManifestProvider;
var workloadSet = provider?.ResolvedWorkloadSet;

if (workloadSet is null)
{
// No workload set is being used
return;
}

if (provider == null || !provider.HasMissingManifests(workloadSet))
{
return;
}

if (failureMode == ManifestCorruptionFailureMode.Throw)
{
throw new InvalidOperationException(string.Format(CliCommandStrings.WorkloadSetHasMissingManifests, workloadSet.Version));
}

_reporter.WriteLine($"Repairing workload set {workloadSet.Version}...");
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message "Repairing workload set {version}..." is user-facing and currently hardcoded. Consider moving it to CliCommandStrings (and XLF) for localization consistency with the rest of the workload command output.

Suggested change
_reporter.WriteLine($"Repairing workload set {workloadSet.Version}...");
_reporter.WriteLine(string.Format(CliCommandStrings.RepairingWorkloadSet, workloadSet.Version));

Copilot uses AI. Check for mistakes.
CliTransaction.RunNew(context => RepairCorruptWorkloadSet(context, workloadSet));
}

private void RepairCorruptWorkloadSet(ITransactionContext context, WorkloadSet workloadSet)
{
var manifestUpdates = CreateManifestUpdatesFromWorkloadSet(workloadSet);

foreach (var manifestUpdate in manifestUpdates)
{
_workloadInstaller.InstallWorkloadManifest(manifestUpdate, context);
}
}

[MemberNotNull(nameof(_packageDownloader))]
private IEnumerable<ManifestVersionUpdate> CreateManifestUpdatesFromWorkloadSet(WorkloadSet workloadSet)
{
if (_packageDownloader is null)
{
throw new InvalidOperationException("Package downloader is required to repair workload manifests.");
}

var manifestUpdater = new WorkloadManifestUpdater(
_reporter,
_workloadResolver,
_packageDownloader,
_userProfileDir,
_workloadInstaller.GetWorkloadInstallationRecordRepository(),
_workloadInstaller,
_packageSourceLocation,
displayManifestUpdates: _verbosity >= VerbosityOptions.detailed);

return manifestUpdater.CalculateManifestUpdatesForWorkloadSet(workloadSet);
}
}
5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading