File: Commands\Workload\WorkloadInstallDetector.cs
Web Access
Project: 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 Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer;
using Microsoft.NET.Sdk.WorkloadManifestReader;

namespace Microsoft.DotNet.Cli.Commands.Workload;

/// <summary>
///  Read-only, dependency-light detection of whether any workloads are installed for the running
///  SDK's feature band.
///
///  <para>
///  <see cref="WorkloadIntegrityChecker.RunFirstUseCheck"/> only performs an (expensive) repair when
///  installed workloads exist; the resolver/installer plumbing it uses to discover that fact drags in
///  the NuGet engine (and, on Windows, the MSI installer IPC). This type answers the same "are any
///  workloads installed?" question by reading only the filesystem and registry, so it has no NuGet,
///  MSBuild, cryptography, or installer-IPC dependencies and can run in-process under NativeAOT. The
///  repair itself stays on the managed CLI.
///  </para>
/// </summary>
internal static class WorkloadInstallDetector
{
    /// <summary>
    ///  Returns <see langword="true"/> if at least one workload is recorded as installed for the
    ///  current SDK feature band. File-based installs are read from the workload metadata directory;
    ///  MSI-based installs (Windows) are read from the machine registry.
    /// </summary>
    /// <param name="dotnetDir">
    ///  The dotnet root to inspect. Defaults to the directory of the running process.
    /// </param>
    public static bool HasInstalledWorkloadsForCurrentBand(string? dotnetDir = null)
    {
        dotnetDir = string.IsNullOrWhiteSpace(dotnetDir) ? Path.GetDirectoryName(Environment.ProcessPath) : dotnetDir;
        var sdkFeatureBand = new SdkFeatureBand(Product.Version);

        return WorkloadInstallType.GetWorkloadInstallType(sdkFeatureBand, dotnetDir) switch
        {
            InstallType.Msi => HasMsiWorkloadRecords(sdkFeatureBand),
            _ => HasFileBasedWorkloadRecords(dotnetDir, sdkFeatureBand),
        };
    }

    private static bool HasFileBasedWorkloadRecords(string? dotnetDir, SdkFeatureBand sdkFeatureBand)
    {
        // Records live under {workloadRoot}/metadata/workloads, where workloadRoot is the user profile
        // for user-local installs and the dotnet root otherwise. This mirrors the layout used by
        // FileBasedInstaller / FileBasedInstallationRecordRepository.
        var workloadRootDir = IsUserLocal(dotnetDir, sdkFeatureBand)
            ? CliFolderPathCalculator.DotnetUserProfileFolderPath
            : dotnetDir;
        if (workloadRootDir is null)
        {
            return false;
        }

        var metadataDir = Path.Combine(workloadRootDir, "metadata", "workloads");
        return new FileBasedInstallationRecordRepository(metadataDir)
            .GetInstalledWorkloads(sdkFeatureBand)
            .Any();
    }

    // Equivalent to WorkloadFileBasedInstall.IsUserLocal, inlined here to avoid pulling that type's
    // workload-history (System.Text.Json) helpers into the NativeAOT build.
    private static bool IsUserLocal(string? dotnetDir, SdkFeatureBand sdkFeatureBand)
        => dotnetDir is not null
           && File.Exists(Path.Combine(dotnetDir, "metadata", "workloads", sdkFeatureBand.ToString(), "userlocal"));

    private static bool HasMsiWorkloadRecords(SdkFeatureBand sdkFeatureBand)
    {
        if (!OperatingSystem.IsWindows())
        {
            return false;
        }

#if CLI_AOT
        return new RegistryWorkloadInstallationRecordRepository()
            .GetInstalledWorkloads(sdkFeatureBand)
            .Any();
#else
        // This detector is only exercised on the NativeAOT first-run path; the managed build uses
        // WorkloadIntegrityChecker instead. The read-only registry repository constructor used above
        // only exists under CLI_AOT, so there is nothing to do here in the managed build.
        return false;
#endif
    }
}