File: Commands\Workload\Clean\WorkloadCleanCommand.cs
Web Access
Project: src\src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using Microsoft.Deployment.DotNet.Releases;
using Microsoft.DotNet.Cli.Commands.Workload.Install;
using Microsoft.DotNet.Cli.Commands.Workload.List;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.NET.Sdk.WorkloadManifestReader;

namespace Microsoft.DotNet.Cli.Commands.Workload.Clean;

internal sealed class WorkloadCleanCommand : WorkloadCommandBase<WorkloadCleanCommandDefinition>
{
    private readonly bool _cleanAll;

    private readonly string _dotnetPath;
    private readonly string _userProfileDir;

    private readonly ReleaseVersion _sdkVersion;
    private readonly IInstaller _workloadInstaller;
    private readonly IWorkloadResolverFactory _workloadResolverFactory;

    public WorkloadCleanCommand(
        ParseResult parseResult,
        IReporter? reporter = null,
        IWorkloadResolverFactory? workloadResolverFactory = null) : base(parseResult, reporter: reporter)
    {
        _cleanAll = parseResult.GetValue(Definition.CleanAllOption);

        _workloadResolverFactory = workloadResolverFactory ?? new WorkloadResolverFactory();

        if (!string.IsNullOrEmpty(parseResult.GetValue(Definition.SdkVersionOption)))
        {
            throw new GracefulException(CliCommandStrings.SdkVersionOptionNotSupported);
        }

        var creationResult = _workloadResolverFactory.Create();

        _dotnetPath = creationResult.DotnetPath;
        _userProfileDir = creationResult.UserProfileDir;
        _sdkVersion = creationResult.SdkVersion;

        var sdkFeatureBand = new SdkFeatureBand(_sdkVersion);
        _workloadInstaller = WorkloadInstallerFactory.GetWorkloadInstaller(Reporter, sdkFeatureBand, creationResult.WorkloadResolver, Verbosity, creationResult.UserProfileDir, VerifySignatures, PackageDownloader, creationResult.DotnetPath);
    }

    public override int Execute()
    {
        ExecuteGarbageCollection();
        return 0;
    }

    private void ExecuteGarbageCollection()
    {
        _workloadInstaller.GarbageCollect(workloadVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadVersion),
            cleanAllPacks: _cleanAll);

        DisplayUninstallableVSWorkloads();
    }

    /// <summary>
    /// 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.
    /// </summary>
    private void DisplayUninstallableVSWorkloads()
    {
#if TARGET_WINDOWS
        // 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<string> vsWorkloadUninstallWarnings = [];

            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(string.Format(CliCommandStrings.CannotAnalyzeVSWorkloadBand, sdkVersion, _dotnetPath, defaultDotnetWinPath).Yellow());
                        continue;
                    }

                    var workloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(bandedDotnetPath, sdkVersion, _userProfileDir, SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory));
                    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(CliCommandStrings.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(string.Format(CliCommandStrings.CannotAnalyzeVSWorkloadBand, sdkVersion, _dotnetPath, defaultDotnetWinPath).Yellow());
                    Utils.Reporter.Verbose.WriteLine(ex.Message);
                }
            }

            foreach (string warning in vsWorkloadUninstallWarnings)
            {
                Reporter.WriteLine(warning.Yellow());
            }
        }
#endif
    }
}