File: MSBuild\MSBuildProjectLoader.cs
Web Access
Project: src\src\Workspaces\Core\MSBuild\Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj (Microsoft.CodeAnalysis.Workspaces.MSBuild)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Immutable;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Microsoft.CodeAnalysis.Host;
using Roslyn.Utilities;
using MSB = Microsoft.Build;
 
namespace Microsoft.CodeAnalysis.MSBuild
{
    /// <summary>
    /// An API for loading MSBuild project files.
    /// </summary>
    public partial class MSBuildProjectLoader
    {
        // the services for the projects and solutions are intended to be loaded into.
        private readonly SolutionServices _solutionServices;
 
        private readonly DiagnosticReporter _diagnosticReporter;
        private readonly Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory;
        private readonly PathResolver _pathResolver;
        private readonly ProjectFileExtensionRegistry _projectFileExtensionRegistry;
 
        // used to protect access to the following mutable state
        private readonly NonReentrantLock _dataGuard = new();
 
        internal MSBuildProjectLoader(
            SolutionServices solutionServices,
            DiagnosticReporter diagnosticReporter,
            Microsoft.Extensions.Logging.ILoggerFactory loggerFactory,
            ProjectFileExtensionRegistry projectFileExtensionRegistry,
            ImmutableDictionary<string, string>? properties)
        {
            _solutionServices = solutionServices;
            _diagnosticReporter = diagnosticReporter;
            _loggerFactory = loggerFactory;
            _pathResolver = new PathResolver(_diagnosticReporter);
            _projectFileExtensionRegistry = projectFileExtensionRegistry;
 
            Properties = ImmutableDictionary.Create<string, string>(StringComparer.OrdinalIgnoreCase);
 
            if (properties != null)
            {
                Properties = Properties.AddRange(properties);
            }
        }
 
        /// <summary>
        /// Create a new instance of an <see cref="MSBuildProjectLoader"/>.
        /// </summary>
        /// <param name="workspace">The workspace whose services this <see cref="MSBuildProjectLoader"/> should use.</param>
        /// <param name="properties">An optional dictionary of additional MSBuild properties and values to use when loading projects.
        /// These are the same properties that are passed to MSBuild via the /property:&lt;n&gt;=&lt;v&gt; command line argument.</param>
        public MSBuildProjectLoader(Workspace workspace, ImmutableDictionary<string, string>? properties = null)
        {
            _solutionServices = workspace.Services.SolutionServices;
            _diagnosticReporter = new DiagnosticReporter(workspace);
            _loggerFactory = DiagnosticReporterLoggerProvider.CreateLoggerFactoryForDiagnosticReporter(_diagnosticReporter);
            _pathResolver = new PathResolver(_diagnosticReporter);
            _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(_solutionServices, _diagnosticReporter);
 
            Properties = ImmutableDictionary.Create<string, string>(StringComparer.OrdinalIgnoreCase);
 
            if (properties != null)
            {
                Properties = Properties.AddRange(properties);
            }
        }
 
        /// <summary>
        /// The MSBuild properties used when interpreting project files.
        /// These are the same properties that are passed to MSBuild via the /property:&lt;n&gt;=&lt;v&gt; command line argument.
        /// </summary>
        public ImmutableDictionary<string, string> Properties { get; private set; }
 
        /// <summary>
        /// Determines if metadata from existing output assemblies is loaded instead of opening referenced projects.
        /// If the referenced project is already opened, the metadata will not be loaded.
        /// If the metadata assembly cannot be found the referenced project will be opened instead.
        /// </summary>
        public bool LoadMetadataForReferencedProjects { get; set; } = false;
 
        /// <summary>
        /// Determines if unrecognized projects are skipped when solutions or projects are opened.
        ///
        /// A project is unrecognized if it either has
        ///   a) an invalid file path,
        ///   b) a non-existent project file,
        ///   c) has an unrecognized file extension or
        ///   d) a file extension associated with an unsupported language.
        ///
        /// If unrecognized projects cannot be skipped a corresponding exception is thrown.
        /// </summary>
        public bool SkipUnrecognizedProjects { get; set; } = true;
 
        /// <summary>
        /// Associates a project file extension with a language name.
        /// </summary>
        /// <param name="projectFileExtension">The project file extension to associate with <paramref name="language"/>.</param>
        /// <param name="language">The language to associate with <paramref name="projectFileExtension"/>. This value
        /// should typically be taken from <see cref="LanguageNames"/>.</param>
        public void AssociateFileExtensionWithLanguage(string projectFileExtension, string language)
        {
            if (projectFileExtension == null)
            {
                throw new ArgumentNullException(nameof(projectFileExtension));
            }
 
            if (language == null)
            {
                throw new ArgumentNullException(nameof(language));
            }
 
            _projectFileExtensionRegistry.AssociateFileExtensionWithLanguage(projectFileExtension, language);
        }
 
        private void SetSolutionProperties(string? solutionFilePath)
        {
            const string SolutionDirProperty = "SolutionDir";
 
            // When MSBuild is building an individual project, it doesn't define $(SolutionDir).
            // However when building an .sln file, or when working inside Visual Studio,
            // $(SolutionDir) is defined to be the directory where the .sln file is located.
            // Some projects out there rely on $(SolutionDir) being set (although the best practice is to
            // use MSBuildProjectDirectory which is always defined).
            if (!RoslynString.IsNullOrEmpty(solutionFilePath))
            {
                var solutionDirectory = PathUtilities.GetDirectoryName(solutionFilePath) + PathUtilities.DirectorySeparatorChar;
 
                if (Directory.Exists(solutionDirectory))
                {
                    Properties = Properties.SetItem(SolutionDirProperty, solutionDirectory);
                }
            }
        }
 
        private DiagnosticReportingMode GetReportingModeForUnrecognizedProjects()
            => this.SkipUnrecognizedProjects
                ? DiagnosticReportingMode.Log
                : DiagnosticReportingMode.Throw;
 
        /// <summary>
        /// Loads the <see cref="SolutionInfo"/> for the specified solution file, including all projects referenced by the solution file and
        /// all the projects referenced by the project files.
        /// </summary>
        /// <param name="solutionFilePath">The path to the solution file to be loaded. This may be an absolute path or a path relative to the
        /// current working directory.</param>
        /// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the solution is loaded.</param>
        /// <param name="msbuildLogger">An optional <see cref="ILogger"/> that will log MSBuild results.</param>
        /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
        public async Task<SolutionInfo> LoadSolutionInfoAsync(
            string solutionFilePath,
            IProgress<ProjectLoadProgress>? progress = null,
#pragma warning disable IDE0060 // TODO: decide what to do with this unusued ILogger, since we can't reliabily use it if we're sending builds out of proc
            ILogger? msbuildLogger = null,
#pragma warning restore IDE0060
            CancellationToken cancellationToken = default)
        {
            if (solutionFilePath == null)
            {
                throw new ArgumentNullException(nameof(solutionFilePath));
            }
 
            if (!_pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
            {
                // TryGetAbsoluteSolutionPath should throw before we get here.
                return null!;
            }
 
            var projectFilter = ImmutableHashSet<string>.Empty;
            if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
                !SolutionFilterReader.TryRead(absoluteSolutionPath, _pathResolver, out absoluteSolutionPath, out projectFilter))
            {
                throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
            }
 
            using (_dataGuard.DisposableWait(cancellationToken))
            {
                this.SetSolutionProperties(absoluteSolutionPath);
            }
 
            var solutionFile = MSB.Construction.SolutionFile.Parse(absoluteSolutionPath);
            var reportingMode = GetReportingModeForUnrecognizedProjects();
 
            var reportingOptions = new DiagnosticReportingOptions(
                onPathFailure: reportingMode,
                onLoaderFailure: reportingMode);
 
            var projectPaths = ImmutableArray.CreateBuilder<string>();
 
            // load all the projects
            foreach (var project in solutionFile.ProjectsInOrder)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                if (project.ProjectType == MSB.Construction.SolutionProjectType.SolutionFolder)
                {
                    continue;
                }
 
                // Load project if we have an empty project filter and the project path is present.
                if (projectFilter.IsEmpty ||
                    projectFilter.Contains(project.AbsolutePath))
                {
                    projectPaths.Add(project.RelativePath);
                }
            }
 
            var buildHostProcessManager = new BuildHostProcessManager(Properties, loggerFactory: _loggerFactory);
            await using var _ = buildHostProcessManager.ConfigureAwait(false);
 
            var worker = new Worker(
                _solutionServices,
                _diagnosticReporter,
                _pathResolver,
                _projectFileExtensionRegistry,
                buildHostProcessManager,
                projectPaths.ToImmutable(),
                // TryGetAbsoluteSolutionPath should not return an invalid path
                baseDirectory: Path.GetDirectoryName(absoluteSolutionPath)!,
                Properties,
                projectMap: null,
                progress,
                requestedProjectOptions: reportingOptions,
                discoveredProjectOptions: reportingOptions,
                preferMetadataForReferencesOfDiscoveredProjects: false);
 
            var projects = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
 
            // construct workspace from loaded project infos
            return SolutionInfo.Create(
                SolutionId.CreateNewId(debugName: absoluteSolutionPath),
                version: default,
                absoluteSolutionPath,
                projects);
        }
 
        /// <summary>
        /// Loads the <see cref="ProjectInfo"/> from the specified project file and all referenced projects.
        /// The first <see cref="ProjectInfo"/> in the result corresponds to the specified project file.
        /// </summary>
        /// <param name="projectFilePath">The path to the project file to be loaded. This may be an absolute path or a path relative to the
        /// current working directory.</param>
        /// <param name="projectMap">An optional <see cref="ProjectMap"/> that will be used to resolve project references to existing projects.
        /// This is useful when populating a custom <see cref="Workspace"/>.</param>
        /// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the project is loaded.</param>
        /// <param name="msbuildLogger">An optional <see cref="ILogger"/> that will log msbuild results.</param>
        /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
        public async Task<ImmutableArray<ProjectInfo>> LoadProjectInfoAsync(
            string projectFilePath,
            ProjectMap? projectMap = null,
            IProgress<ProjectLoadProgress>? progress = null,
#pragma warning disable IDE0060 // TODO: decide what to do with this unusued ILogger, since we can't reliabily use it if we're sending builds out of proc
            ILogger? msbuildLogger = null,
#pragma warning restore IDE0060
            CancellationToken cancellationToken = default)
        {
            if (projectFilePath == null)
            {
                throw new ArgumentNullException(nameof(projectFilePath));
            }
 
            var requestedProjectOptions = DiagnosticReportingOptions.ThrowForAll;
 
            var reportingMode = GetReportingModeForUnrecognizedProjects();
 
            var discoveredProjectOptions = new DiagnosticReportingOptions(
                onPathFailure: reportingMode,
                onLoaderFailure: reportingMode);
 
            var buildHostProcessManager = new BuildHostProcessManager(Properties, loggerFactory: _loggerFactory);
            await using var _ = buildHostProcessManager.ConfigureAwait(false);
 
            var worker = new Worker(
                _solutionServices,
                _diagnosticReporter,
                _pathResolver,
                _projectFileExtensionRegistry,
                buildHostProcessManager,
                requestedProjectPaths: [projectFilePath],
                baseDirectory: Directory.GetCurrentDirectory(),
                globalProperties: Properties,
                projectMap,
                progress,
                requestedProjectOptions,
                discoveredProjectOptions,
                this.LoadMetadataForReferencedProjects);
 
            return await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
        }
    }
}