diff --git a/src/Cli/dotnet/commands/InstallingWorkloadCommand.cs b/src/Cli/dotnet/commands/InstallingWorkloadCommand.cs index 89c8734b5041..225940874d55 100644 --- a/src/Cli/dotnet/commands/InstallingWorkloadCommand.cs +++ b/src/Cli/dotnet/commands/InstallingWorkloadCommand.cs @@ -162,7 +162,6 @@ protected async Task> GetDownloads(IEnumerable GetInstalledWorkloads(bool fromPreviousSdk) { - //var currentFeatureBand = new SdkFeatureBand(_installedFeatureBand.ToString()); if (fromPreviousSdk) { var priorFeatureBands = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetFeatureBandsWithInstallationRecords() diff --git a/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs b/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs index 5426119a16bd..5dfa4df737f6 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs @@ -1,6 +1,7 @@ // 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 System.Collections.Generic; using System.CommandLine; using System.CommandLine.Parsing; @@ -29,11 +30,11 @@ public static Command GetCommand() return Command; } - internal static void ShowWorkloadsInfo(ParseResult parseResult = null, IWorkloadInfoHelper workloadInfoHelper = null, IReporter reporter = null) + internal static void ShowWorkloadsInfo(ParseResult parseResult = null, IWorkloadInfoHelper workloadInfoHelper = null, IReporter reporter = null, string dotnetDir = null) { if(workloadInfoHelper != null) { - workloadInfoHelper ??= new WorkloadInfoHelper(parseResult.HasOption(SharedOptions.InteractiveOption)); + workloadInfoHelper ??= new WorkloadInfoHelper(parseResult != null ? parseResult.HasOption(SharedOptions.InteractiveOption) : false); } else { @@ -42,6 +43,7 @@ internal static void ShowWorkloadsInfo(ParseResult parseResult = null, IWorkload IEnumerable installedList = workloadInfoHelper.InstalledSdkWorkloadIds; InstalledWorkloadsCollection installedWorkloads = workloadInfoHelper.AddInstalledVsWorkloads(installedList); reporter ??= Cli.Utils.Reporter.Output; + string dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath); if (!installedList.Any()) { @@ -73,7 +75,7 @@ internal static void ShowWorkloadsInfo(ParseResult parseResult = null, IWorkload reporter.WriteLine($" {workloadManifest.ManifestPath,align}"); reporter.Write($"{separator}{CommonStrings.WorkloadInstallTypeColumn}:"); - reporter.WriteLine($" {WorkloadInstallerFactory.GetWorkloadInstallType(new SdkFeatureBand(workloadFeatureBand), workloadManifest.ManifestPath).ToString(),align}" + reporter.WriteLine($" {WorkloadInstallerFactory.GetWorkloadInstallType(new SdkFeatureBand(workloadFeatureBand), dotnetPath),align}" ); } } @@ -100,6 +102,7 @@ private static Command ConstructCommand() command.AddCommand(WorkloadUninstallCommandParser.GetCommand()); command.AddCommand(WorkloadRepairCommandParser.GetCommand()); command.AddCommand(WorkloadRestoreCommandParser.GetCommand()); + command.AddCommand(WorkloadCleanCommandParser.GetCommand()); command.AddCommand(WorkloadElevateCommandParser.GetCommand()); command.SetHandler((parseResult) => ProcessArgs(parseResult)); diff --git a/src/Cli/dotnet/commands/dotnet-workload/WorkloadInfoHelper.cs b/src/Cli/dotnet/commands/dotnet-workload/WorkloadInfoHelper.cs index 2de241171a29..3c5b6c202130 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/WorkloadInfoHelper.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/WorkloadInfoHelper.cs @@ -77,8 +77,7 @@ public InstalledWorkloadsCollection AddInstalledVsWorkloads(IEnumerable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Causes clean to remove and uninstall all workload components from all SDK versions. + + + Removes workload components that may have been left behind from previous updates and uninstallations. + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/WorkloadCleanCommand.cs b/src/Cli/dotnet/commands/dotnet-workload/clean/WorkloadCleanCommand.cs new file mode 100644 index 000000000000..9266978a4f85 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/WorkloadCleanCommand.cs @@ -0,0 +1,136 @@ +// 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 System.Collections.Generic; +using System.CommandLine; +using System.IO; +using Microsoft.Deployment.DotNet.Releases; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.Cli.NuGetPackageDownloader; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Configurer; +using Microsoft.DotNet.Installer.Windows; +using Microsoft.DotNet.Workloads.Workload.Install; +using Microsoft.DotNet.Workloads.Workload.Install.InstallRecord; +using Microsoft.DotNet.Workloads.Workload.List; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +#nullable enable + +namespace Microsoft.DotNet.Workloads.Workload.Clean +{ + internal class WorkloadCleanCommand : WorkloadCommandBase + { + private readonly bool _cleanAll; + + private string? _dotnetPath; + private string _userProfileDir; + + private readonly ReleaseVersion _sdkVersion; + private readonly IInstaller _workloadInstaller; + private readonly IWorkloadResolver _workloadResolver; + + public WorkloadCleanCommand( + ParseResult parseResult, + IReporter? reporter = null, + IWorkloadResolver? workloadResolver = null, + string? dotnetDir = null, + string? version = null, + string? userProfileDir = null + ) : base(parseResult, reporter: reporter) + { + _cleanAll = parseResult.GetValue(WorkloadCleanCommandParser.CleanAllOption); + + _dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath); + if (_dotnetPath == null) + { + throw new GracefulException(String.Format(LocalizableStrings.InvalidWorkloadProcessPath, Environment.ProcessPath ?? "null")); + } + + _userProfileDir = userProfileDir ?? CliFolderPathCalculator.DotnetUserProfileFolderPath; + + _sdkVersion = WorkloadOptionsExtensions.GetValidatedSdkVersion(parseResult.GetValue(WorkloadUninstallCommandParser.VersionOption), version, _dotnetPath, userProfileDir, true); + var sdkFeatureBand = new SdkFeatureBand(_sdkVersion); + + var workloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(_dotnetPath, _sdkVersion.ToString(), _userProfileDir); + _workloadResolver = workloadResolver ?? WorkloadResolver.Create(workloadManifestProvider, _dotnetPath, _sdkVersion.ToString(), _userProfileDir); + _workloadInstaller = WorkloadInstallerFactory.GetWorkloadInstaller(Reporter, sdkFeatureBand, _workloadResolver, Verbosity, _userProfileDir, VerifySignatures, PackageDownloader, _dotnetPath); + + } + + public override int Execute() + { + ExecuteGarbageCollection(); + return 0; + } + + private void ExecuteGarbageCollection() + { + _workloadInstaller.GarbageCollectInstalledWorkloadPacks(cleanAllPacks: _cleanAll); + DisplayUninstallableVSWorkloads(); + } + + /// + /// Print VS Workloads with the same machine arch which can't be uninstalled through the SDK CLI to increase user awareness that they must uninstall via VS. + /// + private void DisplayUninstallableVSWorkloads() + { +#if !DOT_NET_BUILD_FROM_SOURCE + // We don't want to print MSI related content in a file-based installation. + if (!(_workloadInstaller.GetType() == typeof(NetSdkMsiInstallerClient))) + { + return; + } + + if (OperatingSystem.IsWindows()) + { + // All VS Workloads should have a corresponding MSI based SDK. This means we can pull all of the VS SDK feature bands using MSI/VS related registry keys. + var installedSDKVersionsWithPotentialVSRecords = MsiInstallerBase.GetInstalledSdkVersions(); + HashSet vsWorkloadUninstallWarnings = new(); + + string defaultDotnetWinPath = MsiInstallerBase.GetDotNetHome(); + foreach (string sdkVersion in installedSDKVersionsWithPotentialVSRecords) + { + try + { +#pragma warning disable CS8604 // We error in the constructor if the dotnet path is null. + + // We don't know if the dotnet installation for the other bands is in a different directory than the current dotnet; check the default directory if it isn't. + var bandedDotnetPath = Path.Exists(Path.Combine(_dotnetPath, "sdk", sdkVersion)) ? _dotnetPath : defaultDotnetWinPath; + + if (!Path.Exists(bandedDotnetPath)) + { + Reporter.WriteLine(AnsiColorExtensions.Yellow(string.Format(LocalizableStrings.CannotAnalyzeVSWorkloadBand, sdkVersion, _dotnetPath, defaultDotnetWinPath))); + continue; + } + + var workloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(bandedDotnetPath, sdkVersion, _userProfileDir); + var bandedResolver = WorkloadResolver.Create(workloadManifestProvider, bandedDotnetPath, sdkVersion.ToString(), _userProfileDir); +#pragma warning restore CS8604 + + InstalledWorkloadsCollection vsWorkloads = new(); + VisualStudioWorkloads.GetInstalledWorkloads(bandedResolver, vsWorkloads, _cleanAll ? null : new SdkFeatureBand(sdkVersion)); + foreach (var vsWorkload in vsWorkloads.AsEnumerable()) + { + vsWorkloadUninstallWarnings.Add(string.Format(LocalizableStrings.VSWorkloadNotRemoved, $"{vsWorkload.Key}", $"{vsWorkload.Value}")); + } + } + catch (WorkloadManifestException ex) + { + // Limitation: We don't know the dotnetPath of the other feature bands when making the manifestProvider and resolvers. + // This can cause the manifest resolver to fail as it may look for manifests in an invalid path. + // It can theoretically be customized, but that is not currently supported for workloads with VS. + Reporter.WriteLine(AnsiColorExtensions.Yellow(string.Format(LocalizableStrings.CannotAnalyzeVSWorkloadBand, sdkVersion, _dotnetPath, defaultDotnetWinPath))); + Cli.Utils.Reporter.Verbose.WriteLine(ex.Message); + } + } + + foreach (string warning in vsWorkloadUninstallWarnings) + { + Reporter.WriteLine(warning.Yellow()); + } + } +#endif + } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/WorkloadCleanCommandParser.cs b/src/Cli/dotnet/commands/dotnet-workload/clean/WorkloadCleanCommandParser.cs new file mode 100644 index 000000000000..8ef5fae4eb8a --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/WorkloadCleanCommandParser.cs @@ -0,0 +1,32 @@ +// 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.CommandLine; +using Microsoft.DotNet.Workloads.Workload.Clean; +using LocalizableStrings = Microsoft.DotNet.Workloads.Workload.Clean.LocalizableStrings; + +namespace Microsoft.DotNet.Cli +{ + internal static class WorkloadCleanCommandParser + { + + public static readonly Option CleanAllOption = new Option("--all", LocalizableStrings.CleanAllOptionDescription); + + private static readonly Command Command = ConstructCommand(); + + public static Command GetCommand() + { + return Command; + } + + private static Command ConstructCommand() + { + Command command = new Command("clean", LocalizableStrings.CommandDescription); + + command.AddOption(CleanAllOption); + + command.SetHandler((parseResult) => new WorkloadCleanCommand(parseResult).Execute()); + + return command; + } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..82f66951c7a5 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.de.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..693cc41bb9c8 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.es.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..ab20f100cdc5 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.fr.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..54380c6e1227 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.it.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..011ffa256411 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.ja.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..f158bbfe1ceb --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.ko.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..91ee85af93d0 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.pl.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..d9c8b451ea9b --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..405068e580e2 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.ru.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..ab0da1e30519 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.tr.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..5e9e7eda8626 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..34211fefeba9 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..4a2b0c5aa4ce --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-workload/clean/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,34 @@ + + + + + + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK. +Paths searched: '{1}', '{2}'. + + + + Causes clean to remove and uninstall all workload components from all SDK versions. + Causes clean to remove and uninstall all workload components from all SDK versions. + + + + Removes workload components that may have been left behind from previous updates and uninstallations. + Removes workload components that may have been left behind from previous updates and uninstallations. + + + + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path. + + + + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it. + + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs b/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs index 245c526076f6..a4a4cc69b50e 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs @@ -5,19 +5,18 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.Configurer; using Microsoft.DotNet.ToolPackage; +using Microsoft.DotNet.Workloads.Workload.Install.InstallRecord; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; using NuGet.Versioning; using static Microsoft.NET.Sdk.WorkloadManifestReader.WorkloadResolver; -using Microsoft.DotNet.Workloads.Workload.Install.InstallRecord; -using System.Text.Json; -using System.Threading.Tasks; namespace Microsoft.DotNet.Workloads.Workload.Install @@ -291,7 +290,7 @@ public IEnumerable GetDownloads(IEnumerable worklo return packs.Select(p => new WorkloadDownload(p.Id, p.ResolvedPackageId, p.Version)).ToList(); } - public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = null) + public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = null, bool cleanAllPacks = false) { var installedSdkFeatureBands = _installationRecordRepository.GetFeatureBandsWithInstallationRecords(); _reporter.WriteLine(string.Format(LocalizableStrings.GarbageCollectingSdkFeatureBandsMessage, string.Join(" ", installedSdkFeatureBands))); @@ -307,30 +306,49 @@ public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = n { foreach (var packVersionDir in Directory.GetDirectories(packIdDir)) { - var bandRecords = Directory.GetFileSystemEntries(packVersionDir); + var bandPackRecordPaths = Directory.GetFileSystemEntries(packVersionDir); - var unneededBandRecords = bandRecords - .Where(recordPath => !installedSdkFeatureBands.Contains(new SdkFeatureBand(Path.GetFileName(recordPath)))); + var unneededBandRecordPaths = bandPackRecordPaths + .Where(recordPath => + { + var thisRecordFeatureBand = new SdkFeatureBand(Path.GetFileName(recordPath)); - var currentBandRecordPath = Path.Combine(packVersionDir, _sdkFeatureBand.ToString()); - if (bandRecords.Contains(currentBandRecordPath) && !currentBandInstallRecords.Contains(currentBandRecordPath)) - { - unneededBandRecords = unneededBandRecords.Append(currentBandRecordPath); - } + // Mark the pack record for garbage collection if we're cleaning all packs. + if (cleanAllPacks) + { + return true; + } + + // Mark the pack record for garbage collection if the feature band is not installed/has no workloads installed. + if (!installedSdkFeatureBands.Contains(thisRecordFeatureBand)) + { + return true; + } + + // Mark the pack record for garbage collection if the pack has no corresponding workload installation record and has the current SDK feature band. + if (thisRecordFeatureBand.Equals(_sdkFeatureBand) && !currentBandInstallRecords.Contains(recordPath)) + { + return true; + } + + return false; + } + ); - if (!unneededBandRecords.Any()) + if (!unneededBandRecordPaths.Any()) { continue; } // Save the pack info in case we need to delete the pack - var jsonPackInfo = File.ReadAllText(unneededBandRecords.First()); - foreach (var unneededRecord in unneededBandRecords) + var jsonPackInfo = File.ReadAllText(unneededBandRecordPaths.First()); + foreach (var unneededPackRecord in unneededBandRecordPaths) { - File.Delete(unneededRecord); + File.Delete(unneededPackRecord); } - if (!bandRecords.Except(unneededBandRecords).Any()) + // If there are no pack records left in the directory we garbage collected, we need to delete the pack and its director(ies). + if (!bandPackRecordPaths.Except(unneededBandRecordPaths).Any()) { Directory.Delete(packVersionDir); var deletablePack = GetPackInfo(packVersionDir); @@ -348,6 +366,29 @@ public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = n Directory.Delete(packIdDir); } } + + if (cleanAllPacks) + { + DeleteAllWorkloadInstallationRecords(); + } + } + + /// + /// Remove all workload installation records that aren't from Visual Studio. + /// + private void DeleteAllWorkloadInstallationRecords() + { + FileBasedInstallationRecordRepository workloadRecordRepository = new(_workloadMetadataDir); + var allFeatureBands = workloadRecordRepository.GetFeatureBandsWithInstallationRecords(); + + foreach (SdkFeatureBand potentialBandToClean in allFeatureBands) + { + var workloadInstallationRecordIds = workloadRecordRepository.GetInstalledWorkloads(potentialBandToClean); + foreach (WorkloadId workloadInstallationRecordId in workloadInstallationRecordIds) + { + workloadRecordRepository.DeleteWorkloadInstallationRecord(workloadInstallationRecordId, potentialBandToClean); + } + } } public void Shutdown() diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/IInstaller.cs b/src/Cli/dotnet/commands/dotnet-workload/install/IInstaller.cs index a33517254982..446070babfa1 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/IInstaller.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/install/IInstaller.cs @@ -20,7 +20,7 @@ internal interface IInstaller : IWorkloadManifestInstaller void RepairWorkloads(IEnumerable workloadIds, SdkFeatureBand sdkFeatureBand, DirectoryPath? offlineCache = null); - void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = null); + void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = null, bool cleanAllPacks = false); void InstallWorkloadManifest(ManifestVersionUpdate manifestUpdate, ITransactionContext transactionContext, DirectoryPath? offlineCache = null, bool isRollback = false); @@ -37,7 +37,7 @@ internal interface IInstaller : IWorkloadManifestInstaller void Shutdown(); - + } // Interface to pass to workload manifest updater diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/LocalizableStrings.resx b/src/Cli/dotnet/commands/dotnet-workload/install/LocalizableStrings.resx index 69e62a8c442c..f8f8ef02d0ec 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/LocalizableStrings.resx +++ b/src/Cli/dotnet/commands/dotnet-workload/install/LocalizableStrings.resx @@ -298,7 +298,7 @@ Workload updates are available. Run `dotnet workload list` for more information. - The machine has a pending reboot. Installation will continue, but you may need to restart. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. Installing {0} diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/MsiInstallerBase.cs b/src/Cli/dotnet/commands/dotnet-workload/install/MsiInstallerBase.cs index 52af2fdf922e..2c64ef832474 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/MsiInstallerBase.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/install/MsiInstallerBase.cs @@ -195,7 +195,7 @@ static void SetRecordMsiProperties(WorkloadPackRecord record, RegistryKey key) /// Determines the per-machine install location for .NET. This is similar to the logic in the standalone installers. /// /// The path where .NET is installed based on the host architecture and operating system bitness. - private string GetDotNetHome() + internal static string GetDotNetHome() { // Configure the default location, e.g., if the registry key is absent. Technically that would be suggesting // that the install is corrupt or we're being asked to run as an admin install in a non-admin deployment. @@ -418,7 +418,7 @@ protected string GetMsiLogName(WorkloadPackRecord record, InstallAction action) /// Get a list of all MSI based SDK installations that match the current host architecture. /// /// A collection of all the installed SDKs. The collection may be empty if no installed versions are found. - protected IEnumerable GetInstalledSdkVersions() + internal static IEnumerable GetInstalledSdkVersions() { // The SDK, regardless of the installer's platform, writes detection keys to the 32-bit hive. using RegistryKey hklm32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/NetSdkMsiInstallerClient.cs b/src/Cli/dotnet/commands/dotnet-workload/install/NetSdkMsiInstallerClient.cs index ef1f532497fb..4d4b9fd8ae01 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/NetSdkMsiInstallerClient.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/install/NetSdkMsiInstallerClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// 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; @@ -107,13 +107,15 @@ public IEnumerable GetDownloads(IEnumerable worklo /// /// Cleans up and removes stale workload packs. /// - public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = null) + public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = null, bool cleanAllPacks = false) { try { ReportPendingReboot(); - Log?.LogMessage("Starting garbage collection."); - IEnumerable installedFeatureBands = GetInstalledFeatureBands(); + Log?.LogMessage($"Starting garbage collection."); + Log?.LogMessage($"Garbage Collection Mode: CleanAllPacks={cleanAllPacks}."); + + IEnumerable installedFeatureBands = GetInstalledFeatureBands(Log); IEnumerable installedWorkloads = RecordRepository.GetInstalledWorkloads(_sdkFeatureBand); var installedPacks = installedWorkloads.SelectMany(workload => _workloadResolver.GetPacksInWorkload(workload)) @@ -156,55 +158,7 @@ public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = n { DependencyProvider depProvider = new DependencyProvider(packRecord.ProviderKeyName); - // Find all the dependents that look like they belong to SDKs. We only care - // about dependents that match the SDK host we're running under. For example, an x86 SDK should not be - // modifying the x64 MSI dependents. - IEnumerable sdkDependents = depProvider.Dependents - .Where(d => d.StartsWith($"{DependentPrefix}")) - .Where(d => d.EndsWith($",{HostArchitecture}")); - - foreach (string dependent in sdkDependents) - { - Log?.LogMessage($"Evaluating dependent for workload pack, dependent: {dependent}, MSI ID: {packRecord.MsiId}, MSI version: {packRecord.MsiNuGetVersion}"); - - // Dependents created by the SDK should have 3 parts, for example, "Microsoft.NET.Sdk,6.0.100,x86". - string[] dependentParts = dependent.Split(','); - - if (dependentParts.Length != 3) - { - Log?.LogMessage($"Skipping dependent: {dependent}"); - continue; - } - - try - { - SdkFeatureBand dependentFeatureBand = new SdkFeatureBand(dependentParts[1]); - - if (!installedFeatureBands.Contains(dependentFeatureBand)) - { - Log?.LogMessage($"Removing dependent '{dependent}' from provider key '{depProvider.ProviderKeyName}' because its SDK feature band does not match any installed feature bands."); - UpdateDependent(InstallRequestType.RemoveDependent, depProvider.ProviderKeyName, dependent); - } - - if (dependentFeatureBand.Equals(_sdkFeatureBand)) - { - // If the current SDK feature band is listed as a dependent, we can validate - // the workload packs against the expected pack IDs and versions to potentially remove it. - if (packRecord.InstalledPacks.All(p => !expectedWorkloadPacks.Contains((p.id, p.version.ToString())))) - { - // None of the packs installed by this MSI are necessary any longer for this feature band, so we can remove the reference count - Log?.LogMessage($"Removing dependent '{dependent}' because the pack record(s) do not match any expected packs."); - UpdateDependent(InstallRequestType.RemoveDependent, depProvider.ProviderKeyName, dependent); - } - } - } - catch (Exception e) - { - Log?.LogMessage($"{e.Message}"); - Log?.LogMessage($"{e.StackTrace}"); - continue; - } - } + UpdateDependentReferenceCounts(packRecord, depProvider, installedFeatureBands, expectedWorkloadPacks, cleanAllPacks); // Recheck the registry to see if there are any remaining dependents. If not, we can // remove the workload pack. We'll add it to the list and remove the packs at the end. @@ -217,46 +171,160 @@ public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = n else { packsToRemove.Add(packRecord); + Log?.LogMessage($"Removing {packRecord.MsiId} ({packRecord.MsiNuGetVersion}) as no dependents remain."); } } - foreach (WorkloadPackRecord record in packsToRemove) + RemoveWorkloadPacks(packsToRemove, offlineCache); + + if (cleanAllPacks) { - // We need to make sure the product is actually installed and that we're not dealing with an orphaned record, e.g. - // if a previous removal was interrupted. We can't safely clean up orphaned records because it's too expensive - // to query all installed components and determine the product codes associated with the component that - // created the record. - DetectState state = DetectPackage(record.ProductCode, out Version _); + DeleteAllWorkloadInstallationRecords(); + } + } + catch (Exception e) + { + LogException(e); + throw; + } + } - if (state == DetectState.Present) - { - // Manually construct the MSI payload package details - string id = $"{record.MsiId}.Msi.{HostArchitecture}"; - MsiPayload msi = GetCachedMsiPayload(id, record.MsiNuGetVersion.ToString(), offlineCache); + /// + /// Find all the dependents that look like they belong to SDKs. We only care + /// about dependents that match the SDK host we're running under. For example, an x86 SDK should not be + /// modifying the x64 MSI dependents. After this, decrement any dependents (registry keys) that should be removed. + /// + /// + /// + /// + /// + /// If true, decrement reference counts for all CLI MSI workloads. Elsewise, only deference dependents for orphaned packs. + private void UpdateDependentReferenceCounts( + WorkloadPackRecord packRecordToUpdate, + DependencyProvider depProvider, + IEnumerable installedFeatureBands, + HashSet<(WorkloadPackId id, string version)> expectedWorkloadPacks, + bool cleanAllPacks + ) + { + IEnumerable sdkDependents = depProvider.Dependents + .Where(d => d.StartsWith($"{DependentPrefix}")) + .Where(d => d.EndsWith($",{HostArchitecture}")); + + foreach (string dependent in sdkDependents) + { + Log?.LogMessage($"Evaluating dependent for workload pack, dependent: {dependent}, MSI ID: {packRecordToUpdate.MsiId}, MSI version: {packRecordToUpdate.MsiNuGetVersion}"); + + // Dependents created by the SDK should have 3 parts, for example, "Microsoft.NET.Sdk,6.0.100,x86". + string[] dependentParts = dependent.Split(','); - // Make sure the package we have in the cache matches with the record. If it doesn't, we'll do the uninstall - // the hard way - if (!string.Equals(record.ProductCode, msi.ProductCode, StringComparison.OrdinalIgnoreCase)) + if (dependentParts.Length != 3) + { + Log?.LogMessage($"Skipping dependent: {dependent}"); + continue; + } + + try + { + SdkFeatureBand dependentFeatureBand = new SdkFeatureBand(dependentParts[1]); + + if (!installedFeatureBands.Contains(dependentFeatureBand)) + { + Log?.LogMessage($"Removing dependent '{dependent}' from provider key '{depProvider.ProviderKeyName}' because its SDK feature band does not match any installed feature bands."); + UpdateDependent(InstallRequestType.RemoveDependent, depProvider.ProviderKeyName, dependent); + } + else if (cleanAllPacks) + { + Log?.LogMessage($"Adding dependent '{dependent}' for removal as part as dotnet has been told to clean everything."); + // VS will manage its own MSI packs, so no need to worry about decrementing too much here. + UpdateDependent(InstallRequestType.RemoveDependent, depProvider.ProviderKeyName, dependent); + } + else if (dependentFeatureBand.Equals(_sdkFeatureBand)) + { + // If the current SDK feature band is listed as a dependent, we can validate + // the workload packs against the expected pack IDs and versions to potentially remove it. + if (packRecordToUpdate.InstalledPacks.All(p => !expectedWorkloadPacks.Contains((p.id, p.version.ToString())))) { - Log?.LogMessage($"ProductCode mismatch! Cached package: {msi.ProductCode}, pack record: {record.ProductCode}."); - string logFile = GetMsiLogName(record, InstallAction.Uninstall); - uint error = ExecuteWithProgress(String.Format(LocalizableStrings.MsiProgressUninstall, id), () => UninstallMsi(record.ProductCode, logFile)); - ExitOnError(error, $"Failed to uninstall {msi.MsiPath}."); + // None of the packs installed by this MSI are necessary any longer for this feature band, so we can remove the reference count + Log?.LogMessage($"Removing dependent '{dependent}' because the pack record(s) do not match any expected packs."); + UpdateDependent(InstallRequestType.RemoveDependent, depProvider.ProviderKeyName, dependent); } else { - // No need to plan. We know that there are no other dependents, the MSI is installed and we - // want to remove it. - VerifyPackage(msi); - ExecutePackage(msi, InstallAction.Uninstall, id); + Log?.LogMessage($"Dependent '{dependent}' was not removed as the packs are still needed. Mode: {cleanAllPacks} | Dependent band: {dependentFeatureBand} | SDK band: {_sdkFeatureBand}."); } } + else + { + Log?.LogMessage($"Dependent '{dependent}' was not removed. Mode: {cleanAllPacks} | Dependent band: {dependentFeatureBand} | SDK band: {_sdkFeatureBand}."); + } + } + catch (Exception e) + { + Log?.LogMessage($"{e.Message}"); + Log?.LogMessage($"{e.StackTrace}"); + continue; } } - catch (Exception e) + } + + private void RemoveWorkloadPacks(List packsToRemove, DirectoryPath? offlineCache) + { + foreach (WorkloadPackRecord record in packsToRemove) { - LogException(e); - throw; + // We need to make sure the product is actually installed and that we're not dealing with an orphaned record, e.g. + // if a previous removal was interrupted. We can't safely clean up orphaned records because it's too expensive + // to query all installed components and determine the product codes associated with the component that + // created the record. + DetectState state = DetectPackage(record.ProductCode, out Version _); + + if (state == DetectState.Present) + { + // Manually construct the MSI payload package details + string id = $"{record.MsiId}.Msi.{HostArchitecture}"; + MsiPayload msi = GetCachedMsiPayload(id, record.MsiNuGetVersion.ToString(), offlineCache); + + // Make sure the package we have in the cache matches with the record. If it doesn't, we'll do the uninstall + // the hard way + if (!string.Equals(record.ProductCode, msi.ProductCode, StringComparison.OrdinalIgnoreCase)) + { + Log?.LogMessage($"ProductCode mismatch! Cached package: {msi.ProductCode}, pack record: {record.ProductCode}."); + string logFile = GetMsiLogName(record, InstallAction.Uninstall); + uint error = ExecuteWithProgress(String.Format(LocalizableStrings.MsiProgressUninstall, id), () => UninstallMsi(record.ProductCode, logFile)); + ExitOnError(error, $"Failed to uninstall {msi.MsiPath}."); + } + else + { + // No need to plan. We know that there are no other dependents, the MSI is installed and we + // want to remove it. + VerifyPackage(msi); + ExecutePackage(msi, InstallAction.Uninstall, id); + } + } + } + } + + /// + /// Remove all workload installation records that aren't from Visual Studio. + /// + private void DeleteAllWorkloadInstallationRecords() + { + var allFeatureBands = RecordRepository.GetFeatureBandsWithInstallationRecords(); + + Log?.LogMessage($"Attempting to delete all workload msi installation records."); + + foreach (SdkFeatureBand potentialBandToClean in allFeatureBands) + { + Log?.LogMessage($"Detected band with installation record: '{potentialBandToClean}'."); + + var workloadInstallationRecordIds = RecordRepository.GetInstalledWorkloads(potentialBandToClean); + foreach (WorkloadId workloadInstallationRecordId in workloadInstallationRecordIds) + { + Log?.LogMessage($"Workload {workloadInstallationRecordId} for '{potentialBandToClean}' has been marked for deletion."); + RecordRepository.DeleteWorkloadInstallationRecord(workloadInstallationRecordId, potentialBandToClean); + } + + Log?.LogMessage($"No more workloads detected in band: '{potentialBandToClean}'."); } } @@ -806,7 +874,7 @@ private string ExtractPackage(string packageId, string packageVersion, Directory /// Gets a set of all the installed SDK feature bands. /// /// A List of all the installed SDK feature bands. - private IEnumerable GetInstalledFeatureBands() + private static IEnumerable GetInstalledFeatureBands(ISetupLogger log = null) { HashSet installedFeatureBands = new(); foreach (string sdkVersion in GetInstalledSdkVersions()) @@ -817,7 +885,7 @@ private IEnumerable GetInstalledFeatureBands() } catch (Exception e) { - Log?.LogMessage($"Failed to map SDK version {sdkVersion} to a feature band. ({e.Message})"); + log?.LogMessage($"Failed to map SDK version {sdkVersion} to a feature band. ({e.Message})"); } } diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallRecords/RegistryWorkloadInstallationRecordRepository.cs b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallRecords/RegistryWorkloadInstallationRecordRepository.cs index 27379bf591f6..8b209581932a 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallRecords/RegistryWorkloadInstallationRecordRepository.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallRecords/RegistryWorkloadInstallationRecordRepository.cs @@ -1,11 +1,13 @@ // 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 System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Versioning; using Microsoft.DotNet.Installer.Windows; +using Microsoft.DotNet.Workloads.Workload.List; using Microsoft.NET.Sdk.WorkloadManifestReader; using Microsoft.Win32; @@ -83,8 +85,13 @@ public IEnumerable GetInstalledWorkloads(SdkFeatureBand sdkFeatureBa { using RegistryKey wrk = _baseKey.OpenSubKey(Path.Combine(BasePath, $"{sdkFeatureBand}")); + return GetWorkloadInstallationRecordsFromRegistry(wrk); + } + + private IEnumerable GetWorkloadInstallationRecordsFromRegistry(RegistryKey sdkFeatureBandWorkloadRegistry) + { // ToList() is needed to ensure deferred execution does not reference closed registry keys. - return wrk?.GetSubKeyNames().Select(id => new WorkloadId(id)).ToList() ?? Enumerable.Empty(); + return sdkFeatureBandWorkloadRegistry?.GetSubKeyNames().Select(id => new WorkloadId(id)).ToList() ?? Enumerable.Empty(); } public void WriteWorkloadInstallationRecord(WorkloadId workloadId, SdkFeatureBand sdkFeatureBand) diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.cs.xlf index 2733985b0d8c..6debe0aa475b 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.cs.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.cs.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - Počítač čeká na restartování. Instalace bude pokračovat, ale bude pravděpodobně nutné restartovat počítač. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + Počítač čeká na restartování. Instalace bude pokračovat, ale bude pravděpodobně nutné restartovat počítač. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.de.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.de.xlf index 944a437b462f..8123dae7275f 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.de.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.de.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - Für den Computer steht ein Neustart aus. Die Installation wird fortgesetzt, aber Sie müssen möglicherweise neu starten. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + Für den Computer steht ein Neustart aus. Die Installation wird fortgesetzt, aber Sie müssen möglicherweise neu starten. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.es.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.es.xlf index f6f18dda3a07..dfc87dad6d02 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.es.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.es.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - El equipo tiene un reinicio pendiente. La instalación continuará, pero es posible que tenga que reiniciar. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + El equipo tiene un reinicio pendiente. La instalación continuará, pero es posible que tenga que reiniciar. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.fr.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.fr.xlf index cf7e0f35c4a9..63f9d96f9535 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.fr.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.fr.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - Le redémarrage de l’ordinateur est en attente. L’installation va se poursuivre, mais vous devrez peut-être redémarrer. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + Le redémarrage de l’ordinateur est en attente. L’installation va se poursuivre, mais vous devrez peut-être redémarrer. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.it.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.it.xlf index e2d908f431a0..56686a1d6ad4 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.it.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.it.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - Il computer ha un riavvio in sospeso. L'installazione continuerà, ma potrebbe essere necessario riavviare. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + Il computer ha un riavvio in sospeso. L'installazione continuerà, ma potrebbe essere necessario riavviare. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ja.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ja.xlf index 221b698c7265..283201777779 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ja.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ja.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - コンピューターの再起動が保留中です。インストールは続行されますが、再起動が必要になる場合があります。 + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + コンピューターの再起動が保留中です。インストールは続行されますが、再起動が必要になる場合があります。 diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ko.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ko.xlf index 62bb350811a4..288de68fabd8 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ko.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ko.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - 컴퓨터에 보류 중인 재부팅이 있습니다. 설치는 계속되지만 다시 시작해야할 수 있습니다. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + 컴퓨터에 보류 중인 재부팅이 있습니다. 설치는 계속되지만 다시 시작해야할 수 있습니다. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.pl.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.pl.xlf index b44e0058b652..1e22defb0895 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.pl.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.pl.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - Maszyna oczekuje na ponowny rozruch. Instalacja będzie kontynuowana, ale może być konieczne ponowne uruchomienie. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + Maszyna oczekuje na ponowny rozruch. Instalacja będzie kontynuowana, ale może być konieczne ponowne uruchomienie. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.pt-BR.xlf index 1aa28f226027..630d53695af0 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.pt-BR.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.pt-BR.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - A máquina tem uma reinicialização pendente. A instalação vai continuar, mas talvez seja necessário reiniciar. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + A máquina tem uma reinicialização pendente. A instalação vai continuar, mas talvez seja necessário reiniciar. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ru.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ru.xlf index c91858a2af4f..189667fe8542 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ru.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.ru.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - Ожидается перезагрузка компьютера. Установка будет продолжена, но, возможно, потребуется перезагрузить компьютер. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + Ожидается перезагрузка компьютера. Установка будет продолжена, но, возможно, потребуется перезагрузить компьютер. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.tr.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.tr.xlf index 5a89bc39aac8..f6a12bb6305d 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.tr.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.tr.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - Makinede bekleyen bir yeniden başlatma işlemi var. Yükleme devam edecek ancak yeniden başlatmanız gerekebilir. + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + Makinede bekleyen bir yeniden başlatma işlemi var. Yükleme devam edecek ancak yeniden başlatmanız gerekebilir. diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.zh-Hans.xlf index d0b5e52cec4a..7f7d7dfa7477 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.zh-Hans.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - 计算机有挂起的重新启动。安装将继续,但可能需要重新启动。 + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + 计算机有挂起的重新启动。安装将继续,但可能需要重新启动。 diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.zh-Hant.xlf index 8b5a6615b6a5..b31e8dbbd1e5 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/commands/dotnet-workload/install/xlf/LocalizableStrings.zh-Hant.xlf @@ -198,8 +198,8 @@ - The machine has a pending reboot. Installation will continue, but you may need to restart. - 機器有擱置中的重新開機。安裝將繼續,但您可能需要重新開機。 + The machine has a pending reboot. The workload operation will continue, but you may need to restart. + 機器有擱置中的重新開機。安裝將繼續,但您可能需要重新開機。 diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/InstalledWorkloadsCollection.cs b/src/Cli/dotnet/commands/dotnet-workload/list/InstalledWorkloadsCollection.cs index 6c22094739ae..b3fb3161ccfe 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/InstalledWorkloadsCollection.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/list/InstalledWorkloadsCollection.cs @@ -22,6 +22,11 @@ public InstalledWorkloadsCollection(IEnumerable workloadIds, string _workloads = new Dictionary(workloadIds.Select(id => new KeyValuePair(id.ToString(), installationSource))); } + public InstalledWorkloadsCollection() + { + _workloads = new(); + } + public IEnumerable> AsEnumerable() => _workloads.AsEnumerable(); diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs b/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs index 0186b8f4b2be..020dcf1513a3 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs @@ -8,10 +8,11 @@ using System.Runtime.Versioning; using Microsoft.Deployment.DotNet.Releases; using Microsoft.DotNet.Workloads.Workload.Install.InstallRecord; +using Microsoft.DotNet.Workloads.Workload.List; using Microsoft.NET.Sdk.WorkloadManifestReader; using Microsoft.VisualStudio.Setup.Configuration; -namespace Microsoft.DotNet.Workloads.Workload.List +namespace Microsoft.DotNet.Workloads.Workload { /// /// Provides functionality to query the status of .NET workloads in Visual Studio. @@ -53,10 +54,12 @@ internal static IEnumerable GetAvailableVisualStudioWorkloads(IWorkloadR /// SDK installed by an instance matches the feature band of the currently executing SDK. /// /// The workload resolver used to obtain available workloads. - /// The feature band of the executing SDK. /// The collection of installed workloads to update. - internal static void GetInstalledWorkloads(IWorkloadResolver workloadResolver, SdkFeatureBand sdkFeatureBand, - InstalledWorkloadsCollection installedWorkloads) + /// The feature band of the executing SDK. + /// If null, then workloads from all feature bands in VS will be returned. + /// + internal static void GetInstalledWorkloads(IWorkloadResolver workloadResolver, + InstalledWorkloadsCollection installedWorkloads, SdkFeatureBand? sdkFeatureBand = null) { IEnumerable visualStudioWorkloadIds = GetAvailableVisualStudioWorkloads(workloadResolver); HashSet installedWorkloadComponents = new(); @@ -81,7 +84,7 @@ internal static void GetInstalledWorkloads(IWorkloadResolver workloadResolver, S continue; } - if (packageId.StartsWith(s_visualStudioSdkPackageIdPrefix)) + if (packageId.StartsWith(s_visualStudioSdkPackageIdPrefix)) // Check if the package owning SDK is installed via VS. Note: if a user checks to add a workload in VS but does not install the SDK, this will cause those workloads to be ignored. { // After trimming the package prefix we should be left with a valid semantic version. If we can't // parse the version we'll skip this instance. @@ -94,7 +97,7 @@ internal static void GetInstalledWorkloads(IWorkloadResolver workloadResolver, S SdkFeatureBand visualStudioSdkFeatureBand = new SdkFeatureBand(visualStudioSdkVersion); // The feature band of the SDK in VS must match that of the SDK on which we're running. - if (!visualStudioSdkFeatureBand.Equals(sdkFeatureBand)) + if (sdkFeatureBand != null && !visualStudioSdkFeatureBand.Equals(sdkFeatureBand)) { break; } @@ -140,8 +143,8 @@ private static List GetVisualStudioInstances() do { setupInstances.Next(1, instances, out fetched); - - if (fetched > 0) + + if (fetched > 0) { ISetupInstance2 instance = (ISetupInstance2)instances[0]; diff --git a/src/Cli/dotnet/commands/dotnet-workload/uninstall/WorkloadUninstallCommand.cs b/src/Cli/dotnet/commands/dotnet-workload/uninstall/WorkloadUninstallCommand.cs index 59b035669f6f..f04ecef2053e 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/uninstall/WorkloadUninstallCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/uninstall/WorkloadUninstallCommand.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Parsing; using System.IO; using System.Linq; using Microsoft.Deployment.DotNet.Releases; @@ -13,11 +12,7 @@ using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Configurer; using Microsoft.DotNet.Workloads.Workload.Install; -using Microsoft.DotNet.Workloads.Workload.Install.InstallRecord; -using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; -using NuGet.Common; -using Product = Microsoft.DotNet.Cli.Utils.Product; namespace Microsoft.DotNet.Workloads.Workload.Uninstall { diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index a0402e4e7bb1..0ef0abadbf36 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -70,6 +70,7 @@ + diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadManifestFormatException.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadManifestFormatException.cs index bfe1039df6a0..c895d92e0432 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadManifestFormatException.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadManifestFormatException.cs @@ -27,7 +27,7 @@ protected WorkloadManifestCompositionException(SerializationInfo info, Streaming } [Serializable] - abstract class WorkloadManifestException: Exception + abstract public class WorkloadManifestException: Exception { protected WorkloadManifestException() { } protected WorkloadManifestException(string messageFormat, params object?[] args) : base(string.Format (messageFormat, args)) { } diff --git a/src/Tests/dotnet-workload-install.Tests/GivenFileBasedWorkloadInstall.cs b/src/Tests/dotnet-workload-install.Tests/GivenFileBasedWorkloadInstall.cs index d06c8f4ba45c..4c90fc271c91 100644 --- a/src/Tests/dotnet-workload-install.Tests/GivenFileBasedWorkloadInstall.cs +++ b/src/Tests/dotnet-workload-install.Tests/GivenFileBasedWorkloadInstall.cs @@ -37,11 +37,11 @@ public GivenFileBasedWorkloadInstall(ITestOutputHelper log) : base(log) [Fact] public void GivenManagedInstallItCanGetFeatureBandsWhenFilesArePresent() { - SdkFeatureBand[] versions = new [] - { + SdkFeatureBand[] versions = new[] + { new SdkFeatureBand("6.0.100"), new SdkFeatureBand("6.0.300"), - new SdkFeatureBand("7.0.100") + new SdkFeatureBand("7.0.100") }; (string dotnetRoot, FileBasedInstaller installer, INuGetPackageDownloader _) = GetTestInstaller(); @@ -78,7 +78,7 @@ public void GivenManagedInstallItCanNotGetFeatureBandsWhenFilesAreNotPresent() public void GivenManagedInstallItCanGetInstalledWorkloads() { var version = "6.0.100"; - var workloads = new WorkloadId[] { new WorkloadId("test-workload-1"), new WorkloadId("test-workload-2"), new WorkloadId("test-workload3")}; + var workloads = new WorkloadId[] { new WorkloadId("test-workload-1"), new WorkloadId("test-workload-2"), new WorkloadId("test-workload3") }; var (dotnetRoot, installer, _) = GetTestInstaller(); // Write fake workloads @@ -251,8 +251,8 @@ public void GivenManagedInstallItCanGarbageCollect() Directory.CreateDirectory(pack.Path); } } - // Write fake install record for 6.0.100 - var workloadsRecordPath = Path.Combine(dotnetRoot, "metadata", "workloads", sdkVersions.First(), "InstalledWorkloads"); + // Write fake install record for 6.0.300 + var workloadsRecordPath = Path.Combine(dotnetRoot, "metadata", "workloads", sdkVersions[1], "InstalledWorkloads"); Directory.CreateDirectory(workloadsRecordPath); File.Create(Path.Combine(workloadsRecordPath, "xamarin-empty-mock")); @@ -336,8 +336,8 @@ public void GivenManagedInstallItDoesNotRemovePacksWithInstallRecords() Directory.CreateDirectory(pack.Path); } } - // Write fake workload install record for 6.0.100 - var installedWorkloadsPath = Path.Combine(dotnetRoot, "metadata", "workloads", sdkVersions.First(), "InstalledWorkloads", "xamarin-android-build"); + // Write fake workload install record for 6.0.300 + var installedWorkloadsPath = Path.Combine(dotnetRoot, "metadata", "workloads", sdkVersions[1], "InstalledWorkloads", "xamarin-android-build"); File.WriteAllText(installedWorkloadsPath, string.Empty); installer.GarbageCollectInstalledWorkloadPacks(); @@ -351,7 +351,7 @@ public void GivenManagedInstallItDoesNotRemovePacksWithInstallRecords() .Should() .BeTrue(); - var expectedRecordPath = Path.Combine(installedPacksPath, pack.Id, pack.Version, sdkVersions.First()); + var expectedRecordPath = Path.Combine(installedPacksPath, pack.Id, pack.Version, sdkVersions[1]); File.Exists(expectedRecordPath) .Should() .BeTrue(); @@ -382,8 +382,8 @@ public void GivenManagedInstallItCanInstallManifestVersion() mockNugetInstaller.DownloadCallParams[0].Should().BeEquivalentTo((new PackageId($"{manifestId}.manifest-{featureBand}"), new NuGetVersion(manifestVersion.ToString()), null as DirectoryPath?, null as PackageSourceLocation)); } - - [Fact] + + [Fact] public void GivenManagedInstallItCanGetDownloads() { var (dotnetRoot, installer, nugetInstaller) = GetTestInstaller(); @@ -437,7 +437,7 @@ public void GivenManagedInstallItCanInstallPacksFromOfflineCache() CliTransaction.RunNew(context => installer.InstallWorkloads(new[] { new WorkloadId("android-sdk-workload") }, new SdkFeatureBand(version), context, new DirectoryPath(cachePath))); var mockNugetInstaller = nugetInstaller as MockNuGetPackageDownloader; - + // We shouldn't download anything, use the cache mockNugetInstaller.DownloadCallParams.Count.Should().Be(0); @@ -458,7 +458,7 @@ public void GivenManagedInstallItCanErrorsWhenMissingOfflineCache() var packVersion = "8.4.7"; var version = "6.0.100"; var cachePath = Path.Combine(dotnetRoot, "MockCache"); - + var exceptionThrown = Assert.Throws(() => CliTransaction.RunNew(context => installer.InstallWorkloads(new[] { new WorkloadId("android-sdk-workload") }, new SdkFeatureBand(version), context, new DirectoryPath(cachePath)))); exceptionThrown.Message.Should().Contain(packId); @@ -471,9 +471,9 @@ public void GivenManagedInstallItCanErrorsWhenMissingOfflineCache() { var testDirectory = _testAssetsManager.CreateTestDirectory(testName, identifier: identifier).Path; var dotnetRoot = Path.Combine(testDirectory, "dotnet"); - INuGetPackageDownloader nugetInstaller = failingInstaller ? new FailingNuGetPackageDownloader(testDirectory) : new MockNuGetPackageDownloader(dotnetRoot, manifestDownload); + INuGetPackageDownloader nugetInstaller = failingInstaller ? new FailingNuGetPackageDownloader(testDirectory) : new MockNuGetPackageDownloader(dotnetRoot, manifestDownload); var workloadResolver = WorkloadResolver.CreateForTests(new MockManifestProvider(new[] { _manifestPath }), dotnetRoot); - var sdkFeatureBand = new SdkFeatureBand("6.0.100"); + var sdkFeatureBand = new SdkFeatureBand("6.0.300"); return (dotnetRoot, new FileBasedInstaller(_reporter, sdkFeatureBand, workloadResolver, userProfileDir: testDirectory, nugetInstaller, dotnetRoot, packageSourceLocation: packageSourceLocation), nugetInstaller); } } diff --git a/src/Tests/dotnet-workload-install.Tests/MockPackWorkloadInstaller.cs b/src/Tests/dotnet-workload-install.Tests/MockPackWorkloadInstaller.cs index 1c95ccf172e4..8d4967051431 100644 --- a/src/Tests/dotnet-workload-install.Tests/MockPackWorkloadInstaller.cs +++ b/src/Tests/dotnet-workload-install.Tests/MockPackWorkloadInstaller.cs @@ -21,7 +21,7 @@ internal class MockPackWorkloadInstaller : IInstaller { public IList InstalledPacks; public List RolledBackPacks = new List(); - public IList<(ManifestVersionUpdate manifestUpdate, DirectoryPath? offlineCache)> InstalledManifests = + public IList<(ManifestVersionUpdate manifestUpdate, DirectoryPath? offlineCache)> InstalledManifests = new List<(ManifestVersionUpdate manifestUpdate, DirectoryPath?)>(); public string CachePath; public bool GarbageCollectionCalled = false; @@ -34,7 +34,7 @@ internal class MockPackWorkloadInstaller : IInstaller public int ExitCode => 0; - public MockPackWorkloadInstaller(string failingWorkload = null, string failingPack = null, bool failingRollback = false, IList installedWorkloads = null, + public MockPackWorkloadInstaller(string failingWorkload = null, string failingPack = null, bool failingRollback = false, IList installedWorkloads = null, IList installedPacks = null, bool failingGarbageCollection = false) { InstallationRecordRepository = new MockInstallationRecordRepository(failingWorkload, installedWorkloads); @@ -92,7 +92,7 @@ public void InstallWorkloads(IEnumerable workloadIds, SdkFeatureBand public void RepairWorkloads(IEnumerable workloadIds, SdkFeatureBand sdkFeatureBand, DirectoryPath? offlineCache = null) => throw new NotImplementedException(); - public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = null) + public void GarbageCollectInstalledWorkloadPacks(DirectoryPath? offlineCache = null, bool cleanAllPacks = false) { if (FailingGarbageCollection) { diff --git a/src/Tests/dotnet.Tests/dotnet-workload-clean/GivenDotnetWorkloadClean.cs b/src/Tests/dotnet.Tests/dotnet-workload-clean/GivenDotnetWorkloadClean.cs new file mode 100644 index 000000000000..4ed36cd7f48b --- /dev/null +++ b/src/Tests/dotnet.Tests/dotnet-workload-clean/GivenDotnetWorkloadClean.cs @@ -0,0 +1,199 @@ +using System.CommandLine; +using System.IO; +using Xunit.Abstractions; +using Xunit; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Utilities; +using Microsoft.DotNet.Cli.NuGetPackageDownloader; +using Microsoft.DotNet.Cli.Workload.Install.Tests; +using ManifestReaderTests; +using Microsoft.NET.Sdk.WorkloadManifestReader; +using Microsoft.DotNet.Workloads.Workload; +using FluentAssertions; +using Microsoft.DotNet.Workloads.Workload.Install; +using Microsoft.DotNet.Workloads.Workload.Repair; +using Microsoft.DotNet.Workloads.Workload.Clean; +using Microsoft.DotNet.Workloads.Workload.List; +using Microsoft.DotNet.Cli.Workload.List.Tests; +using System.Collections.Generic; +using System.CommandLine.Parsing; + +namespace Microsoft.DotNet.Cli.Workload.Clean.Tests +{ + public class GivenDotnetWorkloadClean : SdkTest + { + private readonly BufferedReporter _reporter; + private readonly string _manifestPath; + + private MockWorkloadManifestUpdater _manifestUpdater = new(); + private readonly string _sdkFeatureVersion = "6.0.100"; + private readonly string _installingWorkload = "xamarin-android"; + private readonly string dotnet = nameof(dotnet); + private readonly string _profileDirectoryLeafName = "user-profile"; + + private (string testDirectory, string dotnetRoot, string userProfileDir, WorkloadResolver workloadResolver, MockNuGetPackageDownloader nugetDownloader) Setup(bool userLocal, bool cleanAll) + { + var testDirectory = _testAssetsManager.CreateTestDirectory(identifier: userLocal ? $"userlocal-{cleanAll}" : $"default-{cleanAll}").Path; + var dotnetRoot = Path.Combine(testDirectory, dotnet); + var userProfileDir = Path.Combine(testDirectory, _profileDirectoryLeafName); + var workloadResolver = WorkloadResolver.CreateForTests(new MockManifestProvider(new[] { _manifestPath }), dotnetRoot, userLocal, userProfileDir); + var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot); + + return (testDirectory, dotnetRoot, userProfileDir, workloadResolver, nugetDownloader); + } + + public GivenDotnetWorkloadClean(ITestOutputHelper log) : base(log) + { + _reporter = new BufferedReporter(); + _manifestPath = Path.Combine(_testAssetsManager.GetAndValidateTestProjectDirectory("SampleManifest"), "Sample.json"); + } + + [Theory] + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public void GivenWorkloadCleanFileBasedItRemovesPacksAndPackRecords(bool userLocal, bool cleanAll) + { + var (testDirectory, dotnetRoot, userProfileDir, workloadResolver, nugetDownloader) = Setup(userLocal, cleanAll); + + string installRoot = userLocal ? userProfileDir : dotnetRoot; + if (userLocal) + { + WorkloadFileBasedInstall.SetUserLocal(dotnetRoot, _sdkFeatureVersion); + } + + // Test + InstallWorkload(userProfileDir, dotnetRoot, testDirectory, workloadResolver, nugetDownloader); + + var extraPackRecordPath = MakePackRecord(installRoot); + var extraPackPath = MakePack(installRoot); + + + var cleanCommand = cleanAll ? GenerateWorkloadCleanAllCommand(workloadResolver, userProfileDir, dotnetRoot) : GenerateWorkloadCleanCommand(workloadResolver, userProfileDir, dotnetRoot); + cleanCommand.Execute(); + + AssertExtraneousPacksAreRemoved(extraPackPath, extraPackRecordPath); + AssertValidPackCountsMatchExpected(installRoot, expectedPackCount: cleanAll ? 0 : 7, expectedPackRecordCount: cleanAll ? 0 : 8); + + AssertAdjacentCommandsStillPass(userProfileDir, dotnetRoot, testDirectory, workloadResolver, nugetDownloader); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GivenWorkloadCleanAllFileBasedItCleansAllFeatureBands(bool userLocal) + { + var (testDirectory, dotnetRoot, userProfileDir, workloadResolver, nugetDownloader) = Setup(userLocal, true); + + const string aboveSdkFeatureBand = ToolsetInfo.NextTargetFrameworkVersion + ".100"; + const string belowSdkFeatureBand = "5.0.100"; // At the time of writing this test, it would only run on 7-8.0 SDKs or above. + + string installRoot = userLocal ? userProfileDir : dotnetRoot; + if (userLocal) + { + WorkloadFileBasedInstall.SetUserLocal(dotnetRoot, _sdkFeatureVersion); + } + + // Test + InstallWorkload(userProfileDir, dotnetRoot, testDirectory, workloadResolver, nugetDownloader); + + var extraAboveBandPackRecordPath = MakePackRecord(installRoot, aboveSdkFeatureBand); + var extraBelowBandPackRecordPath = MakePackRecord(installRoot, belowSdkFeatureBand); + var extraPackPath = MakePack(installRoot); + var workloadInstallationRecordDirectory = Path.Combine(installRoot, "metadata", "workloads", _sdkFeatureVersion, "InstalledWorkloads"); + var oldWorkloadInstallationRecordDirectory = workloadInstallationRecordDirectory.Replace(_sdkFeatureVersion, belowSdkFeatureBand); + MakePseudoWorkloadRecord(oldWorkloadInstallationRecordDirectory); + + var cleanCommand = GenerateWorkloadCleanAllCommand(workloadResolver, userProfileDir, dotnetRoot); + cleanCommand.Execute(); + + AssertExtraneousPacksAreRemoved(extraPackPath, extraBelowBandPackRecordPath, true); + AssertExtraneousPacksAreRemoved(extraPackPath, extraAboveBandPackRecordPath); + AssertWorkloadInstallationRecordIsRemoved(workloadInstallationRecordDirectory); + AssertWorkloadInstallationRecordIsRemoved(oldWorkloadInstallationRecordDirectory); + AssertValidPackCountsMatchExpected(installRoot, expectedPackCount: 0, expectedPackRecordCount: 0); + } + + private void InstallWorkload(string userProfileDir, string dotnetRoot, string testDirectory, WorkloadResolver workloadResolver, MockNuGetPackageDownloader nugetDownloader, string sdkBand = null) + { + sdkBand ??= _sdkFeatureVersion; + + var installParseResult = Parser.Instance.Parse(new string[] { "dotnet", "workload", "install", _installingWorkload }); + var installCommand = new WorkloadInstallCommand(installParseResult, reporter: _reporter, workloadResolver: workloadResolver, nugetPackageDownloader: nugetDownloader, + workloadManifestUpdater: _manifestUpdater, userProfileDir: userProfileDir, version: sdkBand, dotnetDir: dotnetRoot, tempDirPath: testDirectory, installedFeatureBand: sdkBand); + + installCommand.Execute(); + } + + private WorkloadCleanCommand GenerateWorkloadCleanCommand(WorkloadResolver workloadResolver, string userProfileDir, string dotnetRoot) + { + var cleanParseResult = Parser.Instance.Parse(new string[] { "dotnet", "workload", "clean" }); + return MakeWorkloadCleanCommand(cleanParseResult, workloadResolver, userProfileDir, dotnetRoot); + } + + private WorkloadCleanCommand MakeWorkloadCleanCommand(ParseResult parseResult, WorkloadResolver workloadResolver, string userProfileDir, string dotnetRoot) + { + return new WorkloadCleanCommand(parseResult, reporter: _reporter, workloadResolver: workloadResolver, userProfileDir: userProfileDir, + version: _sdkFeatureVersion, dotnetDir: dotnetRoot); + } + + private WorkloadCleanCommand GenerateWorkloadCleanAllCommand(WorkloadResolver workloadResolver, string userProfileDir, string dotnetRoot) + { + var cleanParseResult = Parser.Instance.Parse(new string[] { "dotnet", "workload", "clean", "--all" }); + return MakeWorkloadCleanCommand(cleanParseResult, workloadResolver, userProfileDir, dotnetRoot); + } + + private string MakePackRecord(string installRoot, string sdkBand = null) + { + sdkBand ??= _sdkFeatureVersion; + + var packRecordPath = Path.Combine(installRoot, "metadata", "workloads", "InstalledPacks", "v1", "Test.Pack.A", "1.0.0", sdkBand); + Directory.CreateDirectory(Path.GetDirectoryName(packRecordPath)); + File.WriteAllText(packRecordPath, string.Empty); + return packRecordPath; + } + + private string MakePack(string installRoot) + { + var packPath = Path.Combine(installRoot, "packs", "Test.Pack.A", "1.0.0"); + Directory.CreateDirectory(packPath); + return packPath; + } + + private void MakePseudoWorkloadRecord(string installationPath) + { + Directory.CreateDirectory(installationPath); + File.WriteAllText(Path.Combine(installationPath, "foo"), ""); + } + + private void AssertExtraneousPacksAreRemoved(string extraPackPath, string extraPackRecordPath, bool entirePackRootPathShouldRemain = false) + { + File.Exists(extraPackRecordPath).Should().BeFalse(); + if (!entirePackRootPathShouldRemain) + { + Directory.Exists(Path.GetDirectoryName(Path.GetDirectoryName(extraPackRecordPath))).Should().BeFalse(); + Directory.Exists(extraPackPath).Should().BeFalse(); + } + } + + private void AssertWorkloadInstallationRecordIsRemoved(string workloadInstallationRecordDirectory) + { + Assert.Equal(Directory.GetFiles(workloadInstallationRecordDirectory), System.Array.Empty()); + } + + private void AssertValidPackCountsMatchExpected(string installRoot, int expectedPackCount, int expectedPackRecordCount) + { + Directory.GetDirectories(Path.Combine(installRoot, "packs")).Length.Should().Be(expectedPackCount); + Directory.GetDirectories(Path.Combine(installRoot, "metadata", "workloads", "InstalledPacks", "v1")).Length.Should().Be(expectedPackRecordCount); + } + + /// + /// Validate that commands that are likely to fail with invalid packs or invalid pack records do not fail, as an "end to end" safety precaution. + /// + private void AssertAdjacentCommandsStillPass(string userProfileDir, string dotnetRoot, string testDirectory, WorkloadResolver workloadResolver, MockNuGetPackageDownloader nugetDownloader, string sdkBand = null) + { + InstallWorkload(userProfileDir, dotnetRoot, testDirectory, workloadResolver, nugetDownloader, sdkBand); + } + } +}