|
// 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.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.MSBuild.Build;
using Microsoft.CodeAnalysis.MSBuild.Logging;
using Roslyn.Utilities;
using MSB = Microsoft.Build;
namespace Microsoft.CodeAnalysis.MSBuild
{
internal abstract class ProjectFile : IProjectFile
{
private readonly ProjectFileLoader _loader;
private readonly MSB.Evaluation.Project? _loadedProject;
private readonly ProjectBuildManager _buildManager;
private readonly string _projectDirectory;
public DiagnosticLog Log { get; }
public virtual string FilePath => _loadedProject?.FullPath ?? string.Empty;
public string Language => _loader.Language;
protected ProjectFile(ProjectFileLoader loader, MSB.Evaluation.Project? loadedProject, ProjectBuildManager buildManager, DiagnosticLog log)
{
_loader = loader;
_loadedProject = loadedProject;
_buildManager = buildManager;
var directory = loadedProject?.DirectoryPath ?? string.Empty;
_projectDirectory = PathUtilities.EnsureTrailingSeparator(directory);
Log = log;
}
protected abstract SourceCodeKind GetSourceCodeKind(string documentFileName);
public abstract string GetDocumentExtension(SourceCodeKind kind);
protected abstract IEnumerable<MSB.Framework.ITaskItem> GetCompilerCommandLineArgs(MSB.Execution.ProjectInstance executedProject);
protected abstract ImmutableArray<string> ReadCommandLineArgs(MSB.Execution.ProjectInstance project);
public async Task<ImmutableArray<ProjectFileInfo>> GetProjectFileInfosAsync(CancellationToken cancellationToken)
{
if (_loadedProject is null)
{
return ImmutableArray.Create(ProjectFileInfo.CreateEmpty(Language, _loadedProject?.FullPath, Log));
}
var targetFrameworkValue = _loadedProject.GetPropertyValue(PropertyNames.TargetFramework);
var targetFrameworksValue = _loadedProject.GetPropertyValue(PropertyNames.TargetFrameworks);
if (RoslynString.IsNullOrEmpty(targetFrameworkValue) && !RoslynString.IsNullOrEmpty(targetFrameworksValue))
{
// This project has a <TargetFrameworks> property, but does not specify a <TargetFramework>.
// In this case, we need to iterate through the <TargetFrameworks>, set <TargetFramework> with
// each value, and build the project.
var targetFrameworks = targetFrameworksValue.Split(';');
var results = ImmutableArray.CreateBuilder<ProjectFileInfo>(targetFrameworks.Length);
if (!_loadedProject.GlobalProperties.TryGetValue(PropertyNames.TargetFramework, out var initialGlobalTargetFrameworkValue))
initialGlobalTargetFrameworkValue = null;
foreach (var targetFramework in targetFrameworks)
{
_loadedProject.SetGlobalProperty(PropertyNames.TargetFramework, targetFramework);
_loadedProject.ReevaluateIfNecessary();
var projectFileInfo = await BuildProjectFileInfoAsync(cancellationToken).ConfigureAwait(false);
results.Add(projectFileInfo);
}
if (initialGlobalTargetFrameworkValue is null)
{
_loadedProject.RemoveGlobalProperty(PropertyNames.TargetFramework);
}
else
{
_loadedProject.SetGlobalProperty(PropertyNames.TargetFramework, initialGlobalTargetFrameworkValue);
}
_loadedProject.ReevaluateIfNecessary();
return results.ToImmutable();
}
else
{
var projectFileInfo = await BuildProjectFileInfoAsync(cancellationToken).ConfigureAwait(false);
projectFileInfo ??= ProjectFileInfo.CreateEmpty(Language, _loadedProject?.FullPath, Log);
return ImmutableArray.Create(projectFileInfo);
}
}
private async Task<ProjectFileInfo> BuildProjectFileInfoAsync(CancellationToken cancellationToken)
{
if (_loadedProject is null)
{
return ProjectFileInfo.CreateEmpty(Language, _loadedProject?.FullPath, Log);
}
var project = await _buildManager.BuildProjectAsync(_loadedProject, Log, cancellationToken).ConfigureAwait(false);
return project != null
? CreateProjectFileInfo(project)
: ProjectFileInfo.CreateEmpty(Language, _loadedProject.FullPath, Log);
}
private ProjectFileInfo CreateProjectFileInfo(MSB.Execution.ProjectInstance project)
{
var commandLineArgs = GetCommandLineArgs(project);
var outputFilePath = project.ReadPropertyString(PropertyNames.TargetPath);
if (!RoslynString.IsNullOrWhiteSpace(outputFilePath))
{
outputFilePath = GetAbsolutePathRelativeToProject(outputFilePath);
}
var outputRefFilePath = project.ReadPropertyString(PropertyNames.TargetRefPath);
if (!RoslynString.IsNullOrWhiteSpace(outputRefFilePath))
{
outputRefFilePath = GetAbsolutePathRelativeToProject(outputRefFilePath);
}
var intermediateOutputFilePath = project.GetItems(ItemNames.IntermediateAssembly).FirstOrDefault()?.EvaluatedInclude;
if (!RoslynString.IsNullOrWhiteSpace(intermediateOutputFilePath))
{
intermediateOutputFilePath = GetAbsolutePathRelativeToProject(intermediateOutputFilePath);
}
// Right now VB doesn't have the concept of "default namespace". But we conjure one in workspace
// by assigning the value of the project's root namespace to it. So various feature can choose to
// use it for their own purpose.
// In the future, we might consider officially exposing "default namespace" for VB project
// (e.g. through a <defaultnamespace> msbuild property)
var defaultNamespace = project.ReadPropertyString(PropertyNames.RootNamespace) ?? string.Empty;
var targetFramework = project.ReadPropertyString(PropertyNames.TargetFramework);
if (RoslynString.IsNullOrWhiteSpace(targetFramework))
{
targetFramework = null;
}
var docs = project.GetDocuments()
.Where(IsNotTemporaryGeneratedFile)
.Select(MakeDocumentFileInfo)
.ToImmutableArray();
var additionalDocs = project.GetAdditionalFiles()
.Select(MakeNonSourceFileDocumentFileInfo)
.ToImmutableArray();
var analyzerConfigDocs = project.GetEditorConfigFiles()
.Select(MakeNonSourceFileDocumentFileInfo)
.ToImmutableArray();
return ProjectFileInfo.Create(
Language,
project.FullPath,
outputFilePath,
outputRefFilePath,
intermediateOutputFilePath,
defaultNamespace,
targetFramework,
commandLineArgs,
docs,
additionalDocs,
analyzerConfigDocs,
project.GetProjectReferences().ToImmutableArray(),
Log);
}
private ImmutableArray<string> GetCommandLineArgs(MSB.Execution.ProjectInstance project)
{
var commandLineArgs = GetCompilerCommandLineArgs(project)
.Select(item => item.ItemSpec)
.ToImmutableArray();
if (commandLineArgs.Length == 0)
{
// We didn't get any command-line args, which likely means that the build
// was not successful. In that case, try to read the command-line args from
// the ProjectInstance that we have. This is a best effort to provide something
// meaningful for the user, though it will likely be incomplete.
commandLineArgs = ReadCommandLineArgs(project);
}
return commandLineArgs;
}
protected static bool IsNotTemporaryGeneratedFile(MSB.Framework.ITaskItem item)
=> !Path.GetFileName(item.ItemSpec).StartsWith("TemporaryGeneratedFile_", StringComparison.Ordinal);
private DocumentFileInfo MakeDocumentFileInfo(MSB.Framework.ITaskItem documentItem)
{
var filePath = GetDocumentFilePath(documentItem);
var logicalPath = GetDocumentLogicalPath(documentItem, _projectDirectory);
var isLinked = IsDocumentLinked(documentItem);
var isGenerated = IsDocumentGenerated(documentItem);
var sourceCodeKind = GetSourceCodeKind(filePath);
return new DocumentFileInfo(filePath, logicalPath, isLinked, isGenerated, sourceCodeKind);
}
private DocumentFileInfo MakeNonSourceFileDocumentFileInfo(MSB.Framework.ITaskItem documentItem)
{
var filePath = GetDocumentFilePath(documentItem);
var logicalPath = GetDocumentLogicalPath(documentItem, _projectDirectory);
var isLinked = IsDocumentLinked(documentItem);
var isGenerated = IsDocumentGenerated(documentItem);
return new DocumentFileInfo(filePath, logicalPath, isLinked, isGenerated, SourceCodeKind.Regular);
}
/// <summary>
/// Resolves the given path that is possibly relative to the project directory.
/// </summary>
/// <remarks>
/// The resulting path is absolute but might not be normalized.
/// </remarks>
private string GetAbsolutePathRelativeToProject(string path)
{
// TODO (tomat): should we report an error when drive-relative path (e.g. "C:goo.cs") is encountered?
var absolutePath = FileUtilities.ResolveRelativePath(path, _projectDirectory) ?? path;
return FileUtilities.TryNormalizeAbsolutePath(absolutePath) ?? absolutePath;
}
private string GetDocumentFilePath(MSB.Framework.ITaskItem documentItem)
=> GetAbsolutePathRelativeToProject(documentItem.ItemSpec);
private static bool IsDocumentLinked(MSB.Framework.ITaskItem documentItem)
=> !RoslynString.IsNullOrEmpty(documentItem.GetMetadata(MetadataNames.Link));
private IDictionary<string, MSB.Evaluation.ProjectItem>? _documents;
protected bool IsDocumentGenerated(MSB.Framework.ITaskItem documentItem)
{
if (_documents == null)
{
_documents = new Dictionary<string, MSB.Evaluation.ProjectItem>();
if (_loadedProject is null)
{
return false;
}
foreach (var item in _loadedProject.GetItems(ItemNames.Compile))
{
_documents[GetAbsolutePathRelativeToProject(item.EvaluatedInclude)] = item;
}
}
return !_documents.ContainsKey(GetAbsolutePathRelativeToProject(documentItem.ItemSpec));
}
protected static string GetDocumentLogicalPath(MSB.Framework.ITaskItem documentItem, string projectDirectory)
{
var link = documentItem.GetMetadata(MetadataNames.Link);
if (!RoslynString.IsNullOrEmpty(link))
{
// if a specific link is specified in the project file then use it to form the logical path.
return link;
}
else
{
var filePath = documentItem.ItemSpec;
if (!PathUtilities.IsAbsolute(filePath))
{
return filePath;
}
var normalizedPath = FileUtilities.TryNormalizeAbsolutePath(filePath);
if (normalizedPath == null)
{
return filePath;
}
// If the document is within the current project directory (or subdirectory), then the logical path is the relative path
// from the project's directory.
if (normalizedPath.StartsWith(projectDirectory, StringComparison.OrdinalIgnoreCase))
{
return normalizedPath[projectDirectory.Length..];
}
else
{
// if the document lies outside the project's directory (or subdirectory) then place it logically at the root of the project.
// if more than one document ends up with the same logical name then so be it (the workspace will survive.)
return PathUtilities.GetFileName(normalizedPath);
}
}
}
public void AddDocument(string filePath, string? logicalPath = null)
{
if (_loadedProject is null)
{
return;
}
var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, filePath);
Dictionary<string, string>? metadata = null;
if (logicalPath != null && relativePath != logicalPath)
{
metadata = new Dictionary<string, string>
{
{ MetadataNames.Link, logicalPath }
};
relativePath = filePath; // link to full path
}
_loadedProject.AddItem(ItemNames.Compile, relativePath, metadata);
}
public void RemoveDocument(string filePath)
{
if (_loadedProject is null)
{
return;
}
var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, filePath);
var items = _loadedProject.GetItems(ItemNames.Compile);
var item = items.FirstOrDefault(it => PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
|| PathUtilities.PathsEqual(it.EvaluatedInclude, filePath));
if (item != null)
{
_loadedProject.RemoveItem(item);
}
}
public void AddMetadataReference(MetadataReference reference, AssemblyIdentity identity)
{
if (_loadedProject is null)
{
return;
}
if (reference is PortableExecutableReference peRef && peRef.FilePath != null)
{
var metadata = new Dictionary<string, string>();
if (!peRef.Properties.Aliases.IsEmpty)
{
metadata.Add(MetadataNames.Aliases, string.Join(",", peRef.Properties.Aliases));
}
if (IsInGAC(peRef.FilePath) && identity != null)
{
// Since the location of the reference is in GAC, need to use full identity name to find it again.
// This typically happens when you base the reference off of a reflection assembly location.
_loadedProject.AddItem(ItemNames.Reference, identity.GetDisplayName(), metadata);
}
else if (IsFrameworkReferenceAssembly(peRef.FilePath))
{
// just use short name since this will be resolved by msbuild relative to the known framework reference assemblies.
var fileName = identity != null ? identity.Name : Path.GetFileNameWithoutExtension(peRef.FilePath);
_loadedProject.AddItem(ItemNames.Reference, fileName, metadata);
}
else // other location -- need hint to find correct assembly
{
var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, peRef.FilePath);
var fileName = Path.GetFileNameWithoutExtension(peRef.FilePath);
metadata.Add(MetadataNames.HintPath, relativePath);
_loadedProject.AddItem(ItemNames.Reference, fileName, metadata);
}
}
}
private static bool IsInGAC(string filePath)
{
return GlobalAssemblyCacheLocation.RootLocations.Any(static (gloc, filePath) => PathUtilities.IsChildPath(gloc, filePath), filePath);
}
private static string? s_frameworkRoot;
private static string FrameworkRoot
{
get
{
if (RoslynString.IsNullOrEmpty(s_frameworkRoot))
{
var runtimeDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();
s_frameworkRoot = Path.GetDirectoryName(runtimeDir); // back out one directory level to be root path of all framework versions
}
return s_frameworkRoot ?? throw new InvalidOperationException($"Unable to get {nameof(FrameworkRoot)}");
}
}
private static bool IsFrameworkReferenceAssembly(string filePath)
{
return PathUtilities.IsChildPath(FrameworkRoot, filePath);
}
public void RemoveMetadataReference(MetadataReference reference, AssemblyIdentity identity)
{
if (_loadedProject is null)
{
return;
}
if (reference is PortableExecutableReference peRef && peRef.FilePath != null)
{
var item = FindReferenceItem(identity, peRef.FilePath);
if (item != null)
{
_loadedProject.RemoveItem(item);
}
}
}
private MSB.Evaluation.ProjectItem FindReferenceItem(AssemblyIdentity identity, string filePath)
{
if (_loadedProject is null)
{
throw new InvalidOperationException($"Unable to find reference item '{identity?.Name}'");
}
var references = _loadedProject.GetItems(ItemNames.Reference);
MSB.Evaluation.ProjectItem? item = null;
var fileName = Path.GetFileNameWithoutExtension(filePath);
if (identity != null)
{
var shortAssemblyName = identity.Name;
var fullAssemblyName = identity.GetDisplayName();
// check for short name match
item = references.FirstOrDefault(it => string.Compare(it.EvaluatedInclude, shortAssemblyName, StringComparison.OrdinalIgnoreCase) == 0);
// check for full name match
item ??= references.FirstOrDefault(it => string.Compare(it.EvaluatedInclude, fullAssemblyName, StringComparison.OrdinalIgnoreCase) == 0);
}
// check for file path match
if (item == null)
{
var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, filePath);
item = references.FirstOrDefault(it => PathUtilities.PathsEqual(it.EvaluatedInclude, filePath)
|| PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
|| PathUtilities.PathsEqual(GetHintPath(it), filePath)
|| PathUtilities.PathsEqual(GetHintPath(it), relativePath));
}
// check for partial name match
if (item == null && identity != null)
{
var partialName = identity.Name + ",";
var items = references.Where(it => it.EvaluatedInclude.StartsWith(partialName, StringComparison.OrdinalIgnoreCase)).ToList();
if (items.Count == 1)
{
item = items[0];
}
}
return item ?? throw new InvalidOperationException($"Unable to find reference item '{identity?.Name}'");
}
private static string GetHintPath(MSB.Evaluation.ProjectItem item)
=> item.Metadata.FirstOrDefault(m => string.Equals(m.Name, MetadataNames.HintPath, StringComparison.OrdinalIgnoreCase))?.EvaluatedValue ?? string.Empty;
public void AddProjectReference(string projectName, ProjectFileReference reference)
{
if (_loadedProject is null)
{
return;
}
var metadata = new Dictionary<string, string>
{
{ MetadataNames.Name, projectName }
};
if (!reference.Aliases.IsEmpty)
{
metadata.Add(MetadataNames.Aliases, string.Join(",", reference.Aliases));
}
var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, reference.Path);
_loadedProject.AddItem(ItemNames.ProjectReference, relativePath, metadata);
}
public void RemoveProjectReference(string projectName, string projectFilePath)
{
if (_loadedProject is null)
{
return;
}
var item = FindProjectReferenceItem(projectName, projectFilePath);
if (item != null)
{
_loadedProject.RemoveItem(item);
}
}
private MSB.Evaluation.ProjectItem? FindProjectReferenceItem(string projectName, string projectFilePath)
{
if (_loadedProject is null)
{
return null;
}
var references = _loadedProject.GetItems(ItemNames.ProjectReference);
var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, projectFilePath);
MSB.Evaluation.ProjectItem? item = null;
// find by project file path
item = references.First(it => PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
|| PathUtilities.PathsEqual(it.EvaluatedInclude, projectFilePath));
// try to find by project name
item ??= references.First(it => string.Compare(projectName, it.GetMetadataValue(MetadataNames.Name), StringComparison.OrdinalIgnoreCase) == 0);
return item;
}
public void AddAnalyzerReference(AnalyzerReference reference)
{
if (_loadedProject is null)
{
return;
}
if (reference is AnalyzerFileReference fileRef)
{
var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, fileRef.FullPath);
_loadedProject.AddItem(ItemNames.Analyzer, relativePath);
}
}
public void RemoveAnalyzerReference(AnalyzerReference reference)
{
if (_loadedProject is null)
{
return;
}
if (reference is AnalyzerFileReference fileRef)
{
var relativePath = PathUtilities.GetRelativePath(_loadedProject.DirectoryPath, fileRef.FullPath);
var analyzers = _loadedProject.GetItems(ItemNames.Analyzer);
var item = analyzers.FirstOrDefault(it => PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
|| PathUtilities.PathsEqual(it.EvaluatedInclude, fileRef.FullPath));
if (item != null)
{
_loadedProject.RemoveItem(item);
}
}
}
public void Save()
{
if (_loadedProject is null)
{
return;
}
_loadedProject.Save();
}
}
}
|