1 // Copyright (c) Microsoft. All rights reserved. 2 // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 4 using System; 5 using System.Collections; 6 using System.Collections.Generic; 7 using System.IO; 8 using System.Security; 9 using System.Text; 10 using System.Xml; 11 using Microsoft.Build.Shared; 12 13 using XMakeAttributes = Microsoft.Build.Shared.XMakeAttributes; 14 using ProjectFileErrorUtilities = Microsoft.Build.Shared.ProjectFileErrorUtilities; 15 using BuildEventFileInfo = Microsoft.Build.Shared.BuildEventFileInfo; 16 using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; 17 using System.Collections.ObjectModel; 18 using System.Linq; 19 20 namespace Microsoft.Build.Construction 21 { 22 /// <remarks> 23 /// An enumeration defining the different types of projects we might find in an SLN. 24 /// </remarks> 25 public enum SolutionProjectType 26 { 27 /// <summary> 28 /// Everything else besides the below well-known project types. 29 /// </summary> 30 Unknown, 31 /// <summary> 32 /// C#, VB, F#, and VJ# projects 33 /// </summary> 34 KnownToBeMSBuildFormat, 35 /// <summary> 36 /// Solution folders appear in the .sln file, but aren't buildable projects. 37 /// </summary> 38 SolutionFolder, 39 /// <summary> 40 /// ASP.NET projects 41 /// </summary> 42 WebProject, 43 /// <summary> 44 /// Web Deployment (.wdproj) projects 45 /// </summary> 46 WebDeploymentProject, // MSBuildFormat, but Whidbey-era ones specify ProjectReferences differently 47 /// <summary> 48 /// Project inside an Enterprise Template project 49 /// </summary> 50 EtpSubProject, 51 /// <summary> 52 /// A shared project represents a collection of shared files that is not buildable on its own. 53 /// </summary> 54 SharedProject 55 } 56 57 internal struct AspNetCompilerParameters 58 { 59 internal string aspNetVirtualPath; // For Venus projects only, Virtual path for web 60 internal string aspNetPhysicalPath; // For Venus projects only, Physical path for web 61 internal string aspNetTargetPath; // For Venus projects only, Target for output files 62 internal string aspNetForce; // For Venus projects only, Force overwrite of target 63 internal string aspNetUpdateable; // For Venus projects only, compiled web application is updateable 64 internal string aspNetDebug; // For Venus projects only, generate symbols, etc. 65 internal string aspNetKeyFile; // For Venus projects only, strong name key file. 66 internal string aspNetKeyContainer; // For Venus projects only, strong name key container. 67 internal string aspNetDelaySign; // For Venus projects only, delay sign strong name. 68 internal string aspNetAPTCA; // For Venus projects only, AllowPartiallyTrustedCallers. 69 internal string aspNetFixedNames; // For Venus projects only, generate fixed assembly names. 70 } 71 72 /// <remarks> 73 /// This class represents a project (or SLN folder) that is read in from a solution file. 74 /// </remarks> 75 public sealed class ProjectInSolution 76 { 77 #region Constants 78 79 /// <summary> 80 /// Characters that need to be cleansed from a project name. 81 /// </summary> 82 private static readonly char[] s_charsToCleanse = { '%', '$', '@', ';', '.', '(', ')', '\'' }; 83 84 /// <summary> 85 /// Project names that need to be disambiguated when forming a target name 86 /// </summary> 87 internal static readonly string[] projectNamesToDisambiguate = { "Build", "Rebuild", "Clean", "Publish" }; 88 89 /// <summary> 90 /// Character that will be used to replace 'unclean' ones. 91 /// </summary> 92 private const char cleanCharacter = '_'; 93 94 #endregion 95 96 #region Member data 97 98 private SolutionProjectType _projectType; // For example, KnownToBeMSBuildFormat, VCProject, WebProject, etc. 99 private string _projectName; // For example, "WindowsApplication1" 100 private string _relativePath; // Relative from .SLN file. For example, "WindowsApplication1\WindowsApplication1.csproj" 101 private string _projectGuid; // The unique Guid assigned to this project or SLN folder. 102 private List<string> _dependencies; // A list of strings representing the Guids of the dependent projects. 103 private ArrayList _projectReferences; // A list of strings representing the guids of referenced projects. 104 // This is only used for VC/Venus projects 105 private string _parentProjectGuid; // If this project (or SLN folder) is within a SLN folder, this is the Guid of the parent SLN folder. 106 private string _uniqueProjectName; // For example, "MySlnFolder\MySubSlnFolder\WindowsApplication1" 107 private Hashtable _aspNetConfigurations; // Key is configuration name, value is [struct] AspNetCompilerParameters 108 private SolutionFile _parentSolution; // The parent solution for this project 109 private string _targetFrameworkMoniker; // used for website projects, since they don't have a project file in which the 110 // target framework is stored. Defaults to .NETFX 3.5 111 112 /// <summary> 113 /// The project configuration in given solution configuration 114 /// K: full solution configuration name (cfg + platform) 115 /// V: project configuration 116 /// </summary> 117 private Dictionary<string, ProjectConfigurationInSolution> _projectConfigurations; 118 119 #endregion 120 121 #region Constructors 122 ProjectInSolution(SolutionFile solution)123 internal ProjectInSolution(SolutionFile solution) 124 { 125 _projectType = SolutionProjectType.Unknown; 126 _projectName = null; 127 _relativePath = null; 128 _projectGuid = null; 129 _dependencies = new List<string>(); 130 _projectReferences = new ArrayList(); 131 _parentProjectGuid = null; 132 _uniqueProjectName = null; 133 _parentSolution = solution; 134 135 // default to .NET Framework 3.5 if this is an old solution that doesn't explicitly say. 136 _targetFrameworkMoniker = ".NETFramework,Version=v3.5"; 137 138 // This hashtable stores a AspNetCompilerParameters struct for each configuration name supported. 139 _aspNetConfigurations = new Hashtable(StringComparer.OrdinalIgnoreCase); 140 141 _projectConfigurations = new Dictionary<string, ProjectConfigurationInSolution>(StringComparer.OrdinalIgnoreCase); 142 } 143 144 #endregion 145 146 #region Properties 147 148 /// <summary> 149 /// This project's name 150 /// </summary> 151 public string ProjectName 152 { 153 get { return _projectName; } 154 internal set { _projectName = value; } 155 } 156 157 /// <summary> 158 /// The path to this project file, relative to the solution location 159 /// </summary> 160 public string RelativePath 161 { 162 get { return _relativePath; } 163 internal set 164 { 165 _relativePath = FileUtilities.MaybeAdjustFilePath(value, 166 baseDirectory:this.ParentSolution.SolutionFileDirectory ?? String.Empty); 167 } 168 } 169 170 /// <summary> 171 /// Returns the absolute path for this project 172 /// </summary> 173 public string AbsolutePath 174 { 175 get 176 { 177 return Path.Combine(this.ParentSolution.SolutionFileDirectory, this.RelativePath); 178 } 179 } 180 181 /// <summary> 182 /// The unique guid associated with this project, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form 183 /// </summary> 184 public string ProjectGuid 185 { 186 get { return _projectGuid; } 187 internal set { _projectGuid = value; } 188 } 189 190 /// <summary> 191 /// The guid, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form, of this project's 192 /// parent project, if any. 193 /// </summary> 194 public string ParentProjectGuid 195 { 196 get { return _parentProjectGuid; } 197 internal set { _parentProjectGuid = value; } 198 } 199 200 /// <summary> 201 /// List of guids, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form, mapping to projects 202 /// that this project has a build order dependency on, as defined in the solution file. 203 /// </summary> 204 public IReadOnlyList<string> Dependencies 205 { 206 get { return _dependencies.AsReadOnly(); } 207 } 208 209 /// <summary> 210 /// Configurations for this project, keyed off the configuration's full name, e.g. "Debug|x86" 211 /// </summary> 212 public IReadOnlyDictionary<string, ProjectConfigurationInSolution> ProjectConfigurations 213 { 214 get { return new ReadOnlyDictionary<string, ProjectConfigurationInSolution>(_projectConfigurations); } 215 } 216 217 /// <summary> 218 /// Extension of the project file, if any 219 /// </summary> 220 internal string Extension 221 { 222 get 223 { 224 return Path.GetExtension(_relativePath); 225 } 226 } 227 228 /// <summary> 229 /// This project's type. 230 /// </summary> 231 public SolutionProjectType ProjectType 232 { 233 get { return _projectType; } 234 set { _projectType = value; } 235 } 236 237 /// <summary> 238 /// Only applies to websites -- for other project types, references are 239 /// either specified as Dependencies above, or as ProjectReferences in the 240 /// project file, which the solution doesn't have insight into. 241 /// </summary> 242 internal ArrayList ProjectReferences 243 { 244 get { return _projectReferences; } 245 } 246 247 internal SolutionFile ParentSolution 248 { 249 get { return _parentSolution; } 250 set { _parentSolution = value; } 251 } 252 253 internal Hashtable AspNetConfigurations 254 { 255 get { return _aspNetConfigurations; } 256 set { _aspNetConfigurations = value; } 257 } 258 259 internal string TargetFrameworkMoniker 260 { 261 get { return _targetFrameworkMoniker; } 262 set { _targetFrameworkMoniker = value; } 263 } 264 265 #endregion 266 267 #region Methods 268 269 private bool _checkedIfCanBeMSBuildProjectFile = false; 270 private bool _canBeMSBuildProjectFile; 271 private string _canBeMSBuildProjectFileErrorMessage; 272 273 /// <summary> 274 /// Add the guid of a referenced project to our dependencies list. 275 /// </summary> AddDependency(string referencedProjectGuid)276 internal void AddDependency(string referencedProjectGuid) 277 { 278 _dependencies.Add(referencedProjectGuid); 279 } 280 281 /// <summary> 282 /// Set the requested project configuration. 283 /// </summary> SetProjectConfiguration(string configurationName, ProjectConfigurationInSolution configuration)284 internal void SetProjectConfiguration(string configurationName, ProjectConfigurationInSolution configuration) 285 { 286 _projectConfigurations[configurationName] = configuration; 287 } 288 289 /// <summary> 290 /// Looks at the project file node and determines (roughly) if the project file is in the MSBuild format. 291 /// The results are cached in case this method is called multiple times. 292 /// </summary> 293 /// <param name="errorMessage">Detailed error message in case we encounter critical problems reading the file</param> 294 /// <returns></returns> CanBeMSBuildProjectFile(out string errorMessage)295 internal bool CanBeMSBuildProjectFile(out string errorMessage) 296 { 297 if (_checkedIfCanBeMSBuildProjectFile) 298 { 299 errorMessage = _canBeMSBuildProjectFileErrorMessage; 300 return _canBeMSBuildProjectFile; 301 } 302 303 _checkedIfCanBeMSBuildProjectFile = true; 304 _canBeMSBuildProjectFile = false; 305 errorMessage = null; 306 307 try 308 { 309 // Read project thru a XmlReader with proper setting to avoid DTD processing 310 XmlReaderSettings xrSettings = new XmlReaderSettings(); 311 xrSettings.DtdProcessing = DtdProcessing.Ignore; 312 313 XmlDocument projectDocument = new XmlDocument(); 314 315 using (XmlReader xmlReader = XmlReader.Create(this.AbsolutePath, xrSettings)) 316 { 317 // Load the project file and get the first node 318 projectDocument.Load(xmlReader); 319 } 320 321 XmlElement mainProjectElement = null; 322 323 // The XML parser will guarantee that we only have one real root element, 324 // but we need to find it amongst the other types of XmlNode at the root. 325 foreach (XmlNode childNode in projectDocument.ChildNodes) 326 { 327 if (childNode.NodeType == XmlNodeType.Element) 328 { 329 mainProjectElement = (XmlElement)childNode; 330 break; 331 } 332 } 333 334 if (mainProjectElement != null && mainProjectElement.LocalName == "Project") 335 { 336 // MSBuild supports project files with an empty (supported in Visual Studio 2017) or the default MSBuild 337 // namespace. 338 bool emptyNamespace = string.IsNullOrEmpty(mainProjectElement.NamespaceURI); 339 bool defaultNamespace = String.Compare(mainProjectElement.NamespaceURI, 340 XMakeAttributes.defaultXmlNamespace, 341 StringComparison.OrdinalIgnoreCase) == 0; 342 bool projectElementInvalid = ElementContainsInvalidNamespaceDefitions(mainProjectElement); 343 344 // If the MSBuild namespace is declared, it is very likely an MSBuild project that should be built. 345 if (defaultNamespace) 346 { 347 _canBeMSBuildProjectFile = true; 348 return _canBeMSBuildProjectFile; 349 } 350 351 // This is a bit of a special case, but an rptproj file will contain a Project with no schema that is 352 // not an MSBuild file. It will however have ToolsVersion="2.0" which is not supported with an empty 353 // schema. This is not a great solution, but it should cover the customer reported issue. See: 354 // https://github.com/Microsoft/msbuild/issues/2064 355 if (emptyNamespace && !projectElementInvalid && mainProjectElement.GetAttribute("ToolsVersion") != "2.0") 356 { 357 _canBeMSBuildProjectFile = true; 358 return _canBeMSBuildProjectFile; 359 } 360 } 361 } 362 // catch all sorts of exceptions - if we encounter any problems here, we just assume the project file is not 363 // in the MSBuild format 364 365 // handle errors in path resolution 366 catch (SecurityException e) 367 { 368 _canBeMSBuildProjectFileErrorMessage = e.Message; 369 } 370 // handle errors in path resolution 371 catch (NotSupportedException e) 372 { 373 _canBeMSBuildProjectFileErrorMessage = e.Message; 374 } 375 // handle errors in loading project file 376 catch (IOException e) 377 { 378 _canBeMSBuildProjectFileErrorMessage = e.Message; 379 } 380 // handle errors in loading project file 381 catch (UnauthorizedAccessException e) 382 { 383 _canBeMSBuildProjectFileErrorMessage = e.Message; 384 } 385 // handle XML parsing errors (when reading project file) 386 // this is not critical, since the project file doesn't have to be in XML formal 387 catch (XmlException) 388 { 389 } 390 391 errorMessage = _canBeMSBuildProjectFileErrorMessage; 392 393 return _canBeMSBuildProjectFile; 394 } 395 396 /// <summary> 397 /// Find the unique name for this project, e.g. SolutionFolder\SubSolutionFolder\ProjectName 398 /// </summary> GetUniqueProjectName()399 internal string GetUniqueProjectName() 400 { 401 if (_uniqueProjectName == null) 402 { 403 // EtpSubProject and Venus projects have names that are already unique. No need to prepend the SLN folder. 404 if ((this.ProjectType == SolutionProjectType.WebProject) || (this.ProjectType == SolutionProjectType.EtpSubProject)) 405 { 406 _uniqueProjectName = CleanseProjectName(this.ProjectName); 407 } 408 else 409 { 410 // This is "normal" project, which in this context means anything non-Venus and non-EtpSubProject. 411 412 // If this project has a parent SLN folder, first get the full unique name for the SLN folder, 413 // and tack on trailing backslash. 414 string uniqueName = String.Empty; 415 416 if (this.ParentProjectGuid != null) 417 { 418 ProjectInSolution proj; 419 if (!this.ParentSolution.ProjectsByGuid.TryGetValue(this.ParentProjectGuid, out proj)) 420 { 421 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null, "SubCategoryForSolutionParsingErrors", 422 new BuildEventFileInfo(_parentSolution.FullPath), "SolutionParseNestedProjectError"); 423 } 424 425 uniqueName = proj.GetUniqueProjectName() + "\\"; 426 } 427 428 // Now tack on our own project name, and cache it in the ProjectInSolution object for future quick access. 429 _uniqueProjectName = CleanseProjectName(uniqueName + this.ProjectName); 430 } 431 } 432 433 return _uniqueProjectName; 434 } 435 436 /// <summary> 437 /// Changes the unique name of the project. 438 /// </summary> UpdateUniqueProjectName(string newUniqueName)439 internal void UpdateUniqueProjectName(string newUniqueName) 440 { 441 ErrorUtilities.VerifyThrowArgumentLength(newUniqueName, "newUniqueName"); 442 443 _uniqueProjectName = newUniqueName; 444 } 445 446 /// <summary> 447 /// Cleanse the project name, by replacing characters like '@', '$' with '_' 448 /// </summary> 449 /// <param name="projectName">The name to be cleansed</param> 450 /// <returns>string</returns> CleanseProjectName(string projectName)451 static private string CleanseProjectName(string projectName) 452 { 453 ErrorUtilities.VerifyThrow(projectName != null, "Null strings not allowed."); 454 455 // If there are no special chars, just return the original string immediately. 456 // Don't even instantiate the StringBuilder. 457 int indexOfChar = projectName.IndexOfAny(s_charsToCleanse); 458 if (indexOfChar == -1) 459 { 460 return projectName; 461 } 462 463 // This is where we're going to work on the final string to return to the caller. 464 StringBuilder cleanProjectName = new StringBuilder(projectName); 465 466 // Replace each unclean character with a clean one 467 foreach (char uncleanChar in s_charsToCleanse) 468 { 469 cleanProjectName.Replace(uncleanChar, cleanCharacter); 470 } 471 472 return cleanProjectName.ToString(); 473 } 474 475 /// <summary> 476 /// If the unique project name provided collides with one of the standard Solution project 477 /// entry point targets (Build, Rebuild, Clean, Publish), then disambiguate it by prepending the string "Solution:" 478 /// </summary> 479 /// <param name="uniqueProjectName">The unique name for the project</param> 480 /// <returns>string</returns> DisambiguateProjectTargetName(string uniqueProjectName)481 static internal string DisambiguateProjectTargetName(string uniqueProjectName) 482 { 483 // Test our unique project name against those names that collide with Solution 484 // entry point targets 485 foreach (string projectName in projectNamesToDisambiguate) 486 { 487 if (String.Compare(uniqueProjectName, projectName, StringComparison.OrdinalIgnoreCase) == 0) 488 { 489 // Prepend "Solution:" so that the collision is resolved, but the 490 // log of the solution project still looks reasonable. 491 return "Solution:" + uniqueProjectName; 492 } 493 } 494 495 return uniqueProjectName; 496 } 497 498 /// <summary> 499 /// Check a Project element for known invalid namespace definitions. 500 /// </summary> 501 /// <param name="mainProjectElement">Project XML Element</param> 502 /// <returns>True if the element contains known invalid namespace definitions</returns> ElementContainsInvalidNamespaceDefitions(XmlElement mainProjectElement)503 private static bool ElementContainsInvalidNamespaceDefitions(XmlElement mainProjectElement) 504 { 505 if (mainProjectElement.HasAttributes) 506 { 507 // Data warehouse projects (.dwproj) will contain a Project element but are invalid MSBuild. Check attributes 508 // on Project for signs that this is a .dwproj file. If there are, it's not a valid MSBuild file. 509 return mainProjectElement.Attributes.OfType<XmlAttribute>().Any(a => 510 a.Name.Equals("xmlns:dwd", StringComparison.OrdinalIgnoreCase) || 511 a.Name.StartsWith("xmlns:dd", StringComparison.OrdinalIgnoreCase)); 512 } 513 514 return false; 515 } 516 517 #endregion 518 519 #region Constants 520 521 internal const int DependencyLevelUnknown = -1; 522 internal const int DependencyLevelBeingDetermined = -2; 523 524 #endregion 525 } 526 } 527