File: Commands\New\BuiltInTemplatePackageProvider.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 Microsoft.DotNet.Cli.Utils;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.TemplatePackage;
using NuGet.Versioning;

namespace Microsoft.DotNet.Cli.Commands.New;

/// <summary>
/// Returns list of *.nupkg files from C:\Program Files\dotnet\templates\x.x.x.x\ (on Windows) to be installed.
/// </summary>
internal sealed class BuiltInTemplatePackageProvider(BuiltInTemplatePackageProviderFactory factory, IEngineEnvironmentSettings settings) : ITemplatePackageProvider
{
    private readonly IEngineEnvironmentSettings _environmentSettings = settings;

    public ITemplatePackageProviderFactory Factory { get; } = factory;

    /// <summary>
    /// We don't trigger this event, we could complicate our life with FileSystemWatcher.
    /// But since "dotnet new" is short lived process is not worth it, plus it would cause some perf hit...
    /// To avoid warnings about being unused, implement empty add/remove accessors.
    /// </summary>
    public event Action? TemplatePackagesChanged
    {
        add { }
        remove { }
    }

    public Task<IReadOnlyList<ITemplatePackage>> GetAllTemplatePackagesAsync(CancellationToken cancellationToken)
    {
        var packages = new List<ITemplatePackage>();
        foreach (string templateFolder in GetTemplateFolders(_environmentSettings))
        {
            foreach (string nupkgPath in Directory.EnumerateFiles(templateFolder, "*.nupkg", SearchOption.TopDirectoryOnly))
            {
                packages.Add(new TemplatePackage(this, nupkgPath, File.GetLastWriteTime(nupkgPath)));
            }
        }
        return Task.FromResult<IReadOnlyList<ITemplatePackage>>(packages);
    }

    private static IEnumerable<string> GetTemplateFolders(IEngineEnvironmentSettings environmentSettings)
    {
        var templateFoldersToInstall = new List<string>();

        var sdksDirectory = new DirectoryInfo(MSBuildForwardingAppWithoutLogging.GetMSBuildSDKsPath());
        var sdkDirectory = sdksDirectory.Parent;
        var sdkPath = sdkDirectory?.FullName ?? string.Empty;
        var dotnetRootPath = sdkDirectory?.Parent?.Parent?.FullName ?? string.Empty;
        // First grab templates from dotnet\templates\M.m folders, in ascending order, up to our version
        string templatesRootFolder = Path.Combine(dotnetRootPath, "templates");
        if (Directory.Exists(templatesRootFolder))
        {
            IReadOnlyDictionary<string, SemanticVersion> parsedNames = GetVersionDirectoriesInDirectory(templatesRootFolder);
            IList<string> versionedFolders = GetBestVersionsByMajorMinor(parsedNames);

            templateFoldersToInstall.AddRange(versionedFolders
                .Select(versionedFolder => Path.Combine(templatesRootFolder, versionedFolder)));
        }

        // Now grab templates from our base folder, if present.
        string templatesDir = Path.Combine(sdkPath, "Templates");
        if (Directory.Exists(templatesDir))
        {
            templateFoldersToInstall.Add(templatesDir);
        }

        return templateFoldersToInstall;
    }

    // Returns a dictionary of fileName -> Parsed version info
    // including all the directories in the input directory whose names are parse-able as versions.
    private static IReadOnlyDictionary<string, SemanticVersion> GetVersionDirectoriesInDirectory(string fullPath)
    {
        var versionFileInfo = new Dictionary<string, SemanticVersion>();

        foreach (string directory in Directory.EnumerateDirectories(fullPath, "*.*", SearchOption.TopDirectoryOnly))
        {
            if (SemanticVersion.TryParse(Path.GetFileName(directory), out SemanticVersion? versionInfo) && versionInfo is not null)
            {
                versionFileInfo.Add(directory, versionInfo);
            }
        }

        return versionFileInfo;
    }

    internal static IList<string> GetBestVersionsByMajorMinor(IReadOnlyDictionary<string, SemanticVersion> versionDirInfo)
    {
        IDictionary<string, (string path, SemanticVersion version)> bestVersionsByBucket = new Dictionary<string, (string path, SemanticVersion version)>();

        Version? sdkVersion = typeof(NewCommandParser).Assembly.GetName().Version;
        foreach (KeyValuePair<string, SemanticVersion> dirInfo in versionDirInfo)
        {
            var majorMinorDirVersion = new Version(dirInfo.Value.Major, dirInfo.Value.Minor);
            // restrict the results to not include from higher versions of the runtime/templates then the SDK
            if (majorMinorDirVersion <= sdkVersion)
            {
                string coreAppVersion = $"{dirInfo.Value.Major}.{dirInfo.Value.Minor}";
                if (!bestVersionsByBucket.TryGetValue(coreAppVersion, out (string path, SemanticVersion version) currentHighest)
                    || dirInfo.Value.CompareTo(currentHighest.version) > 0)
                {
                    bestVersionsByBucket[coreAppVersion] = (dirInfo.Key, dirInfo.Value);
                }
            }
        }

        return [.. bestVersionsByBucket.OrderBy(x => x.Value.version).Select(x => x.Value.path)];
    }
}