File: Commands\Workload\WorkloadInfoHelper.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.Install.WorkloadInstallRecords;
using Microsoft.DotNet.Cli.Commands.Workload.List;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer;
using Microsoft.NET.Sdk.WorkloadManifestReader;
using Product = Microsoft.DotNet.Cli.Utils.Product;

namespace Microsoft.DotNet.Cli.Commands.Workload;

internal class WorkloadInfoHelper : IWorkloadInfoHelper
{
    public readonly SdkFeatureBand _currentSdkFeatureBand;
    private readonly string? _targetSdkVersion;
    public string DotnetPath { get; }
    public string UserLocalPath { get; }

    public WorkloadInfoHelper(
        bool isInteractive,
        VerbosityOptions verbosity = VerbosityOptions.normal,
        string? targetSdkVersion = null,
        bool? verifyMsiSignature = null,
        IReporter? reporter = null,
        IWorkloadInstallationRecordRepository? workloadRecordRepo = null,
        string? currentSdkVersion = null,
        string? dotnetDir = null,
        string? userProfileDir = null,
        IWorkloadResolver? workloadResolver = null)
    {
        DotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath)!;
        ReleaseVersion currentSdkReleaseVersion = new(currentSdkVersion ?? Product.Version);
        _currentSdkFeatureBand = new SdkFeatureBand(currentSdkReleaseVersion);

        _targetSdkVersion = targetSdkVersion;
        userProfileDir ??= CliFolderPathCalculator.DotnetUserProfileFolderPath;
        ManifestProvider =
            new SdkDirectoryWorkloadManifestProvider(DotnetPath,
                string.IsNullOrWhiteSpace(_targetSdkVersion)
                    ? currentSdkReleaseVersion.ToString()
                    : _targetSdkVersion,
                userProfileDir, SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory));
        WorkloadResolver = workloadResolver ?? NET.Sdk.WorkloadManifestReader.WorkloadResolver.Create(
            ManifestProvider, DotnetPath,
            currentSdkReleaseVersion.ToString(), userProfileDir);

        var restoreConfig = new RestoreActionConfig(Interactive: isInteractive);

        // When verifyMsiSignature is not specified by the caller, we delegate to
        // WorkloadUtilities.ShouldVerifySignatures() which respects the registry policy
        // and dotnet host signing status.
        Installer = WorkloadInstallerFactory.GetWorkloadInstaller(
            reporter,
            _currentSdkFeatureBand,
            WorkloadResolver,
            verbosity,
            userProfileDir,
            verifyMsiSignature: verifyMsiSignature ?? WorkloadUtilities.ShouldVerifySignatures(),
            restoreActionConfig: restoreConfig,
            elevationRequired: false,
            shouldLog: false);

        WorkloadRecordRepo = workloadRecordRepo ?? Installer.GetWorkloadInstallationRecordRepository();

        UserLocalPath = dotnetDir ?? (WorkloadFileBasedInstall.IsUserLocal(DotnetPath, _currentSdkFeatureBand.ToString()) ? userProfileDir : DotnetPath);
    }

    public IInstaller Installer { get; private init; }
    public SdkDirectoryWorkloadManifestProvider ManifestProvider { get; }
    public IWorkloadInstallationRecordRepository WorkloadRecordRepo { get; private init; }
    public IWorkloadResolver WorkloadResolver { get; private init; }

    public IEnumerable<WorkloadId> InstalledSdkWorkloadIds => WorkloadRecordRepo.GetInstalledWorkloads(_currentSdkFeatureBand);

    public InstalledWorkloadsCollection AddInstalledVsWorkloads(IEnumerable<WorkloadId> sdkWorkloadIds)
    {
        InstalledWorkloadsCollection installedWorkloads = new(sdkWorkloadIds, $"SDK {_currentSdkFeatureBand}");
#if TARGET_WINDOWS
        if (OperatingSystem.IsWindows())
        {
            VisualStudioWorkloads.GetInstalledWorkloads(WorkloadResolver, installedWorkloads);
        }
#endif
        return installedWorkloads;
    }

    public void CheckTargetSdkVersionIsValid()
    {
        if (!string.IsNullOrWhiteSpace(_targetSdkVersion))
        {
            if (new SdkFeatureBand(_targetSdkVersion).CompareTo(_currentSdkFeatureBand) < 0)
            {
                throw new ArgumentException(
                    $"Version band of {_targetSdkVersion} --- {new SdkFeatureBand(_targetSdkVersion)} should not be smaller than current version band {_currentSdkFeatureBand}");
            }
        }
    }

    /// <inheritdoc/>
    public IEnumerable<WorkloadResolver.WorkloadInfo> InstalledAndExtendedWorkloads
    {
        get
        {
            var installed = AddInstalledVsWorkloads(InstalledSdkWorkloadIds);

            return WorkloadResolver.GetExtendedWorkloads(
                installed.AsEnumerable().Select(t => new WorkloadId(t.Key)));
        }
    }

    internal static string GetWorkloadsVersion(WorkloadInfoHelper? workloadInfoHelper = null)
    {
        workloadInfoHelper ??= new WorkloadInfoHelper(false);

        var versionInfo = workloadInfoHelper.ManifestProvider.GetWorkloadVersion();

        // The explicit space here is intentional, as it's easy to miss in localization and crucial for parsing
        return versionInfo.Version + (versionInfo.IsInstalled ? string.Empty : ' ' + CliCommandStrings.WorkloadVersionNotInstalledShort);
    }

    internal void ShowWorkloadsInfo(IReporter? reporter = null, string? dotnetDir = null, bool showVersion = true)
    {
        reporter ??= Reporter.Output;
        var versionInfo = ManifestProvider.GetWorkloadVersion();

        void WriteUpdateModeAndAnyError(string indent = "")
        {
            var useWorkloadSets = InstallStateContents.FromPath(Path.Combine(WorkloadInstallType.GetInstallStateFolder(_currentSdkFeatureBand, UserLocalPath), "default.json")).ShouldUseWorkloadSets();
            var configurationMessage = useWorkloadSets
                ? CliCommandStrings.WorkloadManifestInstallationConfigurationWorkloadSets
                : CliCommandStrings.WorkloadManifestInstallationConfigurationLooseManifests;
            reporter.WriteLine(indent + configurationMessage);

            if (!versionInfo.IsInstalled)
            {
                reporter.WriteLine(indent + string.Format(CliCommandStrings.WorkloadSetFromGlobalJsonNotInstalled, versionInfo.Version, versionInfo.GlobalJsonPath));
            }
            else if (versionInfo.WorkloadSetsEnabledWithoutWorkloadSet)
            {
                reporter.WriteLine(indent + CliCommandStrings.ShouldInstallAWorkloadSet);
            }
        }

        if (showVersion)
        {
            reporter.WriteLine($" Workload version: {GetWorkloadsVersion()}");

            WriteUpdateModeAndAnyError(indent: " ");
            reporter.WriteLine();
        }

        if (versionInfo.IsInstalled)
        {
            var installedList = InstalledSdkWorkloadIds;
            var installedWorkloads = AddInstalledVsWorkloads(installedList);
            var dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);

            if (installedWorkloads.Count == 0)
            {
                reporter.WriteLine(CliCommandStrings.NoWorkloadsInstalledInfoWarning);
            }
            else
            {
                var manifestInfoDict = WorkloadResolver.GetInstalledManifests().ToDictionary(info => info.Id, StringComparer.OrdinalIgnoreCase);

                foreach (var workload in installedWorkloads.AsEnumerable())
                {
                    var workloadManifest = WorkloadResolver.GetManifestFromWorkload(new WorkloadId(workload.Key));
                    var workloadFeatureBand = manifestInfoDict[workloadManifest.Id].ManifestFeatureBand;

                    const int align = 10;
                    const string separator = "   ";

                    reporter.WriteLine($" [{workload.Key}]");

                    reporter.Write($"{separator}{CliCommandStrings.WorkloadSourceColumn}:");
                    reporter.WriteLine($" {workload.Value,align}");

                    reporter.Write($"{separator}{CliCommandStrings.WorkloadManifestVersionColumn}:");
                    reporter.WriteLine($"    {workloadManifest.Version + '/' + workloadFeatureBand,align}");

                    reporter.Write($"{separator}{CliCommandStrings.WorkloadManifestPathColumn}:");
                    reporter.WriteLine($"       {workloadManifest.ManifestPath,align}");

                    reporter.Write($"{separator}{CliCommandStrings.WorkloadInstallTypeColumn}:");
                    reporter.WriteLine($"       {WorkloadInstallType.GetWorkloadInstallType(new SdkFeatureBand(Product.Version), dotnetPath).ToString(),align}"
                    );
                    reporter.WriteLine("");
                }
            }
        }

        if (!showVersion)
        {
            WriteUpdateModeAndAnyError();
        }
    }
}