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.Xml; 8 using System.IO; 9 using System.Text; 10 using System.Globalization; 11 using System.Security; 12 using System.Text.RegularExpressions; 13 14 using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; 15 using VisualStudioConstants = Microsoft.Build.Shared.VisualStudioConstants; 16 using ProjectFileErrorUtilities = Microsoft.Build.Shared.ProjectFileErrorUtilities; 17 using BuildEventFileInfo = Microsoft.Build.Shared.BuildEventFileInfo; 18 using ResourceUtilities = Microsoft.Build.Shared.ResourceUtilities; 19 using ExceptionUtilities = Microsoft.Build.Shared.ExceptionHandling; 20 using System.Collections.ObjectModel; 21 22 namespace Microsoft.Build.Construction 23 { 24 /// <remarks> 25 /// This class contains the functionality to parse a solution file and return a corresponding 26 /// MSBuild project file containing the projects and dependencies defined in the solution. 27 /// </remarks> 28 public sealed class SolutionFile 29 { 30 #region Solution specific constants 31 32 // An example of a project line looks like this: 33 // Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary1", "ClassLibrary1\ClassLibrary1.csproj", "{05A5AD00-71B5-4612-AF2F-9EA9121C4111}" 34 private static readonly Lazy<Regex> s_crackProjectLine = new Lazy<Regex>( 35 () => new Regex 36 ( 37 "^" // Beginning of line 38 + "Project\\(\"(?<PROJECTTYPEGUID>.*)\"\\)" 39 + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace 40 + "\"(?<PROJECTNAME>.*)\"" 41 + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace 42 + "\"(?<RELATIVEPATH>.*)\"" 43 + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace 44 + "\"(?<PROJECTGUID>.*)\"" 45 + "$", // End-of-line 46 RegexOptions.Compiled 47 ) 48 ); 49 50 // An example of a property line looks like this: 51 // AspNetCompiler.VirtualPath = "/webprecompile" 52 // Because website projects now include the target framework moniker as 53 // one of their properties, <PROPERTYVALUE> may now have '=' in it. 54 55 private static readonly Lazy<Regex> s_crackPropertyLine = new Lazy<Regex>( 56 () => new Regex 57 ( 58 "^" // Beginning of line 59 + "(?<PROPERTYNAME>[^=]*)" 60 + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace 61 + "(?<PROPERTYVALUE>.*)" 62 + "$", // End-of-line 63 RegexOptions.Compiled 64 ) 65 ); 66 67 internal const int slnFileMinUpgradableVersion = 7; // Minimum version for MSBuild to give a nice message 68 internal const int slnFileMinVersion = 9; // Minimum version for MSBuild to actually do anything useful 69 internal const int slnFileMaxVersion = VisualStudioConstants.CurrentVisualStudioSolutionFileVersion; 70 71 private const string vbProjectGuid = "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}"; 72 private const string csProjectGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; 73 private const string cpsProjectGuid = "{13B669BE-BB05-4DDF-9536-439F39A36129}"; 74 private const string cpsCsProjectGuid = "{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"; 75 private const string cpsVbProjectGuid = "{778DAE3C-4631-46EA-AA77-85C1314464D9}"; 76 private const string vjProjectGuid = "{E6FDF86B-F3D1-11D4-8576-0002A516ECE8}"; 77 private const string vcProjectGuid = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"; 78 private const string fsProjectGuid = "{F2A71F9B-5D33-465A-A702-920D77279786}"; 79 private const string dbProjectGuid = "{C8D11400-126E-41CD-887F-60BD40844F9E}"; 80 private const string wdProjectGuid = "{2CFEAB61-6A3B-4EB8-B523-560B4BEEF521}"; 81 private const string webProjectGuid = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}"; 82 private const string solutionFolderGuid = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}"; 83 private const string sharedProjectGuid = "{D954291E-2A0B-460D-934E-DC6B0785DB48}"; 84 85 #endregion 86 87 #region Member data 88 89 private int _slnFileActualVersion = 0; // The major version number of the .SLN file we're reading. 90 private string _solutionFile = null; // Could be absolute or relative path to the .SLN file. 91 private string _solutionFileDirectory = null; // Absolute path the solution file 92 private bool _solutionContainsWebProjects = false; // Does this SLN contain any web projects? 93 private bool _solutionContainsWebDeploymentProjects = false; // Does this SLN contain .wdproj projects? 94 private bool _parsingForConversionOnly = false; // Are we parsing this solution to get project reference data during 95 // conversion, or in preparation for actually building the solution? 96 97 // The list of projects in this SLN, keyed by the project GUID. 98 private Dictionary<string, ProjectInSolution> _projects = null; 99 100 // The list of projects in the SLN, in order of their appearance in the SLN. 101 private List<ProjectInSolution> _projectsInOrder = null; 102 103 // The list of solution configurations in the solution 104 private List<SolutionConfigurationInSolution> _solutionConfigurations; 105 106 // cached default configuration name for GetDefaultConfigurationName 107 private string _defaultConfigurationName; 108 109 // cached default platform name for GetDefaultPlatformName 110 private string _defaultPlatformName; 111 112 //List of warnings that occured while parsing solution 113 private ArrayList _solutionParserWarnings = null; 114 115 //List of comments that occured while parsing solution 116 private ArrayList _solutionParserComments = null; 117 118 // unit-testing only 119 private ArrayList _solutionParserErrorCodes = null; 120 121 // VisualStudionVersion specified in Dev12+ solutions 122 private Version _currentVisualStudioVersion = null; 123 124 private StreamReader _reader = null; 125 private int _currentLineNumber = 0; 126 127 #endregion 128 129 #region Constructors 130 131 /// <summary> 132 /// Constructor 133 /// </summary> SolutionFile()134 internal SolutionFile() 135 { 136 _solutionParserWarnings = new ArrayList(); 137 _solutionParserErrorCodes = new ArrayList(); 138 _solutionParserComments = new ArrayList(); 139 } 140 141 #endregion 142 143 #region Properties 144 145 /// <summary> 146 /// This property returns the list of warnings that were generated during solution parsing 147 /// </summary> 148 internal ArrayList SolutionParserWarnings 149 { 150 get 151 { 152 return _solutionParserWarnings; 153 } 154 } 155 156 /// <summary> 157 /// This property returns the list of comments that were generated during the solution parsing 158 /// </summary> 159 internal ArrayList SolutionParserComments 160 { 161 get 162 { 163 return _solutionParserComments; 164 } 165 } 166 167 /// <summary> 168 /// This property returns the list of error codes for warnings/errors that were generated during solution parsing. 169 /// </summary> 170 internal ArrayList SolutionParserErrorCodes 171 { 172 get 173 { 174 return _solutionParserErrorCodes; 175 } 176 } 177 178 /// <summary> 179 /// Returns the actual major version of the parsed solution file 180 /// </summary> 181 internal int Version 182 { 183 get 184 { 185 return _slnFileActualVersion; 186 } 187 } 188 189 /// <summary> 190 /// Returns Visual Studio major version 191 /// </summary> 192 internal int VisualStudioVersion 193 { 194 get 195 { 196 if (_currentVisualStudioVersion != null) 197 { 198 return _currentVisualStudioVersion.Major; 199 } 200 else 201 { 202 return this.Version - 1; 203 } 204 } 205 } 206 207 /// <summary> 208 /// Returns true if the solution contains any web projects 209 /// </summary> 210 internal bool ContainsWebProjects 211 { 212 get 213 { 214 return _solutionContainsWebProjects; 215 } 216 } 217 218 /// <summary> 219 /// Returns true if the solution contains any .wdproj projects. Used to determine 220 /// whether we need to load up any projects to examine dependencies. 221 /// </summary> 222 internal bool ContainsWebDeploymentProjects 223 { 224 get 225 { 226 return _solutionContainsWebDeploymentProjects; 227 } 228 } 229 230 /// <summary> 231 /// All projects in this solution, in the order they appeared in the solution file 232 /// </summary> 233 public IReadOnlyList<ProjectInSolution> ProjectsInOrder 234 { 235 get 236 { 237 return _projectsInOrder.AsReadOnly(); 238 } 239 } 240 241 /// <summary> 242 /// The collection of projects in this solution, accessible by their guids as a 243 /// string in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form 244 /// </summary> 245 public IReadOnlyDictionary<string, ProjectInSolution> ProjectsByGuid 246 { 247 get 248 { 249 return new ReadOnlyDictionary<string, ProjectInSolution>(_projects); 250 } 251 } 252 253 /// <summary> 254 /// This is the read/write accessor for the solution file which we will parse. This 255 /// must be set before calling any other methods on this class. 256 /// </summary> 257 /// <value></value> 258 internal string FullPath 259 { 260 get 261 { 262 return _solutionFile; 263 } 264 265 set 266 { 267 // Should already be canonicalized to a full path 268 ErrorUtilities.VerifyThrowInternalRooted(value); 269 _solutionFile = value; 270 } 271 } 272 273 internal string SolutionFileDirectory 274 { 275 get 276 { 277 return _solutionFileDirectory; 278 } 279 // This setter is only used by the unit tests 280 set 281 { 282 _solutionFileDirectory = value; 283 } 284 } 285 286 /// <summary> 287 /// For unit-testing only. 288 /// </summary> 289 /// <value></value> 290 internal StreamReader SolutionReader 291 { 292 get 293 { 294 return _reader; 295 } 296 297 set 298 { 299 _reader = value; 300 } 301 } 302 303 /// <summary> 304 /// The list of all full solution configurations (configuration + platform) in this solution 305 /// </summary> 306 public IReadOnlyList<SolutionConfigurationInSolution> SolutionConfigurations 307 { 308 get 309 { 310 return _solutionConfigurations.AsReadOnly(); 311 } 312 } 313 314 #endregion 315 316 #region Methods 317 318 /// <summary> 319 /// This method takes a path to a solution file, parses the projects and project dependencies 320 /// in the solution file, and creates internal data structures representing the projects within 321 /// the SLN. 322 /// </summary> Parse(string solutionFile)323 public static SolutionFile Parse(string solutionFile) 324 { 325 SolutionFile parser = new SolutionFile(); 326 parser.FullPath = solutionFile; 327 328 parser.ParseSolutionFile(); 329 330 return parser; 331 } 332 333 /// <summary> 334 /// Returns "true" if it's a project that's expected to be buildable, or false if it's 335 /// not (e.g. a solution folder) 336 /// </summary> 337 /// <param name="project">The project in the solution</param> 338 /// <returns>Whether the project is expected to be buildable</returns> IsBuildableProject(ProjectInSolution project)339 internal static bool IsBuildableProject(ProjectInSolution project) 340 { 341 return (project.ProjectType != SolutionProjectType.SolutionFolder && project.ProjectConfigurations.Count > 0); 342 } 343 344 /// <summary> 345 /// Given a solution file, parses the header and returns the major version numbers of the solution file 346 /// and the visual studio. 347 /// Throws InvalidProjectFileException if the solution header is invalid, or if the version is less than 348 /// our minimum version. 349 /// </summary> GetSolutionFileAndVisualStudioMajorVersions(string solutionFile, out int solutionVersion, out int visualStudioMajorVersion)350 internal static void GetSolutionFileAndVisualStudioMajorVersions(string solutionFile, out int solutionVersion, out int visualStudioMajorVersion) 351 { 352 ErrorUtilities.VerifyThrow(!String.IsNullOrEmpty(solutionFile), "null solution file passed to GetSolutionFileMajorVersion!"); 353 ErrorUtilities.VerifyThrowInternalRooted(solutionFile); 354 355 const string slnFileHeaderNoVersion = "Microsoft Visual Studio Solution File, Format Version "; 356 const string slnFileVSVLinePrefix = "VisualStudioVersion"; 357 FileStream fileStream = null; 358 StreamReader reader = null; 359 bool validVersionFound = false; 360 361 solutionVersion = 0; 362 visualStudioMajorVersion = 0; 363 364 try 365 { 366 // Open the file 367 fileStream = File.OpenRead(solutionFile); 368 reader = new StreamReader(fileStream, Encoding.GetEncoding(0)); // HIGHCHAR: If solution files have no byte-order marks, then assume ANSI rather than ASCII. 369 370 // Read first 4 lines of the solution file. 371 // The header is expected to be in line 1 or 2 372 // VisualStudioVersion is expected to be in line 3 or 4. 373 for (int i = 0; i < 4; i++) 374 { 375 string line = reader.ReadLine(); 376 377 if (line == null) 378 { 379 break; 380 } 381 382 if (line.Trim().StartsWith(slnFileHeaderNoVersion, StringComparison.Ordinal)) 383 { 384 // Found it. Validate the version. 385 string fileVersionFromHeader = line.Substring(slnFileHeaderNoVersion.Length); 386 387 Version version = null; 388 if (!System.Version.TryParse(fileVersionFromHeader, out version)) 389 { 390 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile 391 ( 392 false /* just throw the exception */, 393 "SubCategoryForSolutionParsingErrors", 394 new BuildEventFileInfo(solutionFile), 395 "SolutionParseVersionMismatchError", 396 slnFileMinUpgradableVersion, 397 slnFileMaxVersion 398 ); 399 } 400 401 solutionVersion = version.Major; 402 403 // Validate against our min & max 404 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile 405 ( 406 solutionVersion >= slnFileMinUpgradableVersion, 407 "SubCategoryForSolutionParsingErrors", 408 new BuildEventFileInfo(solutionFile), 409 "SolutionParseVersionMismatchError", 410 slnFileMinUpgradableVersion, 411 slnFileMaxVersion 412 ); 413 414 validVersionFound = true; 415 } 416 else if (line.Trim().StartsWith(slnFileVSVLinePrefix, StringComparison.Ordinal)) 417 { 418 Version visualStudioVersion = ParseVisualStudioVersion(line); 419 if (visualStudioVersion != null) 420 { 421 visualStudioMajorVersion = visualStudioVersion.Major; 422 } 423 } 424 } 425 } 426 finally 427 { 428 if (fileStream != null) 429 { 430 fileStream.Dispose(); 431 } 432 433 if (reader != null) 434 { 435 reader.Dispose(); 436 } 437 } 438 439 if (validVersionFound) 440 { 441 return; 442 } 443 444 // Didn't find the header in lines 1-4, so the solution file is invalid. 445 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile 446 ( 447 false /* just throw the exception */, 448 "SubCategoryForSolutionParsingErrors", 449 new BuildEventFileInfo(solutionFile), 450 "SolutionParseNoHeaderError" 451 ); 452 453 return; /* UNREACHABLE */ 454 } 455 456 /// <summary> 457 /// Adds a configuration to this solution 458 /// </summary> AddSolutionConfiguration(string configurationName, string platformName)459 internal void AddSolutionConfiguration(string configurationName, string platformName) 460 { 461 _solutionConfigurations.Add(new SolutionConfigurationInSolution(configurationName, platformName)); 462 } 463 464 /// <summary> 465 /// Reads a line from the StreamReader, trimming leading and trailing whitespace. 466 /// </summary> 467 /// <returns></returns> ReadLine()468 private string ReadLine() 469 { 470 ErrorUtilities.VerifyThrow(_reader != null, "ParseFileHeader(): reader is null!"); 471 472 string line = _reader.ReadLine(); 473 _currentLineNumber++; 474 475 if (line != null) 476 { 477 line = line.Trim(); 478 } 479 480 return line; 481 } 482 483 /// <summary> 484 /// This method takes a path to a solution file, parses the projects and project dependencies 485 /// in the solution file, and creates internal data structures representing the projects within 486 /// the SLN. Used for conversion, which means it allows situations that we refuse to actually build. 487 /// </summary> ParseSolutionFileForConversion()488 internal void ParseSolutionFileForConversion() 489 { 490 _parsingForConversionOnly = true; 491 ParseSolutionFile(); 492 } 493 494 /// <summary> 495 /// This method takes a path to a solution file, parses the projects and project dependencies 496 /// in the solution file, and creates internal data structures representing the projects within 497 /// the SLN. 498 /// </summary> ParseSolutionFile()499 internal void ParseSolutionFile() 500 { 501 ErrorUtilities.VerifyThrow((_solutionFile != null) && (_solutionFile.Length != 0), "ParseSolutionFile() got a null solution file!"); 502 ErrorUtilities.VerifyThrowInternalRooted(_solutionFile); 503 504 FileStream fileStream = null; 505 _reader = null; 506 507 try 508 { 509 // Open the file 510 fileStream = File.OpenRead(_solutionFile); 511 // Store the directory of the file as the current directory may change while we are processes the file 512 _solutionFileDirectory = Path.GetDirectoryName(_solutionFile); 513 _reader = new StreamReader(fileStream, Encoding.GetEncoding(0)); // HIGHCHAR: If solution files have no byte-order marks, then assume ANSI rather than ASCII. 514 this.ParseSolution(); 515 } 516 catch (Exception e) 517 { 518 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(!ExceptionUtilities.IsIoRelatedException(e), new BuildEventFileInfo(_solutionFile), "InvalidProjectFile", e.Message); 519 throw; 520 } 521 finally 522 { 523 if (fileStream != null) 524 { 525 fileStream.Dispose(); 526 } 527 528 if (_reader != null) 529 { 530 _reader.Dispose(); 531 } 532 } 533 } 534 535 /// <summary> 536 /// Parses the SLN file represented by the StreamReader in this.reader, and populates internal 537 /// data structures based on the SLN file contents. 538 /// </summary> ParseSolution()539 internal void ParseSolution() 540 { 541 _projects = new Dictionary<string, ProjectInSolution>(StringComparer.OrdinalIgnoreCase); 542 _projectsInOrder = new List<ProjectInSolution>(); 543 _solutionContainsWebProjects = false; 544 _slnFileActualVersion = 0; 545 _currentLineNumber = 0; 546 _solutionConfigurations = new List<SolutionConfigurationInSolution>(); 547 _defaultConfigurationName = null; 548 _defaultPlatformName = null; 549 550 // the raw list of project configurations in solution configurations, to be processed after it's fully read in. 551 Hashtable rawProjectConfigurationsEntries = null; 552 553 ParseFileHeader(); 554 555 string str; 556 while ((str = ReadLine()) != null) 557 { 558 if (str.StartsWith("Project(", StringComparison.Ordinal)) 559 { 560 ParseProject(str); 561 } 562 else if (str.StartsWith("GlobalSection(NestedProjects)", StringComparison.Ordinal)) 563 { 564 ParseNestedProjects(); 565 } 566 else if (str.StartsWith("GlobalSection(SolutionConfigurationPlatforms)", StringComparison.Ordinal)) 567 { 568 ParseSolutionConfigurations(); 569 } 570 else if (str.StartsWith("GlobalSection(ProjectConfigurationPlatforms)", StringComparison.Ordinal)) 571 { 572 rawProjectConfigurationsEntries = ParseProjectConfigurations(); 573 } 574 else if (str.StartsWith("VisualStudioVersion", StringComparison.Ordinal)) 575 { 576 _currentVisualStudioVersion = ParseVisualStudioVersion(str); 577 } 578 else 579 { 580 // No other section types to process at this point, so just ignore the line 581 // and continue. 582 } 583 } 584 585 if (rawProjectConfigurationsEntries != null) 586 { 587 ProcessProjectConfigurationSection(rawProjectConfigurationsEntries); 588 } 589 590 // Cache the unique name of each project, and check that we don't have any duplicates. 591 Hashtable projectsByUniqueName = new Hashtable(StringComparer.OrdinalIgnoreCase); 592 593 foreach (ProjectInSolution proj in _projectsInOrder) 594 { 595 // Find the unique name for the project. This method also caches the unique name, 596 // so it doesn't have to be recomputed later. 597 string uniqueName = proj.GetUniqueProjectName(); 598 599 if (proj.ProjectType == SolutionProjectType.WebProject) 600 { 601 // Examine port information and determine if we need to disambiguate similarly-named projects with different ports. 602 Uri uri; 603 if (Uri.TryCreate(proj.RelativePath, UriKind.Absolute, out uri)) 604 { 605 if (!uri.IsDefaultPort) 606 { 607 // If there are no other projects with the same name as this one, then we will keep this project's unique name, otherwise 608 // we will create a new unique name with the port added. 609 foreach (ProjectInSolution otherProj in _projectsInOrder) 610 { 611 if (Object.ReferenceEquals(proj, otherProj)) 612 { 613 continue; 614 } 615 616 if (String.Equals(otherProj.ProjectName, proj.ProjectName, StringComparison.OrdinalIgnoreCase)) 617 { 618 uniqueName = String.Format(CultureInfo.InvariantCulture, "{0}:{1}", uniqueName, uri.Port); 619 proj.UpdateUniqueProjectName(uniqueName); 620 break; 621 } 622 } 623 } 624 } 625 } 626 627 // Throw an error if there are any duplicates 628 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile( 629 projectsByUniqueName[uniqueName] == null, 630 "SubCategoryForSolutionParsingErrors", 631 new BuildEventFileInfo(FullPath), 632 "SolutionParseDuplicateProject", 633 uniqueName); 634 635 // Update the hash table with this unique name 636 projectsByUniqueName[uniqueName] = proj; 637 } 638 } // ParseSolutionFile() 639 640 /// <summary> 641 /// This method searches the first two lines of the solution file opened by the specified 642 /// StreamReader for the solution file header. An exception is thrown if it is not found. 643 /// 644 /// The solution file header looks like this: 645 /// 646 /// Microsoft Visual Studio Solution File, Format Version 9.00 647 /// 648 /// </summary> ParseFileHeader()649 private void ParseFileHeader() 650 { 651 ErrorUtilities.VerifyThrow(_reader != null, "ParseFileHeader(): reader is null!"); 652 653 const string slnFileHeaderNoVersion = "Microsoft Visual Studio Solution File, Format Version "; 654 655 // Read the file header. This can be on either of the first two lines. 656 for (int i = 1; i <= 2; i++) 657 { 658 string str = ReadLine(); 659 if (str == null) 660 { 661 break; 662 } 663 664 if (str.StartsWith(slnFileHeaderNoVersion, StringComparison.Ordinal)) 665 { 666 // Found it. Validate the version. 667 ValidateSolutionFileVersion(str.Substring(slnFileHeaderNoVersion.Length)); 668 return; 669 } 670 } 671 672 // Didn't find the header on either the first or second line, so the solution file 673 // is invalid. 674 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(false, "SubCategoryForSolutionParsingErrors", 675 new BuildEventFileInfo(FullPath), "SolutionParseNoHeaderError"); 676 } 677 678 /// <summary> 679 /// This method parses the Visual Studio version in Dev 12 solution files 680 /// The version line looks like this: 681 /// 682 /// VisualStudioVersion = 12.0.20311.0 VSPRO_PLATFORM 683 /// 684 /// If such a line is found, the version is stored in this.currentVisualStudioVersion 685 /// </summary> ParseVisualStudioVersion(string str)686 private static Version ParseVisualStudioVersion(string str) 687 { 688 Version currentVisualStudioVersion = null; 689 char[] delimiterChars = { ' ', '=' }; 690 string[] words = str.Split(delimiterChars, StringSplitOptions.RemoveEmptyEntries); 691 692 if (words.Length >= 2) 693 { 694 string versionStr = words[1]; 695 if (!System.Version.TryParse(versionStr, out currentVisualStudioVersion)) 696 { 697 currentVisualStudioVersion = null; 698 } 699 } 700 701 return currentVisualStudioVersion; 702 } 703 /// <summary> 704 /// This method extracts the whole part of the version number from the specified line 705 /// containing the solution file format header, and throws an exception if the version number 706 /// is outside of the valid range. 707 /// 708 /// The solution file header looks like this: 709 /// 710 /// Microsoft Visual Studio Solution File, Format Version 9.00 711 /// 712 /// </summary> 713 /// <param name="versionString"></param> ValidateSolutionFileVersion(string versionString)714 private void ValidateSolutionFileVersion(string versionString) 715 { 716 ErrorUtilities.VerifyThrow(versionString != null, "ValidateSolutionFileVersion() got a null line!"); 717 718 Version version = null; 719 720 if (!System.Version.TryParse(versionString, out version)) 721 { 722 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(false, "SubCategoryForSolutionParsingErrors", 723 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseVersionMismatchError", 724 slnFileMinUpgradableVersion, slnFileMaxVersion); 725 } 726 727 _slnFileActualVersion = version.Major; 728 729 // Validate against our min & max 730 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile( 731 _slnFileActualVersion >= slnFileMinUpgradableVersion, 732 "SubCategoryForSolutionParsingErrors", 733 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), 734 "SolutionParseVersionMismatchError", 735 slnFileMinUpgradableVersion, slnFileMaxVersion); 736 // If the solution file version is greater than the maximum one we will create a comment rather than warn 737 // as users such as blend opening a dev10 project cannot do anything about it. 738 if (_slnFileActualVersion > slnFileMaxVersion) 739 { 740 _solutionParserComments.Add(ResourceUtilities.FormatResourceString("UnrecognizedSolutionComment", _slnFileActualVersion)); 741 } 742 } 743 744 /// <summary> 745 /// 746 /// This method processes a "Project" section in the solution file opened by the specified 747 /// StreamReader, and returns a populated ProjectInSolution instance, if successful. 748 /// An exception is thrown if the solution file is invalid. 749 /// 750 /// The format of the parts of a Project section that we care about is as follows: 751 /// 752 /// Project("{Project type GUID}") = "Project name", "Relative path to project file", "{Project GUID}" 753 /// ProjectSection(ProjectDependencies) = postProject 754 /// {Parent project unique name} = {Parent project unique name} 755 /// ... 756 /// EndProjectSection 757 /// EndProject 758 /// 759 /// </summary> 760 /// <param name="firstLine"></param> 761 /// <returns></returns> ParseProject(string firstLine)762 private void ParseProject(string firstLine) 763 { 764 ErrorUtilities.VerifyThrow((firstLine != null) && (firstLine.Length != 0), "ParseProject() got a null firstLine!"); 765 ErrorUtilities.VerifyThrow(_reader != null, "ParseProject() got a null reader!"); 766 767 ProjectInSolution proj = new ProjectInSolution(this); 768 769 // Extract the important information from the first line. 770 ParseFirstProjectLine(firstLine, proj); 771 772 // Search for project dependencies. Keeping reading lines until we either 1.) reach 773 // the end of the file, 2.) see "ProjectSection(ProjectDependencies)" at the beginning 774 // of the line, or 3.) see "EndProject" at the beginning of the line. 775 string line; 776 while ((line = ReadLine()) != null) 777 { 778 // If we see an "EndProject", well ... that's the end of this project! 779 if (line == "EndProject") 780 { 781 break; 782 } 783 else if (line.StartsWith("ProjectSection(ProjectDependencies)", StringComparison.Ordinal)) 784 { 785 // We have a ProjectDependencies section. Each subsequent line should identify 786 // a dependency. 787 line = ReadLine(); 788 while ((line != null) && (!line.StartsWith("EndProjectSection", StringComparison.Ordinal))) 789 { 790 // This should be a dependency. The GUID identifying the parent project should 791 // be both the property name and the property value. 792 Match match = s_crackPropertyLine.Value.Match(line); 793 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors", 794 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseProjectDepGuidError", proj.ProjectName); 795 796 string parentGuid = match.Groups["PROPERTYNAME"].Value.Trim(); 797 proj.AddDependency(parentGuid); 798 799 line = ReadLine(); 800 } 801 } 802 else if (line.StartsWith("ProjectSection(WebsiteProperties)", StringComparison.Ordinal)) 803 { 804 // We have a WebsiteProperties section. This section is present only in Venus 805 // projects, and contains properties that we'll need in order to call the 806 // AspNetCompiler task. 807 line = ReadLine(); 808 while ((line != null) && (!line.StartsWith("EndProjectSection", StringComparison.Ordinal))) 809 { 810 Match match = s_crackPropertyLine.Value.Match(line); 811 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors", 812 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseWebProjectPropertiesError", proj.ProjectName); 813 814 string propertyName = match.Groups["PROPERTYNAME"].Value.Trim(); 815 string propertyValue = match.Groups["PROPERTYVALUE"].Value.Trim(); 816 817 ParseAspNetCompilerProperty(proj, propertyName, propertyValue); 818 819 line = ReadLine(); 820 } 821 } 822 } 823 824 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(line != null, "SubCategoryForSolutionParsingErrors", 825 new BuildEventFileInfo(FullPath), "SolutionParseProjectEofError", proj.ProjectName); 826 827 // Add the project to the collection 828 AddProjectToSolution(proj); 829 // If the project is an etp project then parse the etp project file 830 // to get the projects contained in it. 831 if (IsEtpProjectFile(proj.RelativePath)) 832 { 833 ParseEtpProject(proj); 834 } 835 } // ParseProject() 836 837 /// <summary> 838 /// This method will parse a .etp project recursively and 839 /// add all the projects found to projects and projectsInOrder 840 /// </summary> 841 /// <param name="etpProj">ETP Project</param> ParseEtpProject(ProjectInSolution etpProj)842 internal void ParseEtpProject(ProjectInSolution etpProj) 843 { 844 XmlDocument etpProjectDocument = new XmlDocument(); 845 // Get the full path to the .etp project file 846 string fullPathToEtpProj = Path.Combine(_solutionFileDirectory, etpProj.RelativePath); 847 string etpProjectRelativeDir = Path.GetDirectoryName(etpProj.RelativePath); 848 try 849 { 850 /**************************************************************************** 851 * A Typical .etp project file will look like this 852 *<?xml version="1.0"?> 853 *<EFPROJECT> 854 * <GENERAL> 855 * <BANNER>Microsoft Visual Studio Application Template File</BANNER> 856 * <VERSION>1.00</VERSION> 857 * <Views> 858 * <ProjectExplorer> 859 * <File>ClassLibrary2\ClassLibrary2.csproj</File> 860 * </ProjectExplorer> 861 * </Views> 862 * <References> 863 * <Reference> 864 * <FILE>ClassLibrary2\ClassLibrary2.csproj</FILE> 865 * <GUIDPROJECTID>{73D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FE}</GUIDPROJECTID> 866 * </Reference> 867 * </References> 868 * </GENERAL> 869 *</EFPROJECT> 870 **********************************************************************************/ 871 // Make sure the XML reader ignores DTD processing 872 XmlReaderSettings readerSettings = new XmlReaderSettings(); 873 readerSettings.DtdProcessing = DtdProcessing.Ignore; 874 875 // Load the .etp project file thru the XML reader 876 using (XmlReader xmlReader = XmlReader.Create(fullPathToEtpProj, readerSettings)) 877 { 878 etpProjectDocument.Load(xmlReader); 879 } 880 881 // We need to parse the .etp project file to get the names of projects contained 882 // in the .etp Project. The projects are listed under /EFPROJECT/GENERAL/References/Reference node in the .etp project file. 883 // The /EFPROJECT/GENERAL/Views/ProjectExplorer node will not necessarily contain 884 // all the projects in the .etp project. Therefore, we need to look at 885 // /EFPROJECT/GENERAL/References/Reference. 886 // Find the /EFPROJECT/GENERAL/References/Reference node 887 // Note that this is case sensitive 888 XmlNodeList referenceNodes = etpProjectDocument.DocumentElement.SelectNodes("/EFPROJECT/GENERAL/References/Reference"); 889 // Do the right thing for each <REference> element 890 foreach (XmlNode referenceNode in referenceNodes) 891 { 892 // Get the relative path to the project file 893 string fileElementValue = referenceNode.SelectSingleNode("FILE").InnerText; 894 // If <FILE> element is not present under <Reference> then we don't do anything. 895 if (fileElementValue != null) 896 { 897 // Create and populate a ProjectInSolution for the project 898 ProjectInSolution proj = new ProjectInSolution(this); 899 proj.RelativePath = Path.Combine(etpProjectRelativeDir, fileElementValue); 900 901 // Verify the relative path specified in the .etp proj file 902 ValidateProjectRelativePath(proj); 903 proj.ProjectType = SolutionProjectType.EtpSubProject; 904 proj.ProjectName = proj.RelativePath; 905 XmlNode projGuidNode = referenceNode.SelectSingleNode("GUIDPROJECTID"); 906 if (projGuidNode != null) 907 { 908 proj.ProjectGuid = projGuidNode.InnerText; 909 } 910 // It is ok for a project to not have a guid inside an etp project. 911 // If a solution file contains a project without a guid it fails to 912 // load in Everett. But if an etp project contains a project without 913 // a guid it loads well in Everett and p2p references to/from this project 914 // are preserved. So we should make sure that we don’t error in this 915 // situation while upgrading. 916 else 917 { 918 proj.ProjectGuid = String.Empty; 919 } 920 // Add the recently created proj to the collection of projects 921 AddProjectToSolution(proj); 922 // If the project is an etp project recurse 923 if (IsEtpProjectFile(fileElementValue)) 924 { 925 ParseEtpProject(proj); 926 } 927 } 928 } 929 } 930 // catch all sorts of exceptions - if we encounter any problems here, we just assume the .etp project file is not in the correct format 931 932 // handle security errors 933 catch (SecurityException e) 934 { 935 // Log a warning 936 string errorCode, ignoredKeyword; 937 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded", 938 etpProj.RelativePath, e.Message); 939 _solutionParserWarnings.Add(warning); 940 _solutionParserErrorCodes.Add(errorCode); 941 } 942 // handle errors in path resolution 943 catch (NotSupportedException e) 944 { 945 // Log a warning 946 string errorCode, ignoredKeyword; 947 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded", 948 etpProj.RelativePath, e.Message); 949 _solutionParserWarnings.Add(warning); 950 _solutionParserErrorCodes.Add(errorCode); 951 } 952 // handle errors in loading project file 953 catch (IOException e) 954 { 955 // Log a warning 956 string errorCode, ignoredKeyword; 957 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded", 958 etpProj.RelativePath, e.Message); 959 _solutionParserWarnings.Add(warning); 960 _solutionParserErrorCodes.Add(errorCode); 961 } 962 // handle errors in loading project file 963 catch (UnauthorizedAccessException e) 964 { 965 // Log a warning 966 string errorCode, ignoredKeyword; 967 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded", 968 etpProj.RelativePath, e.Message); 969 _solutionParserWarnings.Add(warning); 970 _solutionParserErrorCodes.Add(errorCode); 971 } 972 // handle XML parsing errors 973 catch (XmlException e) 974 { 975 // Log a warning 976 string errorCode, ignoredKeyword; 977 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.InvalidProjectFile", 978 etpProj.RelativePath, e.Message); 979 _solutionParserWarnings.Add(warning); 980 _solutionParserErrorCodes.Add(errorCode); 981 } 982 } 983 984 /// <summary> 985 /// Adds a given project to the project collections of this class 986 /// </summary> 987 /// <param name="proj">proj</param> AddProjectToSolution(ProjectInSolution proj)988 private void AddProjectToSolution(ProjectInSolution proj) 989 { 990 if (!String.IsNullOrEmpty(proj.ProjectGuid)) 991 { 992 _projects[proj.ProjectGuid] = proj; 993 } 994 _projectsInOrder.Add(proj); 995 } 996 997 /// <summary> 998 /// Checks whether a given project has a .etp extension. 999 /// </summary> 1000 /// <param name="projectFile"></param> IsEtpProjectFile(string projectFile)1001 private bool IsEtpProjectFile(string projectFile) 1002 { 1003 return projectFile.EndsWith(".etp", StringComparison.OrdinalIgnoreCase); 1004 } 1005 1006 /// <summary> 1007 /// Validate relative path of a project 1008 /// </summary> 1009 /// <param name="proj">proj</param> ValidateProjectRelativePath(ProjectInSolution proj)1010 private void ValidateProjectRelativePath(ProjectInSolution proj) 1011 { 1012 // Verify the relative path is not null 1013 ErrorUtilities.VerifyThrow(proj.RelativePath != null, "Project relative path cannot be null."); 1014 1015 // Verify the relative path does not contain invalid characters 1016 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj.RelativePath.IndexOfAny(Path.GetInvalidPathChars()) == -1, 1017 "SubCategoryForSolutionParsingErrors", 1018 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), 1019 "SolutionParseInvalidProjectFileNameCharacters", 1020 proj.ProjectName, proj.RelativePath); 1021 1022 // Verify the relative path is not empty string 1023 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj.RelativePath.Length > 0, 1024 "SubCategoryForSolutionParsingErrors", 1025 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), 1026 "SolutionParseInvalidProjectFileNameEmpty", 1027 proj.ProjectName); 1028 } 1029 1030 /// <summary> 1031 /// Takes a property name / value that comes from the SLN file for a Venus project, and 1032 /// stores it appropriately in our data structures. 1033 /// </summary> 1034 /// <param name="proj"></param> 1035 /// <param name="propertyName"></param> 1036 /// <param name="propertyValue"></param> ParseAspNetCompilerProperty( ProjectInSolution proj, string propertyName, string propertyValue )1037 private void ParseAspNetCompilerProperty 1038 ( 1039 ProjectInSolution proj, 1040 string propertyName, 1041 string propertyValue 1042 ) 1043 { 1044 // What we expect to find in the SLN file is something that looks like this: 1045 // 1046 // Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "c:\...\myfirstwebsite\", "..\..\..\..\..\..\rajeev\temp\websites\myfirstwebsite", "{956CC04E-FD59-49A9-9099-96888CB6F366}" 1047 // ProjectSection(WebsiteProperties) = preProject 1048 // TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" 1049 // ProjectReferences = "{FD705688-88D1-4C22-9BFF-86235D89C2FC}|CSClassLibrary1.dll;{F0726D09-042B-4A7A-8A01-6BED2422BD5D}|VCClassLibrary1.dll;" 1050 // Debug.AspNetCompiler.VirtualPath = "/publishfirst" 1051 // Debug.AspNetCompiler.PhysicalPath = "..\..\..\..\..\..\rajeev\temp\websites\myfirstwebsite\" 1052 // Debug.AspNetCompiler.TargetPath = "..\..\..\..\..\..\rajeev\temp\publishfirst\" 1053 // Debug.AspNetCompiler.ForceOverwrite = "true" 1054 // Debug.AspNetCompiler.Updateable = "true" 1055 // Debug.AspNetCompiler.Enabled = "true" 1056 // Debug.AspNetCompiler.Debug = "true" 1057 // Debug.AspNetCompiler.KeyFile = "" 1058 // Debug.AspNetCompiler.KeyContainer = "" 1059 // Debug.AspNetCompiler.DelaySign = "true" 1060 // Debug.AspNetCompiler.AllowPartiallyTrustedCallers = "true" 1061 // Debug.AspNetCompiler.FixedNames = "true" 1062 // Release.AspNetCompiler.VirtualPath = "/publishfirst" 1063 // Release.AspNetCompiler.PhysicalPath = "..\..\..\..\..\..\rajeev\temp\websites\myfirstwebsite\" 1064 // Release.AspNetCompiler.TargetPath = "..\..\..\..\..\..\rajeev\temp\publishfirst\" 1065 // Release.AspNetCompiler.ForceOverwrite = "true" 1066 // Release.AspNetCompiler.Updateable = "true" 1067 // Release.AspNetCompiler.Enabled = "true" 1068 // Release.AspNetCompiler.Debug = "false" 1069 // Release.AspNetCompiler.KeyFile = "" 1070 // Release.AspNetCompiler.KeyContainer = "" 1071 // Release.AspNetCompiler.DelaySign = "true" 1072 // Release.AspNetCompiler.AllowPartiallyTrustedCallers = "true" 1073 // Release.AspNetCompiler.FixedNames = "true" 1074 // EndProjectSection 1075 // EndProject 1076 // 1077 // This method is responsible for parsing each of the lines within the "WebsiteProperties" section. 1078 // The first component of each property name is actually the configuration for which that 1079 // property applies. 1080 1081 int indexOfFirstDot = propertyName.IndexOf('.'); 1082 if (indexOfFirstDot != -1) 1083 { 1084 // The portion before the first dot is the configuration name. 1085 string configurationName = propertyName.Substring(0, indexOfFirstDot); 1086 1087 // The rest of it is the actual property name. 1088 string aspNetPropertyName = ((propertyName.Length - indexOfFirstDot) > 0) ? propertyName.Substring(indexOfFirstDot + 1, propertyName.Length - indexOfFirstDot - 1) : ""; 1089 1090 // And the part after the <equals> sign is the property value (which was parsed out for us prior 1091 // to calling this method). 1092 propertyValue = TrimQuotes(propertyValue); 1093 1094 // Grab the parameters for this specific configuration if they exist. 1095 object aspNetCompilerParametersObject = proj.AspNetConfigurations[configurationName]; 1096 AspNetCompilerParameters aspNetCompilerParameters; 1097 1098 if (aspNetCompilerParametersObject == null) 1099 { 1100 // If it didn't exist, create a new one. 1101 aspNetCompilerParameters = new AspNetCompilerParameters(); 1102 aspNetCompilerParameters.aspNetVirtualPath = String.Empty; 1103 aspNetCompilerParameters.aspNetPhysicalPath = String.Empty; 1104 aspNetCompilerParameters.aspNetTargetPath = String.Empty; 1105 aspNetCompilerParameters.aspNetForce = String.Empty; 1106 aspNetCompilerParameters.aspNetUpdateable = String.Empty; 1107 aspNetCompilerParameters.aspNetDebug = String.Empty; 1108 aspNetCompilerParameters.aspNetKeyFile = String.Empty; 1109 aspNetCompilerParameters.aspNetKeyContainer = String.Empty; 1110 aspNetCompilerParameters.aspNetDelaySign = String.Empty; 1111 aspNetCompilerParameters.aspNetAPTCA = String.Empty; 1112 aspNetCompilerParameters.aspNetFixedNames = String.Empty; 1113 } 1114 else 1115 { 1116 // Otherwise just unbox it. 1117 aspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParametersObject; 1118 } 1119 1120 // Update the appropriate field within the parameters struct. 1121 if (aspNetPropertyName == "AspNetCompiler.VirtualPath") 1122 { 1123 aspNetCompilerParameters.aspNetVirtualPath = propertyValue; 1124 } 1125 else if (aspNetPropertyName == "AspNetCompiler.PhysicalPath") 1126 { 1127 aspNetCompilerParameters.aspNetPhysicalPath = propertyValue; 1128 } 1129 else if (aspNetPropertyName == "AspNetCompiler.TargetPath") 1130 { 1131 aspNetCompilerParameters.aspNetTargetPath = propertyValue; 1132 } 1133 else if (aspNetPropertyName == "AspNetCompiler.ForceOverwrite") 1134 { 1135 aspNetCompilerParameters.aspNetForce = propertyValue; 1136 } 1137 else if (aspNetPropertyName == "AspNetCompiler.Updateable") 1138 { 1139 aspNetCompilerParameters.aspNetUpdateable = propertyValue; 1140 } 1141 else if (aspNetPropertyName == "AspNetCompiler.Debug") 1142 { 1143 aspNetCompilerParameters.aspNetDebug = propertyValue; 1144 } 1145 else if (aspNetPropertyName == "AspNetCompiler.KeyFile") 1146 { 1147 aspNetCompilerParameters.aspNetKeyFile = propertyValue; 1148 } 1149 else if (aspNetPropertyName == "AspNetCompiler.KeyContainer") 1150 { 1151 aspNetCompilerParameters.aspNetKeyContainer = propertyValue; 1152 } 1153 else if (aspNetPropertyName == "AspNetCompiler.DelaySign") 1154 { 1155 aspNetCompilerParameters.aspNetDelaySign = propertyValue; 1156 } 1157 else if (aspNetPropertyName == "AspNetCompiler.AllowPartiallyTrustedCallers") 1158 { 1159 aspNetCompilerParameters.aspNetAPTCA = propertyValue; 1160 } 1161 else if (aspNetPropertyName == "AspNetCompiler.FixedNames") 1162 { 1163 aspNetCompilerParameters.aspNetFixedNames = propertyValue; 1164 } 1165 1166 // Store the updated parameters struct back into the hashtable by configuration name. 1167 proj.AspNetConfigurations[configurationName] = aspNetCompilerParameters; 1168 } 1169 else 1170 { 1171 // ProjectReferences = "{FD705688-88D1-4C22-9BFF-86235D89C2FC}|CSClassLibrary1.dll;{F0726D09-042B-4A7A-8A01-6BED2422BD5D}|VCClassLibrary1.dll;" 1172 if (string.Compare(propertyName, "ProjectReferences", StringComparison.OrdinalIgnoreCase) == 0) 1173 { 1174 string[] projectReferenceEntries = propertyValue.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); 1175 1176 foreach (string projectReferenceEntry in projectReferenceEntries) 1177 { 1178 int indexOfBar = projectReferenceEntry.IndexOf('|'); 1179 1180 // indexOfBar could be -1 if we had semicolons in the file names, so skip entries that 1181 // don't contain a guid. File names may not contain the '|' character 1182 if (indexOfBar != -1) 1183 { 1184 int indexOfOpeningBrace = projectReferenceEntry.IndexOf('{'); 1185 if (indexOfOpeningBrace != -1) 1186 { 1187 int indexOfClosingBrace = projectReferenceEntry.IndexOf('}', indexOfOpeningBrace); 1188 if (indexOfClosingBrace != -1) 1189 { 1190 string referencedProjectGuid = projectReferenceEntry.Substring(indexOfOpeningBrace, 1191 indexOfClosingBrace - indexOfOpeningBrace + 1); 1192 1193 proj.AddDependency(referencedProjectGuid); 1194 proj.ProjectReferences.Add(referencedProjectGuid); 1195 } 1196 } 1197 } 1198 } 1199 } 1200 else if (String.Compare(propertyName, "TargetFrameworkMoniker", StringComparison.OrdinalIgnoreCase) == 0) 1201 { 1202 //Website project need to back support 3.5 msbuild parser for the Blend (it is not move to .Net4.0 yet.) 1203 //However, 3.5 version of Solution parser can't handle a equal sign in the value. 1204 //The "=" in targetframeworkMoniker was escaped to "%3D" for Orcas 1205 string targetFrameworkMoniker = TrimQuotes(propertyValue); 1206 proj.TargetFrameworkMoniker = Microsoft.Build.Shared.EscapingUtilities.UnescapeAll(targetFrameworkMoniker); 1207 } 1208 } 1209 } 1210 1211 /// <summary> 1212 /// Strips a single pair of leading/trailing double-quotes from a string. 1213 /// </summary> 1214 /// <param name="property"></param> 1215 /// <returns></returns> TrimQuotes( string property )1216 private string TrimQuotes 1217 ( 1218 string property 1219 ) 1220 { 1221 // If the incoming string starts and ends with a double-quote, strip the double-quotes. 1222 if ((property != null) && (property.Length > 0) && (property[0] == '"') && (property[property.Length - 1] == '"')) 1223 { 1224 return property.Substring(1, property.Length - 2); 1225 } 1226 else 1227 { 1228 return property; 1229 } 1230 } 1231 1232 /// <summary> 1233 /// Parse the first line of a Project section of a solution file. This line should look like: 1234 /// 1235 /// Project("{Project type GUID}") = "Project name", "Relative path to project file", "{Project GUID}" 1236 /// 1237 /// </summary> 1238 /// <param name="firstLine"></param> 1239 /// <param name="proj"></param> ParseFirstProjectLine( string firstLine, ProjectInSolution proj )1240 internal void ParseFirstProjectLine 1241 ( 1242 string firstLine, 1243 ProjectInSolution proj 1244 ) 1245 { 1246 Match match = s_crackProjectLine.Value.Match(firstLine); 1247 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors", 1248 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseProjectError"); 1249 1250 string projectTypeGuid = match.Groups["PROJECTTYPEGUID"].Value.Trim(); 1251 proj.ProjectName = match.Groups["PROJECTNAME"].Value.Trim(); 1252 proj.RelativePath = match.Groups["RELATIVEPATH"].Value.Trim(); 1253 proj.ProjectGuid = match.Groups["PROJECTGUID"].Value.Trim(); 1254 1255 // If the project name is empty (as in some bad solutions) set it to some generated generic value. 1256 // This allows us to at least generate reasonable target names etc. instead of crashing. 1257 if (String.IsNullOrEmpty(proj.ProjectName)) 1258 { 1259 proj.ProjectName = "EmptyProjectName." + Guid.NewGuid(); 1260 } 1261 1262 // Validate project relative path 1263 ValidateProjectRelativePath(proj); 1264 1265 // Figure out what type of project this is. 1266 if ((String.Compare(projectTypeGuid, vbProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || 1267 (String.Compare(projectTypeGuid, csProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || 1268 (String.Compare(projectTypeGuid, cpsProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || 1269 (String.Compare(projectTypeGuid, cpsCsProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || 1270 (String.Compare(projectTypeGuid, cpsVbProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || 1271 (String.Compare(projectTypeGuid, fsProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || 1272 (String.Compare(projectTypeGuid, dbProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || 1273 (String.Compare(projectTypeGuid, vjProjectGuid, StringComparison.OrdinalIgnoreCase) == 0)) 1274 { 1275 proj.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat; 1276 } 1277 else if (String.Compare(projectTypeGuid, sharedProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) 1278 { 1279 proj.ProjectType = SolutionProjectType.SharedProject; 1280 } 1281 else if (String.Compare(projectTypeGuid, solutionFolderGuid, StringComparison.OrdinalIgnoreCase) == 0) 1282 { 1283 proj.ProjectType = SolutionProjectType.SolutionFolder; 1284 } 1285 // MSBuild format VC projects have the same project type guid as old style VC projects. 1286 // If it's not an old-style VC project, we'll assume it's MSBuild format 1287 else if (String.Compare(projectTypeGuid, vcProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) 1288 { 1289 if (String.Equals(proj.Extension, ".vcproj", StringComparison.OrdinalIgnoreCase)) 1290 { 1291 if (!_parsingForConversionOnly) 1292 { 1293 ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(FullPath), "ProjectUpgradeNeededToVcxProj", proj.RelativePath); 1294 } 1295 // otherwise, we're parsing this solution file because we want the P2P information during 1296 // conversion, and it's perfectly valid for an unconverted solution file to still contain .vcprojs 1297 } 1298 else 1299 { 1300 proj.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat; 1301 } 1302 } 1303 else if (String.Compare(projectTypeGuid, webProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) 1304 { 1305 proj.ProjectType = SolutionProjectType.WebProject; 1306 _solutionContainsWebProjects = true; 1307 } 1308 else if (String.Compare(projectTypeGuid, wdProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) 1309 { 1310 proj.ProjectType = SolutionProjectType.WebDeploymentProject; 1311 _solutionContainsWebDeploymentProjects = true; 1312 } 1313 else 1314 { 1315 proj.ProjectType = SolutionProjectType.Unknown; 1316 } 1317 } 1318 1319 /// <summary> 1320 /// Read nested projects section. 1321 /// This is required to find a unique name for each project's target 1322 /// </summary> ParseNestedProjects()1323 internal void ParseNestedProjects() 1324 { 1325 string str; 1326 1327 do 1328 { 1329 str = ReadLine(); 1330 if ((str == null) || (str == "EndGlobalSection")) 1331 { 1332 break; 1333 } 1334 1335 if (String.IsNullOrWhiteSpace(str)) 1336 { 1337 continue; 1338 } 1339 1340 Match match = s_crackPropertyLine.Value.Match(str); 1341 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors", 1342 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseNestedProjectError"); 1343 1344 string projectGuid = match.Groups["PROPERTYNAME"].Value.Trim(); 1345 string parentProjectGuid = match.Groups["PROPERTYVALUE"].Value.Trim(); 1346 1347 ProjectInSolution proj; 1348 if (!_projects.TryGetValue(projectGuid, out proj)) 1349 { 1350 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null, "SubCategoryForSolutionParsingErrors", 1351 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseNestedProjectUndefinedError", projectGuid, parentProjectGuid); 1352 } 1353 1354 proj.ParentProjectGuid = parentProjectGuid; 1355 } while (true); 1356 } 1357 1358 /// <summary> 1359 /// Read solution configuration section. 1360 /// </summary> 1361 /// <remarks> 1362 /// A sample section: 1363 /// 1364 /// GlobalSection(SolutionConfigurationPlatforms) = preSolution 1365 /// Debug|Any CPU = Debug|Any CPU 1366 /// Release|Any CPU = Release|Any CPU 1367 /// EndGlobalSection 1368 /// </remarks> ParseSolutionConfigurations()1369 internal void ParseSolutionConfigurations() 1370 { 1371 string str; 1372 char[] nameValueSeparators = new char[] { '=' }; 1373 char[] configPlatformSeparators = new char[] { SolutionConfigurationInSolution.ConfigurationPlatformSeparator }; 1374 1375 do 1376 { 1377 str = ReadLine(); 1378 1379 if ((str == null) || (str == "EndGlobalSection")) 1380 { 1381 break; 1382 } 1383 1384 if (String.IsNullOrWhiteSpace(str)) 1385 { 1386 continue; 1387 } 1388 1389 string[] configurationNames = str.Split(nameValueSeparators); 1390 1391 // There should be exactly one '=' character, separating two names. 1392 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(configurationNames.Length == 2, "SubCategoryForSolutionParsingErrors", 1393 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidSolutionConfigurationEntry", str); 1394 1395 string fullConfigurationName = configurationNames[0].Trim(); 1396 1397 //Fixing bug 555577: Solution file can have description information, in which case we ignore. 1398 if (0 == String.Compare(fullConfigurationName, "DESCRIPTION", StringComparison.OrdinalIgnoreCase)) 1399 continue; 1400 1401 // Both names must be identical 1402 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(fullConfigurationName == configurationNames[1].Trim(), "SubCategoryForSolutionParsingErrors", 1403 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidSolutionConfigurationEntry", str); 1404 1405 string[] configurationPlatformParts = fullConfigurationName.Split(configPlatformSeparators); 1406 1407 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(configurationPlatformParts.Length == 2, "SubCategoryForSolutionParsingErrors", 1408 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidSolutionConfigurationEntry", str); 1409 1410 _solutionConfigurations.Add(new SolutionConfigurationInSolution(configurationPlatformParts[0], configurationPlatformParts[1])); 1411 } while (true); 1412 } 1413 1414 /// <summary> 1415 /// Read project configurations in solution configurations section. 1416 /// </summary> 1417 /// <remarks> 1418 /// A sample (incomplete) section: 1419 /// 1420 /// GlobalSection(ProjectConfigurationPlatforms) = postSolution 1421 /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 1422 /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU 1423 /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = Release|Any CPU 1424 /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = Release|Any CPU 1425 /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Win32.ActiveCfg = Debug|Any CPU 1426 /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg = Release|Win32 1427 /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.ActiveCfg = Release|Win32 1428 /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.Build.0 = Release|Win32 1429 /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.ActiveCfg = Release|Win32 1430 /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.Build.0 = Release|Win32 1431 /// EndGlobalSection 1432 /// </remarks> 1433 /// <returns>An unprocessed hashtable of entries in this section</returns> ParseProjectConfigurations()1434 internal Hashtable ParseProjectConfigurations() 1435 { 1436 Hashtable rawProjectConfigurationsEntries = new Hashtable(StringComparer.OrdinalIgnoreCase); 1437 string str; 1438 1439 do 1440 { 1441 str = ReadLine(); 1442 1443 if ((str == null) || (str == "EndGlobalSection")) 1444 { 1445 break; 1446 } 1447 1448 if (String.IsNullOrWhiteSpace(str)) 1449 { 1450 continue; 1451 } 1452 1453 string[] nameValue = str.Split(new char[] { '=' }); 1454 1455 // There should be exactly one '=' character, separating the name and value. 1456 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(nameValue.Length == 2, "SubCategoryForSolutionParsingErrors", 1457 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidProjectSolutionConfigurationEntry", str); 1458 1459 rawProjectConfigurationsEntries[nameValue[0].Trim()] = nameValue[1].Trim(); 1460 } while (true); 1461 1462 return rawProjectConfigurationsEntries; 1463 } 1464 1465 /// <summary> 1466 /// Read the project configuration information for every project in the solution, using pre-cached 1467 /// solution section data. 1468 /// </summary> 1469 /// <param name="rawProjectConfigurationsEntries">Cached data from the project configuration section</param> ProcessProjectConfigurationSection(Hashtable rawProjectConfigurationsEntries)1470 internal void ProcessProjectConfigurationSection(Hashtable rawProjectConfigurationsEntries) 1471 { 1472 // Instead of parsing the data line by line, we parse it project by project, constructing the 1473 // entry name (e.g. "{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg") and retrieving its 1474 // value from the raw data. The reason for this is that the IDE does it this way, and as the result 1475 // the '.' character is allowed in configuration names although it technically separates different 1476 // parts of the entry name string. This could lead to ambiguous results if we tried to parse 1477 // the entry name instead of constructing it and looking it up. Although it's pretty unlikely that 1478 // this would ever be a problem, it's safer to do it the same way VS IDE does it. 1479 char[] configPlatformSeparators = new char[] { SolutionConfigurationInSolution.ConfigurationPlatformSeparator }; 1480 1481 foreach (ProjectInSolution project in _projectsInOrder) 1482 { 1483 // Solution folders don't have configurations 1484 if (project.ProjectType != SolutionProjectType.SolutionFolder) 1485 { 1486 foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionConfigurations) 1487 { 1488 // The "ActiveCfg" entry defines the active project configuration in the given solution configuration 1489 // This entry must be present for every possible solution configuration/project combination. 1490 string entryNameActiveConfig = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.ActiveCfg", 1491 project.ProjectGuid, solutionConfiguration.FullName); 1492 1493 // The "Build.0" entry tells us whether to build the project configuration in the given solution configuration. 1494 // Technically, it specifies a configuration name of its own which seems to be a remnant of an initial, 1495 // more flexible design of solution configurations (as well as the '.0' suffix - no higher values are ever used). 1496 // The configuration name is not used, and the whole entry means "build the project configuration" 1497 // if it's present in the solution file, and "don't build" if it's not. 1498 string entryNameBuild = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.Build.0", 1499 project.ProjectGuid, solutionConfiguration.FullName); 1500 1501 if (rawProjectConfigurationsEntries.ContainsKey(entryNameActiveConfig)) 1502 { 1503 string[] configurationPlatformParts = ((string)(rawProjectConfigurationsEntries[entryNameActiveConfig])).Split(configPlatformSeparators); 1504 1505 // Project configuration may not necessarily contain the platform part. Some project support only the configuration part. 1506 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(configurationPlatformParts.Length <= 2, "SubCategoryForSolutionParsingErrors", 1507 new BuildEventFileInfo(FullPath), "SolutionParseInvalidProjectSolutionConfigurationEntry", 1508 string.Format(CultureInfo.InvariantCulture, "{0} = {1}", entryNameActiveConfig, rawProjectConfigurationsEntries[entryNameActiveConfig])); 1509 1510 ProjectConfigurationInSolution projectConfiguration = new ProjectConfigurationInSolution( 1511 configurationPlatformParts[0], 1512 (configurationPlatformParts.Length > 1) ? configurationPlatformParts[1] : string.Empty, 1513 rawProjectConfigurationsEntries.ContainsKey(entryNameBuild) 1514 ); 1515 1516 project.SetProjectConfiguration(solutionConfiguration.FullName, projectConfiguration); 1517 } 1518 } 1519 } 1520 } 1521 } 1522 1523 /// <summary> 1524 /// Gets the default configuration name for this solution. Usually it's Debug, unless it's not present 1525 /// in which case it's the first configuration name we find. 1526 /// </summary> GetDefaultConfigurationName()1527 public string GetDefaultConfigurationName() 1528 { 1529 // Have we done this already? Return the cached name 1530 if (_defaultConfigurationName != null) 1531 { 1532 return _defaultConfigurationName; 1533 } 1534 1535 _defaultConfigurationName = string.Empty; 1536 1537 // Pick the Debug configuration as default if present 1538 foreach (SolutionConfigurationInSolution solutionConfiguration in this.SolutionConfigurations) 1539 { 1540 if (string.Compare(solutionConfiguration.ConfigurationName, "Debug", StringComparison.OrdinalIgnoreCase) == 0) 1541 { 1542 _defaultConfigurationName = solutionConfiguration.ConfigurationName; 1543 break; 1544 } 1545 } 1546 1547 // Failing that, just pick the first configuration name as default 1548 if ((_defaultConfigurationName.Length == 0) && (this.SolutionConfigurations.Count > 0)) 1549 { 1550 _defaultConfigurationName = this.SolutionConfigurations[0].ConfigurationName; 1551 } 1552 1553 return _defaultConfigurationName; 1554 } 1555 1556 /// <summary> 1557 /// Gets the default platform name for this solution. Usually it's Mixed Platforms, unless it's not present 1558 /// in which case it's the first platform name we find. 1559 /// </summary> GetDefaultPlatformName()1560 public string GetDefaultPlatformName() 1561 { 1562 // Have we done this already? Return the cached name 1563 if (_defaultPlatformName != null) 1564 { 1565 return _defaultPlatformName; 1566 } 1567 1568 _defaultPlatformName = string.Empty; 1569 1570 // Pick the Mixed Platforms platform as default if present 1571 foreach (SolutionConfigurationInSolution solutionConfiguration in this.SolutionConfigurations) 1572 { 1573 if (string.Compare(solutionConfiguration.PlatformName, "Mixed Platforms", StringComparison.OrdinalIgnoreCase) == 0) 1574 { 1575 _defaultPlatformName = solutionConfiguration.PlatformName; 1576 break; 1577 } 1578 // We would like this to be chosen if Mixed platforms does not exist. 1579 else if (string.Compare(solutionConfiguration.PlatformName, "Any CPU", StringComparison.OrdinalIgnoreCase) == 0) 1580 { 1581 _defaultPlatformName = solutionConfiguration.PlatformName; 1582 } 1583 } 1584 1585 // Failing that, just pick the first platform name as default 1586 if ((_defaultPlatformName.Length == 0) && (this.SolutionConfigurations.Count > 0)) 1587 { 1588 _defaultPlatformName = this.SolutionConfigurations[0].PlatformName; 1589 } 1590 1591 return _defaultPlatformName; 1592 } 1593 1594 /// <summary> 1595 /// This method takes a string representing one of the project's unique names (guid), and 1596 /// returns the corresponding "friendly" name for this project. 1597 /// </summary> 1598 /// <param name="projectGuid"></param> 1599 /// <returns></returns> GetProjectUniqueNameByGuid(string projectGuid)1600 internal string GetProjectUniqueNameByGuid(string projectGuid) 1601 { 1602 ProjectInSolution proj; 1603 if (_projects.TryGetValue(projectGuid, out proj)) 1604 { 1605 return proj.GetUniqueProjectName(); 1606 } 1607 1608 return null; 1609 } 1610 1611 /// <summary> 1612 /// This method takes a string representing one of the project's unique names (guid), and 1613 /// returns the corresponding relative path to this project. 1614 /// </summary> 1615 /// <param name="projectGuid"></param> 1616 /// <returns></returns> GetProjectRelativePathByGuid(string projectGuid)1617 internal string GetProjectRelativePathByGuid(string projectGuid) 1618 { 1619 ProjectInSolution proj; 1620 if (_projects.TryGetValue(projectGuid, out proj)) 1621 { 1622 return proj.RelativePath; 1623 } 1624 1625 return null; 1626 } 1627 1628 #endregion 1629 } // class SolutionParser 1630 } // namespace Microsoft.Build.Construction 1631