|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Security;
using System.Text;
using System.Xml;
#if !NETFRAMEWORK
using Microsoft.Build.Shared;
#endif
using XMakeAttributes = Microsoft.Build.Shared.XMakeAttributes;
using ProjectFileErrorUtilities = Microsoft.Build.Shared.ProjectFileErrorUtilities;
using BuildEventFileInfo = Microsoft.Build.Shared.BuildEventFileInfo;
using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities;
using System.Collections.ObjectModel;
using System.Linq;
#nullable disable
namespace Microsoft.Build.Construction
{
/// <remarks>
/// An enumeration defining the different types of projects we might find in an SLN.
/// </remarks>
public enum SolutionProjectType
{
/// <summary>
/// Everything else besides the below well-known project types.
/// </summary>
Unknown,
/// <summary>
/// C#, VB, F#, and VJ# projects
/// </summary>
KnownToBeMSBuildFormat,
/// <summary>
/// Solution folders appear in the .sln file, but aren't buildable projects.
/// </summary>
SolutionFolder,
/// <summary>
/// ASP.NET projects
/// </summary>
WebProject,
/// <summary>
/// Web Deployment (.wdproj) projects
/// </summary>
WebDeploymentProject, // MSBuildFormat, but Whidbey-era ones specify ProjectReferences differently
/// <summary>
/// Project inside an Enterprise Template project
/// </summary>
EtpSubProject,
/// <summary>
/// A shared project represents a collection of shared files that is not buildable on its own.
/// </summary>
SharedProject
}
internal struct AspNetCompilerParameters
{
internal string aspNetVirtualPath; // For Venus projects only, Virtual path for web
internal string aspNetPhysicalPath; // For Venus projects only, Physical path for web
internal string aspNetTargetPath; // For Venus projects only, Target for output files
internal string aspNetForce; // For Venus projects only, Force overwrite of target
internal string aspNetUpdateable; // For Venus projects only, compiled web application is updateable
internal string aspNetDebug; // For Venus projects only, generate symbols, etc.
internal string aspNetKeyFile; // For Venus projects only, strong name key file.
internal string aspNetKeyContainer; // For Venus projects only, strong name key container.
internal string aspNetDelaySign; // For Venus projects only, delay sign strong name.
internal string aspNetAPTCA; // For Venus projects only, AllowPartiallyTrustedCallers.
internal string aspNetFixedNames; // For Venus projects only, generate fixed assembly names.
}
/// <remarks>
/// This class represents a project (or SLN folder) that is read in from a solution file.
/// </remarks>
public sealed class ProjectInSolution
{
#region Constants
/// <summary>
/// Characters that need to be cleansed from a project name.
/// </summary>
private static readonly char[] s_charsToCleanse = { '%', '$', '@', ';', '.', '(', ')', '\'' };
/// <summary>
/// Project names that need to be disambiguated when forming a target name
/// </summary>
internal static readonly string[] projectNamesToDisambiguate = { "Build", "Rebuild", "Clean", "Publish" };
/// <summary>
/// Character that will be used to replace 'unclean' ones.
/// </summary>
private const char cleanCharacter = '_';
#endregion
#region Member data
private string _relativePath; // Relative from .SLN file. For example, "WindowsApplication1\WindowsApplication1.csproj"
private string _absolutePath; // Absolute path to the project file
private readonly List<string> _dependencies; // A list of strings representing the Guids of the dependent projects.
private IReadOnlyList<string> _dependenciesAsReadonly;
private string _uniqueProjectName; // For example, "MySlnFolder\MySubSlnFolder\Windows_Application1"
private string _originalProjectName; // For example, "MySlnFolder\MySubSlnFolder\Windows.Application1"
/// <summary>
/// The project configuration in given solution configuration
/// K: full solution configuration name (cfg + platform)
/// V: project configuration
/// </summary>
private readonly Dictionary<string, ProjectConfigurationInSolution> _projectConfigurations;
private IReadOnlyDictionary<string, ProjectConfigurationInSolution> _projectConfigurationsReadOnly;
#endregion
#region Constructors
internal ProjectInSolution(SolutionFile solution)
{
ProjectType = SolutionProjectType.Unknown;
ProjectName = null;
_relativePath = null;
ProjectGuid = null;
_dependencies = new List<string>();
ParentProjectGuid = null;
_uniqueProjectName = null;
ParentSolution = solution;
// default to .NET Framework 3.5 if this is an old solution that doesn't explicitly say.
TargetFrameworkMoniker = ".NETFramework,Version=v3.5";
// This hashtable stores a AspNetCompilerParameters struct for each configuration name supported.
AspNetConfigurations = new Hashtable(StringComparer.OrdinalIgnoreCase);
_projectConfigurations = new Dictionary<string, ProjectConfigurationInSolution>(StringComparer.OrdinalIgnoreCase);
}
#endregion
#region Properties
/// <summary>
/// This project's name
/// </summary>
public string ProjectName { get; internal set; }
/// <summary>
/// The path to this project file, relative to the solution location
/// </summary>
public string RelativePath
{
get
{
return _relativePath;
}
internal set
{
#if NETFRAMEWORK
// Avoid loading System.Runtime.InteropServices.RuntimeInformation in full-framework
// cases. It caused https://github.com/NuGet/Home/issues/6918.
_relativePath = value;
#else
_relativePath = FileUtilities.MaybeAdjustFilePath(value, ParentSolution.SolutionFileDirectory);
#endif
}
}
/// <summary>
/// Returns the absolute path for this project
/// </summary>
public string AbsolutePath
{
get
{
if (_absolutePath == null)
{
_absolutePath = Path.Combine(ParentSolution.SolutionFileDirectory, _relativePath);
// For web site projects, Visual Studio stores the URL of the site as the relative path so it cannot be normalized.
// Legacy behavior dictates that we must just return the result of Path.Combine()
if (!Uri.TryCreate(_relativePath, UriKind.Absolute, out Uri _))
{
try
{
#if NETFRAMEWORK
_absolutePath = Path.GetFullPath(_absolutePath);
#else
_absolutePath = FileUtilities.NormalizePath(_absolutePath);
#endif
}
catch (Exception)
{
// The call to GetFullPath() can throw if the relative path is some unsupported value or the paths are too long for the current file system
// This falls back to previous behavior of returning a path that may not be correct but at least returns some value
}
}
}
return _absolutePath;
}
}
/// <summary>
/// The unique guid associated with this project, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form
/// </summary>
public string ProjectGuid { get; internal set; }
/// <summary>
/// The guid, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form, of this project's
/// parent project, if any.
/// </summary>
public string ParentProjectGuid { get; internal set; }
/// <summary>
/// List of guids, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form, mapping to projects
/// that this project has a build order dependency on, as defined in the solution file.
/// </summary>
public IReadOnlyList<string> Dependencies => _dependenciesAsReadonly ?? (_dependenciesAsReadonly = _dependencies.AsReadOnly());
/// <summary>
/// Configurations for this project, keyed off the configuration's full name, e.g. "Debug|x86"
/// They contain only the project configurations from the solution file that fully matched (configuration and platform) against the solution configurations.
/// </summary>
public IReadOnlyDictionary<string, ProjectConfigurationInSolution> ProjectConfigurations
=>
_projectConfigurationsReadOnly
?? (_projectConfigurationsReadOnly = new ReadOnlyDictionary<string, ProjectConfigurationInSolution>(_projectConfigurations));
/// <summary>
/// Extension of the project file, if any
/// </summary>
internal string Extension => Path.GetExtension(_relativePath);
/// <summary>
/// This project's type.
/// </summary>
public SolutionProjectType ProjectType { get; set; }
/// <summary>
/// Only applies to websites -- for other project types, references are
/// either specified as Dependencies above, or as ProjectReferences in the
/// project file, which the solution doesn't have insight into.
/// </summary>
internal List<string> ProjectReferences { get; } = new List<string>();
internal SolutionFile ParentSolution { get; set; }
// Key is configuration name, value is [struct] AspNetCompilerParameters
internal Hashtable AspNetConfigurations { get; set; }
internal string TargetFrameworkMoniker { get; set; }
#endregion
#region Methods
private bool _checkedIfCanBeMSBuildProjectFile;
private bool _canBeMSBuildProjectFile;
private string _canBeMSBuildProjectFileErrorMessage;
/// <summary>
/// Add the guid of a referenced project to our dependencies list.
/// </summary>
internal void AddDependency(string referencedProjectGuid)
{
_dependencies.Add(referencedProjectGuid);
_dependenciesAsReadonly = null;
}
/// <summary>
/// Set the requested project configuration.
/// </summary>
internal void SetProjectConfiguration(string configurationName, ProjectConfigurationInSolution configuration)
{
_projectConfigurations[configurationName] = configuration;
_projectConfigurationsReadOnly = null;
}
/// <summary>
/// Looks at the project file node and determines (roughly) if the project file is in the MSBuild format.
/// The results are cached in case this method is called multiple times.
/// </summary>
/// <param name="errorMessage">Detailed error message in case we encounter critical problems reading the file</param>
/// <returns></returns>
internal bool CanBeMSBuildProjectFile(out string errorMessage)
{
if (_checkedIfCanBeMSBuildProjectFile)
{
errorMessage = _canBeMSBuildProjectFileErrorMessage;
return _canBeMSBuildProjectFile;
}
_checkedIfCanBeMSBuildProjectFile = true;
_canBeMSBuildProjectFile = false;
errorMessage = null;
try
{
// Read project thru a XmlReader with proper setting to avoid DTD processing
var xrSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, CloseInput = true };
var projectDocument = new XmlDocument();
FileStream fs = File.OpenRead(AbsolutePath);
using (XmlReader xmlReader = XmlReader.Create(fs, xrSettings))
{
// Load the project file and get the first node
projectDocument.Load(xmlReader);
}
XmlElement mainProjectElement = null;
// The XML parser will guarantee that we only have one real root element,
// but we need to find it amongst the other types of XmlNode at the root.
foreach (XmlNode childNode in projectDocument.ChildNodes)
{
if (childNode.NodeType == XmlNodeType.Element)
{
mainProjectElement = (XmlElement)childNode;
break;
}
}
if (mainProjectElement?.LocalName == "Project")
{
// MSBuild supports project files with an empty (supported in Visual Studio 2017) or the default MSBuild
// namespace.
bool emptyNamespace = string.IsNullOrEmpty(mainProjectElement.NamespaceURI);
bool defaultNamespace = String.Equals(mainProjectElement.NamespaceURI,
XMakeAttributes.defaultXmlNamespace,
StringComparison.OrdinalIgnoreCase);
bool projectElementInvalid = ElementContainsInvalidNamespaceDefitions(mainProjectElement);
// If the MSBuild namespace is declared, it is very likely an MSBuild project that should be built.
if (defaultNamespace)
{
_canBeMSBuildProjectFile = true;
return _canBeMSBuildProjectFile;
}
// This is a bit of a special case, but an rptproj file will contain a Project with no schema that is
// not an MSBuild file. It will however have ToolsVersion="2.0" which is not supported with an empty
// schema. This is not a great solution, but it should cover the customer reported issue. See:
// https://github.com/dotnet/msbuild/issues/2064
if (emptyNamespace && !projectElementInvalid && mainProjectElement.GetAttribute("ToolsVersion") != "2.0")
{
_canBeMSBuildProjectFile = true;
return _canBeMSBuildProjectFile;
}
}
}
// catch all sorts of exceptions - if we encounter any problems here, we just assume the project file is not
// in the MSBuild format
// handle errors in path resolution
catch (SecurityException e)
{
_canBeMSBuildProjectFileErrorMessage = e.Message;
}
// handle errors in path resolution
catch (NotSupportedException e)
{
_canBeMSBuildProjectFileErrorMessage = e.Message;
}
// handle errors in loading project file
catch (IOException e)
{
_canBeMSBuildProjectFileErrorMessage = e.Message;
}
// handle errors in loading project file
catch (UnauthorizedAccessException e)
{
_canBeMSBuildProjectFileErrorMessage = e.Message;
}
// handle XML parsing errors (when reading project file)
// this is not critical, since the project file doesn't have to be in XML formal
catch (XmlException)
{
}
errorMessage = _canBeMSBuildProjectFileErrorMessage;
return _canBeMSBuildProjectFile;
}
/// <summary>
/// Find the unique name for this project, e.g. SolutionFolder\SubSolutionFolder\Project_Name
/// </summary>
internal string GetUniqueProjectName()
{
if (_uniqueProjectName == null)
{
// EtpSubProject and Venus projects have names that are already unique. No need to prepend the SLN folder.
if ((ProjectType == SolutionProjectType.WebProject) || (ProjectType == SolutionProjectType.EtpSubProject))
{
_uniqueProjectName = CleanseProjectName(ProjectName);
}
else
{
// This is "normal" project, which in this context means anything non-Venus and non-EtpSubProject.
// If this project has a parent SLN folder, first get the full unique name for the SLN folder,
// and tack on trailing backslash.
string uniqueName = String.Empty;
if (ParentProjectGuid != null)
{
ProjectInSolution proj = null;
ProjectInSolution solutionFolder = null;
// For the new parser, solution folders are not saved in ProjectsByGuid but in the SolutionFoldersByGuid.
if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out proj) &&
!ParentSolution.SolutionFoldersByGuid.TryGetValue(ParentProjectGuid, out solutionFolder))
{
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null || solutionFolder != null, "SubCategoryForSolutionParsingErrors",
new BuildEventFileInfo(ParentSolution.FullPath), "SolutionParseNestedProjectErrorWithNameAndGuid", ProjectName, ProjectGuid, ParentProjectGuid);
}
uniqueName = (proj != null ? proj.GetUniqueProjectName() : solutionFolder.GetUniqueProjectName()) + "\\";
}
// Now tack on our own project name, and cache it in the ProjectInSolution object for future quick access.
_uniqueProjectName = CleanseProjectName(uniqueName + ProjectName);
}
}
return _uniqueProjectName;
}
/// <summary>
/// Gets the original project name with the parent project as it is declared in the solution file, e.g. SolutionFolder\SubSolutionFolder\Project.Name
/// </summary>
internal string GetOriginalProjectName()
{
if (_originalProjectName == null)
{
// EtpSubProject and Venus projects have names that are already unique. No need to prepend the SLN folder.
if ((ProjectType == SolutionProjectType.WebProject) || (ProjectType == SolutionProjectType.EtpSubProject))
{
_originalProjectName = ProjectName;
}
else
{
// This is "normal" project, which in this context means anything non-Venus and non-EtpSubProject.
// If this project has a parent SLN folder, first get the full project name for the SLN folder,
// and tack on trailing backslash.
string projectName = String.Empty;
ProjectInSolution proj = null;
ProjectInSolution solutionFolder = null;
if (ParentProjectGuid != null)
{
if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out proj) &&
!ParentSolution.SolutionFoldersByGuid.TryGetValue(ParentProjectGuid, out solutionFolder))
{
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null || solutionFolder != null, "SubCategoryForSolutionParsingErrors",
new BuildEventFileInfo(ParentSolution.FullPath), "SolutionParseNestedProjectErrorWithNameAndGuid", ProjectName, ProjectGuid, ParentProjectGuid);
}
projectName = (proj != null ? proj.GetOriginalProjectName() : solutionFolder.GetOriginalProjectName()) + "\\";
}
// Now tack on our own project name, and cache it in the ProjectInSolution object for future quick access.
_originalProjectName = projectName + ProjectName;
}
}
return _originalProjectName;
}
internal string GetProjectGuidWithoutCurlyBrackets()
{
if (string.IsNullOrEmpty(ProjectGuid))
{
return null;
}
return ProjectGuid.Trim(['{', '}']);
}
/// <summary>
/// Changes the unique name of the project.
/// </summary>
internal void UpdateUniqueProjectName(string newUniqueName)
{
ErrorUtilities.VerifyThrowArgumentLength(newUniqueName);
_uniqueProjectName = newUniqueName;
}
/// <summary>
/// Cleanse the project name, by replacing characters like '@', '$' with '_'
/// </summary>
/// <param name="projectName">The name to be cleansed</param>
/// <returns>string</returns>
private static string CleanseProjectName(string projectName)
{
ErrorUtilities.VerifyThrow(projectName != null, "Null strings not allowed.");
// If there are no special chars, just return the original string immediately.
// Don't even instantiate the StringBuilder.
int indexOfChar = projectName.IndexOfAny(s_charsToCleanse);
if (indexOfChar == -1)
{
return projectName;
}
// This is where we're going to work on the final string to return to the caller.
var cleanProjectName = new StringBuilder(projectName);
// Replace each unclean character with a clean one
foreach (char uncleanChar in s_charsToCleanse)
{
cleanProjectName.Replace(uncleanChar, cleanCharacter);
}
return cleanProjectName.ToString();
}
/// <summary>
/// If the unique project name provided collides with one of the standard Solution project
/// entry point targets (Build, Rebuild, Clean, Publish), then disambiguate it by prepending the string "Solution:"
/// </summary>
/// <param name="uniqueProjectName">The unique name for the project</param>
/// <returns>string</returns>
internal static string DisambiguateProjectTargetName(string uniqueProjectName)
{
// Test our unique project name against those names that collide with Solution
// entry point targets
foreach (string projectName in projectNamesToDisambiguate)
{
if (String.Equals(uniqueProjectName, projectName, StringComparison.OrdinalIgnoreCase))
{
// Prepend "Solution:" so that the collision is resolved, but the
// log of the solution project still looks reasonable.
return "Solution:" + uniqueProjectName;
}
}
return uniqueProjectName;
}
/// <summary>
/// Check a Project element for known invalid namespace definitions.
/// </summary>
/// <param name="mainProjectElement">Project XML Element</param>
/// <returns>True if the element contains known invalid namespace definitions</returns>
private static bool ElementContainsInvalidNamespaceDefitions(XmlElement mainProjectElement)
{
if (mainProjectElement.HasAttributes)
{
// Data warehouse projects (.dwproj) will contain a Project element but are invalid MSBuild. Check attributes
// on Project for signs that this is a .dwproj file. If there are, it's not a valid MSBuild file.
return mainProjectElement.Attributes.OfType<XmlAttribute>().Any(a =>
a.Name.Equals("xmlns:dwd", StringComparison.OrdinalIgnoreCase) ||
a.Name.StartsWith("xmlns:dd", StringComparison.OrdinalIgnoreCase));
}
return false;
}
#endregion
#region Constants
internal const int DependencyLevelUnknown = -1;
internal const int DependencyLevelBeingDetermined = -2;
#endregion
}
}
|