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 // </copyright> 5 // <summary>Definition of ProjectRootElement class.</summary> 6 //----------------------------------------------------------------------- 7 8 using System; 9 using System.Collections.Generic; 10 using System.Diagnostics; 11 using System.Globalization; 12 using System.IO; 13 using System.Linq; 14 using System.Runtime.CompilerServices; 15 using System.Text; 16 using System.Text.RegularExpressions; 17 using System.Threading; 18 using System.Xml; 19 using Microsoft.Build.BackEnd.Logging; 20 using Microsoft.Build.Collections; 21 using Microsoft.Build.Evaluation; 22 using Microsoft.Build.Framework; 23 using Microsoft.Build.Shared; 24 using Microsoft.Build.Internal; 25 #if (!STANDALONEBUILD) 26 using Microsoft.Internal.Performance; 27 #if MSBUILDENABLEVSPROFILING 28 using Microsoft.VisualStudio.Profiler; 29 #endif 30 #endif 31 using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; 32 using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; 33 using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; 34 35 namespace Microsoft.Build.Construction 36 { 37 /// <summary> 38 /// Event handler for the event fired after this project file is named or renamed. 39 /// If the project file has not previously had a name, oldFullPath is null. 40 /// </summary> RenameHandlerDelegate(string oldFullPath)41 internal delegate void RenameHandlerDelegate(string oldFullPath); 42 43 /// <summary> 44 /// ProjectRootElement class represents an MSBuild project, an MSBuild targets file or any other file that conforms to MSBuild 45 /// project file schema. 46 /// This class and its related classes allow a complete MSBuild project or targets file to be read and written. 47 /// Comments and whitespace cannot be edited through this model at present. 48 /// 49 /// Each project root element is associated with exactly one ProjectCollection. This allows the owner of that project collection 50 /// to control its lifetime and not be surprised by edits via another project collection. 51 /// </summary> 52 [DebuggerDisplay("{FullPath} #Children={Count} DefaultTargets={DefaultTargets} ToolsVersion={ToolsVersion} InitialTargets={InitialTargets} ExplicitlyLoaded={IsExplicitlyLoaded}")] 53 public class ProjectRootElement : ProjectElementContainer 54 { 55 /// Constants for default (empty) project file. 56 private const string EmptyProjectFileContent = "{0}<Project{1}{2}>\r\n</Project>"; 57 private const string EmptyProjectFileXmlDeclaration = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"; 58 private const string EmptyProjectFileToolsVersion = " ToolsVersion=\"" + MSBuildConstants.CurrentToolsVersion + "\""; 59 private const string EmptyProjectFileXmlNamespace = " xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\""; 60 61 /// <summary> 62 /// The singleton delegate that loads projects into the ProjectRootElement 63 /// </summary> 64 private static readonly ProjectRootElementCache.OpenProjectRootElement s_openLoaderDelegate = OpenLoader; 65 66 private static readonly ProjectRootElementCache.OpenProjectRootElement s_openLoaderPreserveFormattingDelegate = OpenLoaderPreserveFormatting; 67 68 /// <summary> 69 /// Used to determine if a file is an empty XML file if it ONLY contains an XML declaration like <?xml version="1.0" encoding="utf-8"?>. 70 /// </summary> 71 private static readonly Lazy<Regex> XmlDeclarationRegEx = new Lazy<Regex>(() => new Regex(@"\A\s*\<\?\s*xml.*\?\>\s*\Z"), isThreadSafe: true); 72 73 /// <summary> 74 /// The default encoding to use / assume for a new project. 75 /// </summary> 76 private static readonly Encoding s_defaultEncoding = Encoding.UTF8; 77 78 /// <summary> 79 /// A global counter used to ensure each project version is distinct from every other. 80 /// </summary> 81 /// <remarks> 82 /// This number is static so that it is unique across the appdomain. That is so that a host 83 /// can know when a ProjectRootElement has been unloaded (perhaps after modification) and 84 /// reloaded -- the version won't reset to '0'. 85 /// </remarks> 86 private static int s_globalVersionCounter; 87 88 /// <summary> 89 /// Version number of this object that was last saved to disk, or last loaded from disk. 90 /// Used to figure whether this object is dirty for saving. 91 /// Saving to or loading from a provided stream reader does not modify this value, only saving to or loading from disk. 92 /// The actual value is meaningless (since the counter is shared with all projects) -- 93 /// it should only be compared to a stored value. 94 /// Immediately after loading from disk, this has the same value as <see cref="_version">version</see>. 95 /// </summary> 96 private int _versionOnDisk; 97 98 /// <summary> 99 /// Current version number of this object. 100 /// Used to figure whether this object is dirty for saving, or projects evaluated from 101 /// this object need to be re-evaluated. 102 /// The actual value is meaningless (since the counter is shared with all projects) -- 103 /// it should only be compared to a stored value. 104 /// </summary> 105 /// <remarks> 106 /// Set this only through <see cref="MarkDirty(string, string)"/>. 107 /// </remarks> 108 private int _version; 109 110 /// <summary> 111 /// The encoding of the project that was (if applicable) loaded off disk, and that will be used to save the project. 112 /// </summary> 113 /// <value>Defaults to UTF8 for new projects.</value> 114 private Encoding _encoding; 115 116 /// <summary> 117 /// XML namespace specified and used by this project file. If a namespace was not specified in the project file, this 118 /// value will be string.Empty. 119 /// </summary> 120 internal string XmlNamespace { get; set; } 121 122 /// <summary> 123 /// The project file's location. It can be null if the project is not directly loaded from a file. 124 /// </summary> 125 private ElementLocation _projectFileLocation; 126 127 /// <summary> 128 /// The directory that the project is in. 129 /// Essential for evaluating relative paths. 130 /// If the project is not loaded from disk, returns the current-directory from 131 /// the time the project was loaded - this is the same behavior as Whidbey/Orcas. 132 /// </summary> 133 private string _directory; 134 135 /// <summary> 136 /// The time that this object was last changed. If it hasn't 137 /// been changed since being loaded or created, its value is <see cref="DateTime.MinValue"/>. 138 /// Stored as UTC as this is faster when there are a large number of rapid edits. 139 /// </summary> 140 private DateTime _timeLastChangedUtc; 141 142 /// <summary> 143 /// The last-write-time of the file that was read, when it was read. 144 /// This can be used to see whether the file has been changed on disk 145 /// by an external means. 146 /// </summary> 147 private DateTime _lastWriteTimeWhenRead; 148 149 /// <summary> 150 /// The cache in which this project root element is stored. 151 /// </summary> 152 private readonly ProjectRootElementCache _projectRootElementCache; 153 154 /// <summary> 155 /// Reason it was last marked dirty; unlocalized, for debugging 156 /// </summary> 157 private string _dirtyReason = "first created project {0}"; 158 159 /// <summary> 160 /// Parameter to be formatted into the dirty reason 161 /// </summary> 162 private string _dirtyParameter = String.Empty; 163 164 /// <summary> 165 /// Initialize a ProjectRootElement instance from a XmlReader. 166 /// May throw InvalidProjectFileException. 167 /// Leaves the project dirty, indicating there are unsaved changes. 168 /// Used to create a root element for solutions loaded by the 3.5 version of the solution wrapper. 169 /// </summary> ProjectRootElement(XmlReader xmlReader, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded, bool preserveFormatting)170 internal ProjectRootElement(XmlReader xmlReader, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded, 171 bool preserveFormatting) 172 : base() 173 { 174 ErrorUtilities.VerifyThrowArgumentNull(xmlReader, "xmlReader"); 175 ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache"); 176 177 this.IsExplicitlyLoaded = isExplicitlyLoaded; 178 _projectRootElementCache = projectRootElementCache; 179 _directory = NativeMethodsShared.GetCurrentDirectory(); 180 IncrementVersion(); 181 182 XmlDocumentWithLocation document = LoadDocument(xmlReader, preserveFormatting); 183 184 ProjectParser.Parse(document, this); 185 } 186 187 /// <summary> 188 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. 189 /// Leaves the project dirty, indicating there are unsaved changes. 190 /// </summary> ProjectRootElement(ProjectRootElementCache projectRootElementCache, NewProjectFileOptions projectFileOptions)191 private ProjectRootElement(ProjectRootElementCache projectRootElementCache, NewProjectFileOptions projectFileOptions) 192 { 193 ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache"); 194 195 _projectRootElementCache = projectRootElementCache; 196 _directory = NativeMethodsShared.GetCurrentDirectory(); 197 IncrementVersion(); 198 199 XmlDocumentWithLocation document = new XmlDocumentWithLocation(); 200 201 XmlReaderSettings xrs = new XmlReaderSettings(); 202 xrs.DtdProcessing = DtdProcessing.Ignore; 203 204 var emptyProjectFile = string.Format(EmptyProjectFileContent, 205 (projectFileOptions & NewProjectFileOptions.IncludeXmlDeclaration) != 0 ? EmptyProjectFileXmlDeclaration : string.Empty, 206 (projectFileOptions & NewProjectFileOptions.IncludeToolsVersion) != 0 ? EmptyProjectFileToolsVersion : string.Empty, 207 (projectFileOptions & NewProjectFileOptions.IncludeXmlNamespace) != 0 ? EmptyProjectFileXmlNamespace : string.Empty); 208 209 using (XmlReader xr = XmlReader.Create(new StringReader(emptyProjectFile), xrs)) 210 { 211 document.Load(xr); 212 } 213 214 ProjectParser.Parse(document, this); 215 } 216 217 /// <summary> 218 /// Initialize a ProjectRootElement instance over a project with the specified file path. 219 /// Assumes path is already normalized. 220 /// May throw InvalidProjectFileException. 221 /// </summary> ProjectRootElement(string path, ProjectRootElementCache projectRootElementCache, bool preserveFormatting)222 private ProjectRootElement(string path, ProjectRootElementCache projectRootElementCache, 223 bool preserveFormatting) 224 : base() 225 { 226 ErrorUtilities.VerifyThrowArgumentLength(path, "path"); 227 ErrorUtilities.VerifyThrowInternalRooted(path); 228 ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache"); 229 _projectRootElementCache = projectRootElementCache; 230 231 IncrementVersion(); 232 _versionOnDisk = _version; 233 _timeLastChangedUtc = DateTime.UtcNow; 234 235 XmlDocumentWithLocation document = LoadDocument(path, preserveFormatting); 236 237 ProjectParser.Parse(document, this); 238 239 projectRootElementCache.AddEntry(this); 240 } 241 242 /// <summary> 243 /// Initialize a ProjectRootElement instance from an existing document. 244 /// May throw InvalidProjectFileException. 245 /// Leaves the project dirty, indicating there are unsaved changes. 246 /// </summary> 247 /// <remarks> 248 /// Do not make public: we do not wish to expose particular XML API's. 249 /// </remarks> ProjectRootElement(XmlDocumentWithLocation document, ProjectRootElementCache projectRootElementCache)250 private ProjectRootElement(XmlDocumentWithLocation document, ProjectRootElementCache projectRootElementCache) 251 : base() 252 { 253 ErrorUtilities.VerifyThrowArgumentNull(document, "document"); 254 ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache"); 255 256 _projectRootElementCache = projectRootElementCache; 257 _directory = NativeMethodsShared.GetCurrentDirectory(); 258 IncrementVersion(); 259 260 ProjectParser.Parse(document, this); 261 } 262 263 /// <summary> 264 /// Initialize a ProjectRootElement instance from an existing document. 265 /// Helper constructor for the <see cref="ReloadFrom(string,bool,System.Nullable{bool})"/>> mehtod which needs to check if the document parses 266 /// </summary> 267 /// <remarks> 268 /// Do not make public: we do not wish to expose particular XML API's. 269 /// </remarks> ProjectRootElement(XmlDocumentWithLocation document)270 private ProjectRootElement(XmlDocumentWithLocation document) 271 : base() 272 { 273 ProjectParser.Parse(document, this); 274 } 275 276 /// <summary> 277 /// Event raised after this project is renamed 278 /// </summary> 279 internal event RenameHandlerDelegate OnAfterProjectRename; 280 281 /// <summary> 282 /// Event raised after the project XML is changed. 283 /// </summary> 284 internal event EventHandler<ProjectXmlChangedEventArgs> OnProjectXmlChanged; 285 286 /// <summary> 287 /// Condition should never be set, but the getter returns null instead of throwing 288 /// because a nonexistent condition is implicitly true 289 /// </summary> 290 public override string Condition 291 { 292 get 293 { 294 return null; 295 } 296 297 set 298 { 299 ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition"); 300 } 301 } 302 303 #region ChildEnumerators 304 /// <summary> 305 /// Get a read-only collection of the child chooses, if any 306 /// </summary> 307 /// <remarks> 308 /// The name is inconsistent to make it more understandable, per API review. 309 /// </remarks> 310 public ICollection<ProjectChooseElement> ChooseElements => new ReadOnlyCollection<ProjectChooseElement> 311 ( 312 new FilteringEnumerable<ProjectElement, ProjectChooseElement>(Children) 313 ); 314 315 /// <summary> 316 /// Get a read-only collection of the child item definition groups, if any 317 /// </summary> 318 public ICollection<ProjectItemDefinitionGroupElement> ItemDefinitionGroups => new ReadOnlyCollection<ProjectItemDefinitionGroupElement> 319 ( 320 new FilteringEnumerable<ProjectElement, ProjectItemDefinitionGroupElement>(Children) 321 ); 322 323 /// <summary> 324 /// Get a read-only collection of the child item definitions, if any, in all item definition groups anywhere in the project file. 325 /// </summary> 326 public ICollection<ProjectItemDefinitionElement> ItemDefinitions => new ReadOnlyCollection<ProjectItemDefinitionElement> 327 ( 328 new FilteringEnumerable<ProjectElement, ProjectItemDefinitionElement>(AllChildren) 329 ); 330 331 /// <summary> 332 /// Get a read-only collection over the child item groups, if any. 333 /// Does not include any that may not be at the root, i.e. inside Choose elements. 334 /// </summary> 335 public ICollection<ProjectItemGroupElement> ItemGroups => new ReadOnlyCollection<ProjectItemGroupElement> 336 ( 337 new FilteringEnumerable<ProjectElement, ProjectItemGroupElement>(Children) 338 ); 339 340 /// <summary> 341 /// Get a read-only collection of the child items, if any, in all item groups anywhere in the project file. 342 /// Not restricted to root item groups: traverses through Choose elements. 343 /// </summary> 344 public ICollection<ProjectItemElement> Items => new ReadOnlyCollection<ProjectItemElement> 345 ( 346 new FilteringEnumerable<ProjectElement, ProjectItemElement>(AllChildren) 347 ); 348 349 /// <summary> 350 /// Get a read-only collection of the child import groups, if any. 351 /// </summary> 352 public ICollection<ProjectImportGroupElement> ImportGroups => new ReadOnlyCollection<ProjectImportGroupElement> 353 ( 354 new FilteringEnumerable<ProjectElement, ProjectImportGroupElement>(Children) 355 ); 356 357 /// <summary> 358 /// Get a read-only collection of the child imports 359 /// </summary> 360 public ICollection<ProjectImportElement> Imports => new ReadOnlyCollection<ProjectImportElement> 361 ( 362 new FilteringEnumerable<ProjectElement, ProjectImportElement>(AllChildren) 363 ); 364 365 /// <summary> 366 /// Get a read-only collection of the child property groups, if any. 367 /// Does not include any that may not be at the root, i.e. inside Choose elements. 368 /// </summary> 369 public ICollection<ProjectPropertyGroupElement> PropertyGroups => new ReadOnlyCollection<ProjectPropertyGroupElement> 370 ( 371 new FilteringEnumerable<ProjectElement, ProjectPropertyGroupElement>(Children) 372 ); 373 374 /// <summary> 375 /// Geta read-only collection of the child properties, if any, in all property groups anywhere in the project file. 376 /// Not restricted to root property groups: traverses through Choose elements. 377 /// </summary> 378 public ICollection<ProjectPropertyElement> Properties => new ReadOnlyCollection<ProjectPropertyElement> 379 ( 380 new FilteringEnumerable<ProjectElement, ProjectPropertyElement>(AllChildren) 381 ); 382 383 /// <summary> 384 /// Get a read-only collection of the child targets 385 /// </summary> 386 public ICollection<ProjectTargetElement> Targets => new ReadOnlyCollection<ProjectTargetElement> 387 ( 388 new FilteringEnumerable<ProjectElement, ProjectTargetElement>(Children) 389 ); 390 391 /// <summary> 392 /// Get a read-only collection of the child usingtasks, if any 393 /// </summary> 394 public ICollection<ProjectUsingTaskElement> UsingTasks => new ReadOnlyCollection<ProjectUsingTaskElement> 395 ( 396 new FilteringEnumerable<ProjectElement, ProjectUsingTaskElement>(Children) 397 ); 398 399 /// <summary> 400 /// Get a read-only collection of the child item groups, if any, in reverse order 401 /// </summary> 402 public ICollection<ProjectItemGroupElement> ItemGroupsReversed => new ReadOnlyCollection<ProjectItemGroupElement> 403 ( 404 new FilteringEnumerable<ProjectElement, ProjectItemGroupElement>(ChildrenReversed) 405 ); 406 407 /// <summary> 408 /// Get a read-only collection of the child item definition groups, if any, in reverse order 409 /// </summary> 410 public ICollection<ProjectItemDefinitionGroupElement> ItemDefinitionGroupsReversed => new ReadOnlyCollection<ProjectItemDefinitionGroupElement> 411 ( 412 new FilteringEnumerable<ProjectElement, ProjectItemDefinitionGroupElement>(ChildrenReversed) 413 ); 414 415 /// <summary> 416 /// Get a read-only collection of the child import groups, if any, in reverse order 417 /// </summary> 418 public ICollection<ProjectImportGroupElement> ImportGroupsReversed => new ReadOnlyCollection<ProjectImportGroupElement> 419 ( 420 new FilteringEnumerable<ProjectElement, ProjectImportGroupElement>(ChildrenReversed) 421 ); 422 423 /// <summary> 424 /// Get a read-only collection of the child property groups, if any, in reverse order 425 /// </summary> 426 public ICollection<ProjectPropertyGroupElement> PropertyGroupsReversed => new ReadOnlyCollection<ProjectPropertyGroupElement> 427 ( 428 new FilteringEnumerable<ProjectElement, ProjectPropertyGroupElement>(ChildrenReversed) 429 ); 430 431 #endregion 432 433 /// <summary> 434 /// The directory that the project is in. 435 /// Essential for evaluating relative paths. 436 /// Is never null, even if the FullPath does not contain directory information. 437 /// If the project has not been loaded from disk and has not been given a path, returns the current-directory from 438 /// the time the project was loaded - this is the same behavior as Whidbey/Orcas. 439 /// If the project has not been loaded from disk but has been given a path, this path may not exist. 440 /// </summary> 441 public string DirectoryPath 442 { 443 [DebuggerStepThrough] get { return _directory ?? String.Empty; } 444 internal set { _directory = value; } 445 // Used during solution load to ensure solutions which were created from a file have a location. 446 } 447 448 /// <summary> 449 /// Full path to the project file. 450 /// If the project has not been loaded from disk and has not been given a path, returns null. 451 /// If the project has not been loaded from disk but has been given a path, this path may not exist. 452 /// Setter renames the project, if it already had a name. 453 /// </summary> 454 /// <remarks> 455 /// Updates the ProjectRootElement cache. 456 /// </remarks> 457 public string FullPath 458 { 459 get 460 { 461 return _projectFileLocation?.File; 462 } 463 464 set 465 { 466 ErrorUtilities.VerifyThrowArgumentLength(value, "value"); 467 468 string oldFullPath = _projectFileLocation?.File; 469 470 // We do not control the current directory at this point, but assume that if we were 471 // passed a relative path, the caller assumes we will prepend the current directory. 472 string newFullPath = FileUtilities.NormalizePath(value); 473 474 if (String.Equals(oldFullPath, newFullPath, StringComparison.OrdinalIgnoreCase)) 475 { 476 return; 477 } 478 479 _projectFileLocation = ElementLocation.Create(newFullPath); 480 _directory = Path.GetDirectoryName(newFullPath); 481 482 if (XmlDocument != null) 483 { 484 XmlDocument.FullPath = newFullPath; 485 } 486 487 if (oldFullPath == null) 488 { 489 _projectRootElementCache.AddEntry(this); 490 } 491 else 492 { 493 _projectRootElementCache.RenameEntry(oldFullPath, this); 494 } 495 496 OnAfterProjectRename?.Invoke(oldFullPath); 497 498 MarkDirty("Set project FullPath to '{0}'", FullPath); 499 } 500 } 501 502 /// <summary> 503 /// Encoding that the project file is saved in, or will be saved in, unless 504 /// otherwise specified. 505 /// </summary> 506 /// <remarks> 507 /// Returns the encoding from the Xml declaration if any, otherwise UTF8. 508 /// </remarks> 509 public Encoding Encoding 510 { 511 get 512 { 513 // No thread-safety lock required here because many reader threads would set the same value to the field. 514 if (_encoding == null) 515 { 516 XmlDeclaration declaration = XmlDocument.FirstChild as XmlDeclaration; 517 518 if (declaration?.Encoding.Length > 0) 519 { 520 _encoding = Encoding.GetEncoding(declaration.Encoding); 521 } 522 } 523 524 // Ensure we never return null, in case there was no xml declaration that we could find above. 525 return _encoding ?? s_defaultEncoding; 526 } 527 } 528 529 /// <summary> 530 /// Gets or sets the value of DefaultTargets. If there is no DefaultTargets, returns empty string. 531 /// If the value is null or empty, removes the attribute. 532 /// </summary> 533 public string DefaultTargets 534 { 535 [DebuggerStepThrough] 536 get 537 { 538 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.defaultTargets); 539 } 540 541 [DebuggerStepThrough] 542 set 543 { 544 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.defaultTargets, value); 545 MarkDirty("Set Project DefaultTargets to '{0}'", value); 546 } 547 } 548 549 /// <summary> 550 /// Gets or sets the value of InitialTargets. If there is no InitialTargets, returns empty string. 551 /// If the value is null or empty, removes the attribute. 552 /// </summary> 553 public string InitialTargets 554 { 555 [DebuggerStepThrough] 556 get 557 { 558 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.initialTargets); 559 } 560 561 [DebuggerStepThrough] 562 set 563 { 564 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.initialTargets, value); 565 MarkDirty("Set project InitialTargets to '{0}'", value); 566 } 567 } 568 569 /// <summary> 570 /// Gets or sets a semicolon delimited list of software development kits (SDK) that the project uses. 571 /// If a value is specified, an Sdk.props is simplicity imported at the top of the project and an 572 /// Sdk.targets is simplicity imported at the bottom from the specified SDK. 573 /// If the value is null or empty, removes the attribute. 574 /// </summary> 575 public string Sdk 576 { 577 [DebuggerStepThrough] 578 get 579 { 580 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.sdk); 581 } 582 583 [DebuggerStepThrough] 584 set 585 { 586 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.sdk, value); 587 MarkDirty("Set project Sdk to '{0}'", value); 588 } 589 } 590 591 /// <summary> 592 /// Gets or sets the value of TreatAsLocalProperty. If there is no tag, returns empty string. 593 /// If the value being set is null or empty, removes the attribute. 594 /// </summary> 595 public string TreatAsLocalProperty 596 { 597 [DebuggerStepThrough] 598 get 599 { 600 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.treatAsLocalProperty); 601 } 602 603 [DebuggerStepThrough] 604 set 605 { 606 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.treatAsLocalProperty, value); 607 MarkDirty("Set project TreatAsLocalProperty to '{0}'", value); 608 } 609 } 610 611 /// <summary> 612 /// Gets or sets the value of ToolsVersion. If there is no ToolsVersion, returns empty string. 613 /// If the value is null or empty, removes the attribute. 614 /// </summary> 615 public string ToolsVersion 616 { 617 [DebuggerStepThrough] 618 get 619 { 620 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.toolsVersion); 621 } 622 623 [DebuggerStepThrough] 624 set 625 { 626 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.toolsVersion, value); 627 MarkDirty("Set project ToolsVersion {0}", value); 628 } 629 } 630 631 /// <summary> 632 /// Gets the XML representing this project as a string. 633 /// Does not remove any dirty flag. 634 /// </summary> 635 /// <remarks> 636 /// Useful for debugging. 637 /// Note that we do not expose an XmlDocument or any other specific XML API. 638 /// </remarks> 639 public string RawXml 640 { 641 get 642 { 643 using (StringWriter stringWriter = new EncodingStringWriter(Encoding)) 644 { 645 using (ProjectWriter projectWriter = new ProjectWriter(stringWriter)) 646 { 647 projectWriter.Initialize(XmlDocument); 648 XmlDocument.Save(projectWriter); 649 } 650 651 return stringWriter.ToString(); 652 } 653 } 654 } 655 656 /// <summary> 657 /// Whether the XML has been modified since it was last loaded or saved. 658 /// </summary> 659 public bool HasUnsavedChanges => Version != _versionOnDisk; 660 661 /// <summary> 662 /// Whether the XML is preserving formatting or not. 663 /// </summary> 664 public bool PreserveFormatting => XmlDocument?.PreserveWhitespace ?? false; 665 666 /// <summary> 667 /// Version number of this object. 668 /// A host can compare this to a stored version number to determine whether 669 /// a project's XML has changed, even if it has also been saved since. 670 /// 671 /// The actual value is meaningless: an edit may increment it more than once, 672 /// so it should only be compared to a stored value. 673 /// </summary> 674 /// <remarks> 675 /// Used by the Project class to figure whether changes have occurred that 676 /// it might want to pick up by reevaluation. 677 /// 678 /// Used by the ProjectRootElement class to determine whether it needs to save. 679 /// 680 /// This number is unique to the appdomain. That means that it is possible 681 /// to know when a ProjectRootElement has been unloaded (perhaps after modification) and 682 /// reloaded -- the version won't reset to '0'. 683 /// 684 /// We're assuming we don't have over 2 billion edits. 685 /// </remarks> 686 public int Version => _version; 687 688 /// <summary> 689 /// The time that this object was last changed. If it hasn't 690 /// been changed since being loaded or created, its value is <see cref="DateTime.MinValue"/>. 691 /// </summary> 692 /// <remarks> 693 /// This is used by the VB/C# project system. 694 /// </remarks> 695 public DateTime TimeLastChanged => _timeLastChangedUtc.ToLocalTime(); 696 697 /// <summary> 698 /// The last-write-time of the file that was read, when it was read. 699 /// This can be used to see whether the file has been changed on disk 700 /// by an external means. 701 /// </summary> 702 public DateTime LastWriteTimeWhenRead => _lastWriteTimeWhenRead; 703 704 /// <summary> 705 /// This does not allow conditions, so it should not be called. 706 /// </summary> 707 public override ElementLocation ConditionLocation 708 { 709 get 710 { 711 ErrorUtilities.ThrowInternalError("Should not evaluate this"); 712 return null; 713 } 714 } 715 716 /// <summary> 717 /// Location of the originating file itself, not any specific content within it. 718 /// If the file has not been given a name, returns an empty location. 719 /// This is a case where it is legitimate to "not have a location". 720 /// </summary> 721 public ElementLocation ProjectFileLocation => _projectFileLocation ?? ElementLocation.EmptyLocation; 722 723 /// <summary> 724 /// Location of the toolsversion attribute, if any 725 /// </summary> 726 public ElementLocation ToolsVersionLocation => XmlElement.GetAttributeLocation(XMakeAttributes.toolsVersion); 727 728 /// <summary> 729 /// Location of the defaulttargets attribute, if any 730 /// </summary> 731 public ElementLocation DefaultTargetsLocation => XmlElement.GetAttributeLocation(XMakeAttributes.defaultTargets); 732 733 /// <summary> 734 /// Location of the initialtargets attribute, if any 735 /// </summary> 736 public ElementLocation InitialTargetsLocation => XmlElement.GetAttributeLocation(XMakeAttributes.initialTargets); 737 738 /// <summary> 739 /// Location of the Sdk attribute, if any 740 /// </summary> 741 public ElementLocation SdkLocation => XmlElement.GetAttributeLocation(XMakeAttributes.sdk); 742 743 /// <summary> 744 /// Location of the TreatAsLocalProperty attribute, if any 745 /// </summary> 746 public ElementLocation TreatAsLocalPropertyLocation 747 => XmlElement.GetAttributeLocation(XMakeAttributes.treatAsLocalProperty); 748 749 /// <summary> 750 /// Has the project root element been explicitly loaded for a build or has it been implicitly loaded 751 /// as part of building another project. 752 /// </summary> 753 /// <remarks> 754 /// Internal code that wants to set this to true should call <see cref="MarkAsExplicitlyLoaded"/>. 755 /// The setter is private to make it more difficult to downgrade an existing PRE to an implicitly loaded state, which should never happen. 756 /// </remarks> 757 internal bool IsExplicitlyLoaded 758 { 759 get; 760 private set; 761 } 762 763 /// <summary> 764 /// Retrieves the root element cache with which this root element is associated. 765 /// </summary> 766 internal ProjectRootElementCache ProjectRootElementCache => _projectRootElementCache; 767 768 /// <summary> 769 /// Gets a value indicating whether this PRE is known by its containing collection. 770 /// </summary> 771 internal bool IsMemberOfProjectCollection => _projectFileLocation != null; 772 773 /// <summary> 774 /// Indicates whether there are any targets in this project 775 /// that use the "Returns" attribute. If so, then this project file 776 /// is automatically assumed to be "Returns-enabled", and the default behavior 777 /// for targets without Returns attributes changes from using the Outputs to 778 /// returning nothing by default. 779 /// </summary> 780 internal bool ContainsTargetsWithReturnsAttribute 781 { 782 get; 783 set; 784 } 785 786 /// <summary> 787 /// Gets the ProjectExtensions child, if any, otherwise null. 788 /// </summary> 789 /// <remarks> 790 /// Not public as we do not wish to encourage the use of ProjectExtensions. 791 /// </remarks> 792 internal ProjectExtensionsElement ProjectExtensions 793 => ChildrenReversed.OfType<ProjectExtensionsElement>().FirstOrDefault(); 794 795 /// <summary> 796 /// Returns an unlocalized indication of how this file was last dirtied. 797 /// This is for debugging purposes only. 798 /// String formatting only occurs when retrieved. 799 /// </summary> 800 internal string LastDirtyReason 801 => _dirtyReason == null ? null : String.Format(CultureInfo.InvariantCulture, _dirtyReason, _dirtyParameter); 802 803 /// <summary> 804 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. 805 /// Uses the global project collection. 806 /// </summary> Create()807 public static ProjectRootElement Create() 808 { 809 return Create(ProjectCollection.GlobalProjectCollection, Project.DefaultNewProjectTemplateOptions); 810 } 811 812 /// <summary> 813 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later using the specified <see cref="NewProjectFileOptions"/>. 814 /// Uses the global project collection. 815 /// </summary> Create(NewProjectFileOptions projectFileOptions)816 public static ProjectRootElement Create(NewProjectFileOptions projectFileOptions) 817 { 818 return Create(ProjectCollection.GlobalProjectCollection, projectFileOptions); 819 } 820 821 /// <summary> 822 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. 823 /// Uses the specified project collection. 824 /// </summary> Create(ProjectCollection projectCollection)825 public static ProjectRootElement Create(ProjectCollection projectCollection) 826 { 827 return Create(projectCollection.ProjectRootElementCache); 828 } 829 830 /// <summary> 831 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later using the specified <see cref="ProjectCollection"/> and <see cref="NewProjectFileOptions"/>. 832 /// </summary> Create(ProjectCollection projectCollection, NewProjectFileOptions projectFileOptions)833 public static ProjectRootElement Create(ProjectCollection projectCollection, NewProjectFileOptions projectFileOptions) 834 { 835 ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); 836 837 return Create(projectCollection.ProjectRootElementCache, projectFileOptions); 838 } 839 840 841 /// <summary> 842 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. 843 /// Uses the global project collection. 844 /// </summary> Create(string path)845 public static ProjectRootElement Create(string path) 846 { 847 return Create(path, ProjectCollection.GlobalProjectCollection, Project.DefaultNewProjectTemplateOptions); 848 } 849 850 /// <summary> 851 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later using the specified path and <see cref="NewProjectFileOptions"/>. 852 /// Uses the global project collection. 853 /// </summary> Create(string path, NewProjectFileOptions newProjectFileOptions)854 public static ProjectRootElement Create(string path, NewProjectFileOptions newProjectFileOptions) 855 { 856 return Create(path, ProjectCollection.GlobalProjectCollection, newProjectFileOptions); 857 } 858 859 /// <summary> 860 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. 861 /// Uses the specified project collection. 862 /// </summary> Create(string path, ProjectCollection projectCollection)863 public static ProjectRootElement Create(string path, ProjectCollection projectCollection) 864 { 865 return Create(path, projectCollection, Project.DefaultNewProjectTemplateOptions); 866 } 867 868 /// <summary> 869 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. 870 /// Uses the specified project collection. 871 /// </summary> Create(string path, ProjectCollection projectCollection, NewProjectFileOptions newProjectFileOptions)872 public static ProjectRootElement Create(string path, ProjectCollection projectCollection, NewProjectFileOptions newProjectFileOptions) 873 { 874 ErrorUtilities.VerifyThrowArgumentLength(path, "path"); 875 ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); 876 877 ProjectRootElement projectRootElement = new ProjectRootElement(projectCollection.ProjectRootElementCache, newProjectFileOptions); 878 projectRootElement.FullPath = path; 879 880 return projectRootElement; 881 } 882 883 /// <summary> 884 /// Initialize a ProjectRootElement instance from an XmlReader. 885 /// Uses the global project collection. 886 /// May throw InvalidProjectFileException. 887 /// </summary> Create(XmlReader xmlReader)888 public static ProjectRootElement Create(XmlReader xmlReader) 889 { 890 return Create(xmlReader, ProjectCollection.GlobalProjectCollection, preserveFormatting: false); 891 } 892 893 /// <summary> 894 /// Initialize a ProjectRootElement instance from an XmlReader. 895 /// Uses the specified project collection. 896 /// May throw InvalidProjectFileException. 897 /// </summary> Create(XmlReader xmlReader, ProjectCollection projectCollection)898 public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection) 899 { 900 return Create(xmlReader, projectCollection, preserveFormatting: false); 901 } 902 903 /// <summary> 904 /// Initialize a ProjectRootElement instance from an XmlReader. 905 /// Uses the specified project collection. 906 /// May throw InvalidProjectFileException. 907 /// </summary> Create(XmlReader xmlReader, ProjectCollection projectCollection, bool preserveFormatting)908 public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection, bool preserveFormatting) 909 { 910 ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); 911 912 return new ProjectRootElement(xmlReader, projectCollection.ProjectRootElementCache, true /*Explicitly loaded*/, 913 preserveFormatting); 914 } 915 916 /// <summary> 917 /// Initialize a ProjectRootElement instance by loading from the specified file path. 918 /// Uses the global project collection. 919 /// May throw InvalidProjectFileException. 920 /// </summary> Open(string path)921 public static ProjectRootElement Open(string path) 922 { 923 return Open(path, ProjectCollection.GlobalProjectCollection); 924 } 925 926 /// <summary> 927 /// Initialize a ProjectRootElement instance by loading from the specified file path. 928 /// Uses the specified project collection. 929 /// May throw InvalidProjectFileException. 930 /// </summary> Open(string path, ProjectCollection projectCollection)931 public static ProjectRootElement Open(string path, ProjectCollection projectCollection) 932 { 933 return Open(path, projectCollection, 934 preserveFormatting: null); 935 } 936 937 /// <summary> 938 /// Initialize a ProjectRootElement instance by loading from the specified file path. 939 /// Uses the specified project collection and preserves the formatting of the document if specified. 940 /// </summary> Open(string path, ProjectCollection projectCollection, bool? preserveFormatting)941 public static ProjectRootElement Open(string path, ProjectCollection projectCollection, bool? preserveFormatting) 942 { 943 ErrorUtilities.VerifyThrowArgumentLength(path, "path"); 944 ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); 945 946 path = FileUtilities.NormalizePath(path); 947 948 return Open(path, projectCollection.ProjectRootElementCache, true /*Is explicitly loaded*/, preserveFormatting); 949 } 950 951 /// <summary> 952 /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory. 953 /// Uses the global project collection. 954 /// </summary> 955 /// <param name="path">The path of the ProjectRootElement, cannot be null.</param> 956 /// <returns>The loaded ProjectRootElement, or null if it is not currently in memory.</returns> 957 /// <remarks> 958 /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore 959 /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory. 960 /// </remarks> TryOpen(string path)961 public static ProjectRootElement TryOpen(string path) 962 { 963 ErrorUtilities.VerifyThrowArgumentLength(path, "path"); 964 965 return TryOpen(path, ProjectCollection.GlobalProjectCollection); 966 } 967 968 /// <summary> 969 /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory. 970 /// Uses the specified project collection. 971 /// </summary> 972 /// <param name="path">The path of the ProjectRootElement, cannot be null.</param> 973 /// <param name="projectCollection">The <see cref="ProjectCollection"/> to load the project into.</param> 974 /// <returns>The loaded ProjectRootElement, or null if it is not currently in memory.</returns> 975 /// <remarks> 976 /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore 977 /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory. 978 /// </remarks> TryOpen(string path, ProjectCollection projectCollection)979 public static ProjectRootElement TryOpen(string path, ProjectCollection projectCollection) 980 { 981 return TryOpen(path, projectCollection, preserveFormatting: null); 982 } 983 984 /// <summary> 985 /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory. 986 /// Uses the specified project collection. 987 /// </summary> 988 /// <param name="path">The path of the ProjectRootElement, cannot be null.</param> 989 /// <param name="projectCollection">The <see cref="ProjectCollection"/> to load the project into.</param> 990 /// <param name="preserveFormatting"> 991 /// The formatting to open with. Must match the formatting in the collection to succeed. 992 /// </param> 993 /// <returns>The loaded ProjectRootElement, or null if it is not currently in memory.</returns> 994 /// <remarks> 995 /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore 996 /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory. 997 /// </remarks> TryOpen(string path, ProjectCollection projectCollection, bool? preserveFormatting)998 public static ProjectRootElement TryOpen(string path, ProjectCollection projectCollection, bool? preserveFormatting) 999 { 1000 ErrorUtilities.VerifyThrowArgumentLength(path, "path"); 1001 ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); 1002 1003 path = FileUtilities.NormalizePath(path); 1004 1005 ProjectRootElement projectRootElement = projectCollection.ProjectRootElementCache.TryGet(path, preserveFormatting); 1006 1007 return projectRootElement; 1008 } 1009 1010 /// <summary> 1011 /// Convenience method that picks a location based on a heuristic: 1012 /// If import groups exist, inserts into the last one without a condition on it. 1013 /// Otherwise, creates an import at the end of the project. 1014 /// </summary> AddImport(string project)1015 public ProjectImportElement AddImport(string project) 1016 { 1017 ErrorUtilities.VerifyThrowArgumentLength(project, "project"); 1018 1019 ProjectImportGroupElement importGroupToAddTo = 1020 ImportGroupsReversed.FirstOrDefault(importGroup => importGroup.Condition.Length <= 0); 1021 1022 ProjectImportElement import; 1023 1024 if (importGroupToAddTo != null) 1025 { 1026 import = importGroupToAddTo.AddImport(project); 1027 } 1028 else 1029 { 1030 import = CreateImportElement(project); 1031 AppendChild(import); 1032 } 1033 1034 return import; 1035 } 1036 1037 /// <summary> 1038 /// Convenience method that picks a location based on a heuristic: 1039 /// Creates an import group at the end of the project. 1040 /// </summary> AddImportGroup()1041 public ProjectImportGroupElement AddImportGroup() 1042 { 1043 ProjectImportGroupElement importGroup = CreateImportGroupElement(); 1044 AppendChild(importGroup); 1045 1046 return importGroup; 1047 } 1048 1049 /// <summary> 1050 /// Convenience method that picks a location based on a heuristic: 1051 /// Finds item group with no condition with at least one item of same type, or else adds a new item group; 1052 /// adds the item to that item group with items of the same type, ordered by include. 1053 /// </summary> 1054 /// <remarks> 1055 /// Per the previous implementation, it actually finds the last suitable item group, not the first. 1056 /// </remarks> AddItem(string itemType, string include)1057 public ProjectItemElement AddItem(string itemType, string include) 1058 { 1059 return AddItem(itemType, include, null); 1060 } 1061 1062 /// <summary> 1063 /// Convenience method that picks a location based on a heuristic: 1064 /// Finds first item group with no condition with at least one item of same type, or else an empty item group; or else adds a new item group; 1065 /// adds the item to that item group with items of the same type, ordered by include. 1066 /// Does not attempt to check whether the item matches an existing wildcard expression; that is only possible 1067 /// in the evaluated world. 1068 /// </summary> 1069 /// <remarks> 1070 /// Per the previous implementation, it actually finds the last suitable item group, not the first. 1071 /// </remarks> AddItem(string itemType, string include, IEnumerable<KeyValuePair<string, string>> metadata)1072 public ProjectItemElement AddItem(string itemType, string include, IEnumerable<KeyValuePair<string, string>> metadata) 1073 { 1074 ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType"); 1075 ErrorUtilities.VerifyThrowArgumentLength(include, "include"); 1076 1077 ProjectItemGroupElement itemGroupToAddTo = null; 1078 1079 foreach (ProjectItemGroupElement itemGroup in ItemGroups) 1080 { 1081 if (itemGroup.Condition.Length > 0) 1082 { 1083 continue; 1084 } 1085 1086 if (itemGroupToAddTo == null && itemGroup.Count == 0) 1087 { 1088 itemGroupToAddTo = itemGroup; 1089 } 1090 1091 if (itemGroup.Items.Any(item => MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, item.ItemType))) 1092 { 1093 itemGroupToAddTo = itemGroup; 1094 } 1095 1096 if (itemGroupToAddTo != null && itemGroupToAddTo.Count > 0) 1097 { 1098 break; 1099 } 1100 } 1101 1102 if (itemGroupToAddTo == null) 1103 { 1104 itemGroupToAddTo = AddItemGroup(); 1105 } 1106 1107 ProjectItemElement newItem = itemGroupToAddTo.AddItem(itemType, include, metadata); 1108 1109 return newItem; 1110 } 1111 1112 /// <summary> 1113 /// Convenience method that picks a location based on a heuristic: 1114 /// Adds an item group after the last existing item group, if any; otherwise 1115 /// adds an item group after the last existing property group, if any; otherwise 1116 /// adds a new item group at the end of the project. 1117 /// </summary> AddItemGroup()1118 public ProjectItemGroupElement AddItemGroup() 1119 { 1120 ProjectElement reference = ItemGroupsReversed.FirstOrDefault(); 1121 1122 if (reference == null) 1123 { 1124 foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed) 1125 { 1126 reference = propertyGroup; 1127 break; 1128 } 1129 } 1130 1131 ProjectItemGroupElement newItemGroup = CreateItemGroupElement(); 1132 1133 if (reference == null) 1134 { 1135 AppendChild(newItemGroup); 1136 } 1137 else 1138 { 1139 InsertAfterChild(newItemGroup, reference); 1140 } 1141 1142 return newItemGroup; 1143 } 1144 1145 /// <summary> 1146 /// Convenience method that picks a location based on a heuristic: 1147 /// Finds first item definition group with no condition with at least one item definition of same item type, or else adds a new item definition group. 1148 /// </summary> AddItemDefinition(string itemType)1149 public ProjectItemDefinitionElement AddItemDefinition(string itemType) 1150 { 1151 ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType"); 1152 1153 ProjectItemDefinitionGroupElement itemDefinitionGroupToAddTo = null; 1154 1155 foreach (ProjectItemDefinitionGroupElement itemDefinitionGroup in ItemDefinitionGroups) 1156 { 1157 if (itemDefinitionGroup.Condition.Length > 0) 1158 { 1159 continue; 1160 } 1161 1162 foreach (ProjectItemDefinitionElement itemDefinition in itemDefinitionGroup.ItemDefinitions) 1163 { 1164 if (MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, itemDefinition.ItemType)) 1165 { 1166 itemDefinitionGroupToAddTo = itemDefinitionGroup; 1167 break; 1168 } 1169 } 1170 1171 if (itemDefinitionGroupToAddTo != null) 1172 { 1173 break; 1174 } 1175 } 1176 1177 if (itemDefinitionGroupToAddTo == null) 1178 { 1179 itemDefinitionGroupToAddTo = AddItemDefinitionGroup(); 1180 } 1181 1182 ProjectItemDefinitionElement newItemDefinition = CreateItemDefinitionElement(itemType); 1183 1184 itemDefinitionGroupToAddTo.AppendChild(newItemDefinition); 1185 1186 return newItemDefinition; 1187 } 1188 1189 /// <summary> 1190 /// Convenience method that picks a location based on a heuristic: 1191 /// Adds an item definition group after the last existing item definition group, if any; otherwise 1192 /// adds an item definition group after the last existing property group, if any; otherwise 1193 /// adds a new item definition group at the end of the project. 1194 /// </summary> AddItemDefinitionGroup()1195 public ProjectItemDefinitionGroupElement AddItemDefinitionGroup() 1196 { 1197 ProjectElement reference = null; 1198 1199 foreach (ProjectItemDefinitionGroupElement itemDefinitionGroup in ItemDefinitionGroupsReversed) 1200 { 1201 reference = itemDefinitionGroup; 1202 break; 1203 } 1204 1205 if (reference == null) 1206 { 1207 foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed) 1208 { 1209 reference = propertyGroup; 1210 break; 1211 } 1212 } 1213 1214 ProjectItemDefinitionGroupElement newItemDefinitionGroup = CreateItemDefinitionGroupElement(); 1215 1216 InsertAfterChild(newItemDefinitionGroup, reference); 1217 1218 return newItemDefinitionGroup; 1219 } 1220 1221 /// <summary> 1222 /// Convenience method that picks a location based on a heuristic: 1223 /// Adds a new property group after the last existing property group, if any; otherwise 1224 /// at the start of the project. 1225 /// </summary> AddPropertyGroup()1226 public ProjectPropertyGroupElement AddPropertyGroup() 1227 { 1228 ProjectPropertyGroupElement reference = null; 1229 1230 foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed) 1231 { 1232 reference = propertyGroup; 1233 break; 1234 } 1235 1236 ProjectPropertyGroupElement newPropertyGroup = CreatePropertyGroupElement(); 1237 1238 InsertAfterChild(newPropertyGroup, reference); 1239 1240 return newPropertyGroup; 1241 } 1242 1243 /// <summary> 1244 /// Convenience method that picks a location based on a heuristic. 1245 /// Updates the last existing property with the specified name that has no condition on itself or its property group, if any. 1246 /// Otherwise, adds a new property in the first property group without a condition, creating a property group if necessary after 1247 /// the last existing property group, else at the start of the project. 1248 /// </summary> AddProperty(string name, string value)1249 public ProjectPropertyElement AddProperty(string name, string value) 1250 { 1251 ProjectPropertyGroupElement matchingPropertyGroup = null; 1252 ProjectPropertyElement matchingProperty = null; 1253 1254 foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroups) 1255 { 1256 if (propertyGroup.Condition.Length > 0) 1257 { 1258 continue; 1259 } 1260 1261 if (matchingPropertyGroup == null) 1262 { 1263 matchingPropertyGroup = propertyGroup; 1264 } 1265 1266 foreach (ProjectPropertyElement property in propertyGroup.Properties) 1267 { 1268 if (property.Condition.Length > 0) 1269 { 1270 continue; 1271 } 1272 1273 if (MSBuildNameIgnoreCaseComparer.Default.Equals(property.Name, name)) 1274 { 1275 matchingProperty = property; 1276 } 1277 } 1278 } 1279 1280 if (matchingProperty != null) 1281 { 1282 matchingProperty.Value = value; 1283 1284 return matchingProperty; 1285 } 1286 1287 if (matchingPropertyGroup == null) 1288 { 1289 matchingPropertyGroup = AddPropertyGroup(); 1290 } 1291 1292 ProjectPropertyElement newProperty = matchingPropertyGroup.AddProperty(name, value); 1293 1294 return newProperty; 1295 } 1296 1297 /// <summary> 1298 /// Convenience method that picks a location based on a heuristic: 1299 /// Creates a target at the end of the project. 1300 /// </summary> AddTarget(string name)1301 public ProjectTargetElement AddTarget(string name) 1302 { 1303 ProjectTargetElement target = CreateTargetElement(name); 1304 AppendChild(target); 1305 1306 return target; 1307 } 1308 1309 /// <summary> 1310 /// Convenience method that picks a location based on a heuristic: 1311 /// Creates a usingtask at the end of the project. 1312 /// Exactly one of assemblyName or assemblyFile must be null. 1313 /// </summary> AddUsingTask(string name, string assemblyFile, string assemblyName)1314 public ProjectUsingTaskElement AddUsingTask(string name, string assemblyFile, string assemblyName) 1315 { 1316 ProjectUsingTaskElement usingTask = CreateUsingTaskElement(name, FileUtilities.FixFilePath(assemblyFile), assemblyName); 1317 AppendChild(usingTask); 1318 1319 return usingTask; 1320 } 1321 1322 /// <summary> 1323 /// Creates a choose. 1324 /// Caller must add it to the location of choice in the project. 1325 /// </summary> CreateChooseElement()1326 public ProjectChooseElement CreateChooseElement() 1327 { 1328 return ProjectChooseElement.CreateDisconnected(this); 1329 } 1330 1331 /// <summary> 1332 /// Creates an import. 1333 /// Caller must add it to the location of choice in the project. 1334 /// </summary> CreateImportElement(string project)1335 public ProjectImportElement CreateImportElement(string project) 1336 { 1337 return ProjectImportElement.CreateDisconnected(project, this); 1338 } 1339 1340 /// <summary> 1341 /// Creates an item node. 1342 /// Caller must add it to the location of choice in the project. 1343 /// </summary> CreateItemElement(string itemType)1344 public ProjectItemElement CreateItemElement(string itemType) 1345 { 1346 return ProjectItemElement.CreateDisconnected(itemType, this); 1347 } 1348 1349 /// <summary> 1350 /// Creates an item node with an include. 1351 /// Caller must add it to the location of choice in the project. 1352 /// </summary> CreateItemElement(string itemType, string include)1353 public ProjectItemElement CreateItemElement(string itemType, string include) 1354 { 1355 ProjectItemElement item = ProjectItemElement.CreateDisconnected(itemType, this); 1356 1357 item.Include = include; 1358 1359 return item; 1360 } 1361 1362 /// <summary> 1363 /// Creates an item definition. 1364 /// Caller must add it to the location of choice in the project. 1365 /// </summary> CreateItemDefinitionElement(string itemType)1366 public ProjectItemDefinitionElement CreateItemDefinitionElement(string itemType) 1367 { 1368 return ProjectItemDefinitionElement.CreateDisconnected(itemType, this); 1369 } 1370 1371 /// <summary> 1372 /// Creates an item definition group. 1373 /// Caller must add it to the location of choice in the project. 1374 /// </summary> CreateItemDefinitionGroupElement()1375 public ProjectItemDefinitionGroupElement CreateItemDefinitionGroupElement() 1376 { 1377 return ProjectItemDefinitionGroupElement.CreateDisconnected(this); 1378 } 1379 1380 /// <summary> 1381 /// Creates an item group. 1382 /// Caller must add it to the location of choice in the project. 1383 /// </summary> CreateItemGroupElement()1384 public ProjectItemGroupElement CreateItemGroupElement() 1385 { 1386 return ProjectItemGroupElement.CreateDisconnected(this); 1387 } 1388 1389 /// <summary> 1390 /// Creates an import group. 1391 /// Caller must add it to the location of choice in the project. 1392 /// </summary> CreateImportGroupElement()1393 public ProjectImportGroupElement CreateImportGroupElement() 1394 { 1395 return ProjectImportGroupElement.CreateDisconnected(this); 1396 } 1397 1398 /// <summary> 1399 /// Creates a metadata node. 1400 /// Caller must add it to the location of choice in the project. 1401 /// </summary> CreateMetadataElement(string name)1402 public ProjectMetadataElement CreateMetadataElement(string name) 1403 { 1404 return ProjectMetadataElement.CreateDisconnected(name, this); 1405 } 1406 1407 /// <summary> 1408 /// Creates a metadata node. 1409 /// Caller must add it to the location of choice in the project. 1410 /// </summary> CreateMetadataElement(string name, string unevaluatedValue)1411 public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue) 1412 { 1413 ProjectMetadataElement metadatum = ProjectMetadataElement.CreateDisconnected(name, this); 1414 1415 metadatum.Value = unevaluatedValue; 1416 1417 return metadatum; 1418 } 1419 1420 /// <summary> 1421 /// Creates an on error node. 1422 /// Caller must add it to the location of choice in the project. 1423 /// </summary> CreateOnErrorElement(string executeTargets)1424 public ProjectOnErrorElement CreateOnErrorElement(string executeTargets) 1425 { 1426 return ProjectOnErrorElement.CreateDisconnected(executeTargets, this); 1427 } 1428 1429 /// <summary> 1430 /// Creates an otherwise node. 1431 /// Caller must add it to the location of choice in the project. 1432 /// </summary> CreateOtherwiseElement()1433 public ProjectOtherwiseElement CreateOtherwiseElement() 1434 { 1435 return ProjectOtherwiseElement.CreateDisconnected(this); 1436 } 1437 1438 /// <summary> 1439 /// Creates an output node. 1440 /// Exactly one of itemType and propertyName must be specified. 1441 /// Caller must add it to the location of choice in the project. 1442 /// </summary> CreateOutputElement(string taskParameter, string itemType, string propertyName)1443 public ProjectOutputElement CreateOutputElement(string taskParameter, string itemType, string propertyName) 1444 { 1445 return ProjectOutputElement.CreateDisconnected(taskParameter, itemType, propertyName, this); 1446 } 1447 1448 /// <summary> 1449 /// Creates a project extensions node. 1450 /// Caller must add it to the location of choice in the project. 1451 /// </summary> CreateProjectExtensionsElement()1452 public ProjectExtensionsElement CreateProjectExtensionsElement() 1453 { 1454 return ProjectExtensionsElement.CreateDisconnected(this); 1455 } 1456 1457 /// <summary> 1458 /// Creates a property group. 1459 /// Caller must add it to the location of choice in the project. 1460 /// </summary> CreatePropertyGroupElement()1461 public ProjectPropertyGroupElement CreatePropertyGroupElement() 1462 { 1463 return ProjectPropertyGroupElement.CreateDisconnected(this); 1464 } 1465 1466 /// <summary> 1467 /// Creates a property. 1468 /// Caller must add it to the location of choice in the project. 1469 /// </summary> CreatePropertyElement(string name)1470 public ProjectPropertyElement CreatePropertyElement(string name) 1471 { 1472 return ProjectPropertyElement.CreateDisconnected(name, this); 1473 } 1474 1475 /// <summary> 1476 /// Creates a target. 1477 /// Caller must add it to the location of choice in this project. 1478 /// </summary> CreateTargetElement(string name)1479 public ProjectTargetElement CreateTargetElement(string name) 1480 { 1481 return ProjectTargetElement.CreateDisconnected(name, this); 1482 } 1483 1484 /// <summary> 1485 /// Creates a task. 1486 /// Caller must add it to the location of choice in this project. 1487 /// </summary> CreateTaskElement(string name)1488 public ProjectTaskElement CreateTaskElement(string name) 1489 { 1490 return ProjectTaskElement.CreateDisconnected(name, this); 1491 } 1492 1493 /// <summary> 1494 /// Creates a using task. 1495 /// Caller must add it to the location of choice in the project. 1496 /// Exactly one of assembly file and assembly name must be provided. 1497 /// </summary> CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName)1498 public ProjectUsingTaskElement CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName) 1499 { 1500 return CreateUsingTaskElement(taskName, assemblyFile, assemblyName, null, null); 1501 } 1502 1503 /// <summary> 1504 /// Creates a using task. 1505 /// Caller must add it to the location of choice in the project. 1506 /// Exactly one of assembly file and assembly name must be provided. 1507 /// Also allows providing optional runtime and architecture specifiers. Null is OK. 1508 /// </summary> CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName, string runtime, string architecture)1509 public ProjectUsingTaskElement CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName, string runtime, string architecture) 1510 { 1511 return ProjectUsingTaskElement.CreateDisconnected(taskName, assemblyFile, assemblyName, runtime, architecture, this); 1512 } 1513 1514 /// <summary> 1515 /// Creates a ParameterGroup for use in a using task. 1516 /// Caller must add it to the location of choice in the project under a using task. 1517 /// </summary> CreateUsingTaskParameterGroupElement()1518 public UsingTaskParameterGroupElement CreateUsingTaskParameterGroupElement() 1519 { 1520 return UsingTaskParameterGroupElement.CreateDisconnected(this); 1521 } 1522 1523 /// <summary> 1524 /// Creates a Parameter for use in a using ParameterGroup. 1525 /// Caller must add it to the location of choice in the project under a using task. 1526 /// </summary> CreateUsingTaskParameterElement(string name, string output, string required, string parameterType)1527 public ProjectUsingTaskParameterElement CreateUsingTaskParameterElement(string name, string output, string required, string parameterType) 1528 { 1529 return ProjectUsingTaskParameterElement.CreateDisconnected(name, output, required, parameterType, this); 1530 } 1531 1532 /// <summary> 1533 /// Creates a Task element for use in a using task. 1534 /// Caller must add it to the location of choice in the project under a using task. 1535 /// </summary> CreateUsingTaskBodyElement(string evaluate, string body)1536 public ProjectUsingTaskBodyElement CreateUsingTaskBodyElement(string evaluate, string body) 1537 { 1538 return ProjectUsingTaskBodyElement.CreateDisconnected(evaluate, body, this); 1539 } 1540 1541 /// <summary> 1542 /// Creates a when. 1543 /// Caller must add it to the location of choice in this project. 1544 /// </summary> CreateWhenElement(string condition)1545 public ProjectWhenElement CreateWhenElement(string condition) 1546 { 1547 return ProjectWhenElement.CreateDisconnected(condition, this); 1548 } 1549 1550 /// <summary> 1551 /// Creates a project SDK element attached to this project. 1552 /// </summary> CreateProjectSdkElement(string sdkName, string sdkVersion)1553 public ProjectSdkElement CreateProjectSdkElement(string sdkName, string sdkVersion) 1554 { 1555 return ProjectSdkElement.CreateDisconnected(sdkName, sdkVersion, this); 1556 } 1557 1558 /// <summary> 1559 /// Save the project to the file system, if dirty. 1560 /// Uses the Encoding returned by the Encoding property. 1561 /// Creates any necessary directories. 1562 /// May throw IO-related exceptions. 1563 /// Clears the dirty flag. 1564 /// </summary> Save()1565 public void Save() 1566 { 1567 Save(Encoding); 1568 } 1569 1570 /// <summary> 1571 /// Save the project to the file system, if dirty. 1572 /// Creates any necessary directories. 1573 /// May throw IO-related exceptions. 1574 /// Clears the dirty flag. 1575 /// </summary> Save(Encoding saveEncoding)1576 public void Save(Encoding saveEncoding) 1577 { 1578 ErrorUtilities.VerifyThrowInvalidOperation(_projectFileLocation != null, "OM_MustSetFileNameBeforeSave"); 1579 1580 #if MSBUILDENABLEVSPROFILING 1581 try 1582 { 1583 string beginProjectSave = String.Format(CultureInfo.CurrentCulture, "Save Project {0} To File - Begin", projectFileLocation.File); 1584 DataCollection.CommentMarkProfile(8810, beginProjectSave); 1585 #endif 1586 1587 Directory.CreateDirectory(DirectoryPath); 1588 #if (!STANDALONEBUILD) 1589 using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectSaveToFileBegin, CodeMarkerEvent.perfMSBuildProjectSaveToFileEnd)) 1590 #endif 1591 { 1592 // Note: We're using string Equals on encoding and not EncodingUtilities.SimilarToEncoding in order 1593 // to force a save if the Encoding changed from UTF8 with BOM to UTF8 w/o BOM (for example). 1594 if (HasUnsavedChanges || !Equals(saveEncoding, Encoding)) 1595 { 1596 using (ProjectWriter projectWriter = new ProjectWriter(_projectFileLocation.File, saveEncoding)) 1597 { 1598 projectWriter.Initialize(XmlDocument); 1599 XmlDocument.Save(projectWriter); 1600 } 1601 1602 _encoding = saveEncoding; 1603 1604 FileInfo fileInfo = FileUtilities.GetFileInfoNoThrow(_projectFileLocation.File); 1605 1606 // If the file was deleted by a race with someone else immediately after it was written above 1607 // then we obviously can't read the write time. In this obscure case, we'll retain the 1608 // older last write time, which at worst would cause the next load to unnecessarily 1609 // come from disk. 1610 if (fileInfo != null) 1611 { 1612 _lastWriteTimeWhenRead = fileInfo.LastWriteTime; 1613 } 1614 1615 _versionOnDisk = Version; 1616 } 1617 } 1618 #if MSBUILDENABLEVSPROFILING 1619 } 1620 finally 1621 { 1622 string endProjectSave = String.Format(CultureInfo.CurrentCulture, "Save Project {0} To File - End", projectFileLocation.File); 1623 DataCollection.CommentMarkProfile(8811, endProjectSave); 1624 } 1625 #endif 1626 } 1627 1628 /// <summary> 1629 /// Save the project to the file system, if dirty or the path is different. 1630 /// Creates any necessary directories. 1631 /// May throw IO related exceptions. 1632 /// Clears the Dirty flag. 1633 /// </summary> Save(string path)1634 public void Save(string path) 1635 { 1636 Save(path, Encoding); 1637 } 1638 1639 /// <summary> 1640 /// Save the project to the file system, if dirty or the path is different. 1641 /// Creates any necessary directories. 1642 /// May throw IO related exceptions. 1643 /// Clears the Dirty flag. 1644 /// </summary> Save(string path, Encoding encoding)1645 public void Save(string path, Encoding encoding) 1646 { 1647 FullPath = path; 1648 1649 Save(encoding); 1650 } 1651 1652 /// <summary> 1653 /// Save the project to the provided TextWriter, whether or not it is dirty. 1654 /// Uses the encoding of the TextWriter. 1655 /// Clears the Dirty flag. 1656 /// </summary> Save(TextWriter writer)1657 public void Save(TextWriter writer) 1658 { 1659 using (ProjectWriter projectWriter = new ProjectWriter(writer)) 1660 { 1661 projectWriter.Initialize(XmlDocument); 1662 XmlDocument.Save(projectWriter); 1663 } 1664 1665 _versionOnDisk = Version; 1666 } 1667 1668 /// <summary> 1669 /// Returns a clone of this project. 1670 /// </summary> 1671 /// <returns>The cloned element.</returns> DeepClone()1672 public ProjectRootElement DeepClone() 1673 { 1674 return (ProjectRootElement)this.DeepClone(this, null); 1675 } 1676 1677 /// <summary> 1678 /// Reload the existing project root element from its file. 1679 /// An <see cref="InvalidOperationException"/> is thrown if the project root element is not associated with any file on disk. 1680 /// 1681 /// See <see cref="ProjectRootElement.ReloadFrom(XmlReader, bool, bool?)"/> 1682 /// </summary> Reload(bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)1683 public void Reload(bool throwIfUnsavedChanges = true, bool? preserveFormatting = null) 1684 { 1685 ErrorUtilities.VerifyThrowInvalidOperation(!string.IsNullOrEmpty(FullPath), "ValueNotSet", $"{nameof(ProjectRootElement)}.{nameof(FullPath)}"); 1686 1687 ReloadFrom(FullPath, throwIfUnsavedChanges, preserveFormatting); 1688 } 1689 1690 /// <summary> 1691 /// Reload the existing project root element from the given path 1692 /// An <see cref="InvalidOperationException"/> is thrown if the path does not exist. 1693 /// 1694 /// See <see cref="ProjectRootElement.ReloadFrom(XmlReader, bool, bool?)"/> 1695 /// </summary> ReloadFrom(string path, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)1696 public void ReloadFrom(string path, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null) 1697 { 1698 ErrorUtilities.VerifyThrowInvalidOperation(File.Exists(path), "FileToReloadFromDoesNotExist", path); 1699 1700 Func<bool, XmlDocumentWithLocation> documentProducer = shouldPreserveFormatting => LoadDocument(path, shouldPreserveFormatting); 1701 ReloadFrom(documentProducer, throwIfUnsavedChanges, preserveFormatting); 1702 } 1703 1704 /// <summary> 1705 /// Reload the existing project root element from the given <paramref name="reader"/> 1706 /// A reload operation completely replaces the state of this <see cref="ProjectRootElement"/> object. This operation marks the 1707 /// object as dirty (see <see cref="ProjectRootElement.MarkDirty"/> for side effects). 1708 /// 1709 /// If the new state has invalid XML or MSBuild syntax, then this method throws an <see cref="InvalidProjectFileException"/>. 1710 /// When this happens, the state of this object does not change. 1711 /// 1712 /// Reloading from an XMLReader will retain the previous root element location (<see cref="FullPath"/>, <see cref="DirectoryPath"/>, <see cref="ProjectFileLocation"/>). 1713 /// 1714 /// </summary> 1715 /// <param name="reader">Reader to read from</param> 1716 /// <param name="throwIfUnsavedChanges"> 1717 /// If set to false, the reload operation will discard any unsaved changes. 1718 /// Otherwise, an <see cref="InvalidOperationException"/> is thrown when unsaved changes are present. 1719 /// </param> 1720 /// <param name="preserveFormatting"> 1721 /// Whether the reload should preserve formatting or not. A null value causes the reload to reuse the existing <see cref="PreserveFormatting"/> value. 1722 /// </param> ReloadFrom(XmlReader reader, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)1723 public void ReloadFrom(XmlReader reader, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null) 1724 { 1725 Func<bool, XmlDocumentWithLocation> documentProducer = shouldPreserveFormatting => 1726 { 1727 var document = LoadDocument(reader, shouldPreserveFormatting); 1728 1729 document.FullPath = this.FullPath; 1730 1731 return document; 1732 }; 1733 1734 ReloadFrom(documentProducer, throwIfUnsavedChanges, preserveFormatting); 1735 } 1736 ReloadFrom(Func<bool, XmlDocumentWithLocation> documentProducer, bool throwIfUnsavedChanges, bool? preserveFormatting)1737 private void ReloadFrom(Func<bool, XmlDocumentWithLocation> documentProducer, bool throwIfUnsavedChanges, bool? preserveFormatting) 1738 { 1739 ThrowIfUnsavedChanges(throwIfUnsavedChanges); 1740 1741 XmlDocumentWithLocation document = documentProducer(preserveFormatting ?? PreserveFormatting); 1742 1743 // Reload should only mutate the state if there are no parse errors. 1744 ThrowIfDocumentHasParsingErrors(document); 1745 1746 // Do not clear the string cache. 1747 // Based on the assumption that Projects are reloaded repeatedly from their file with small increments, 1748 // and thus most strings would get reused 1749 //this.XmlDocument.ClearAnyCachedStrings(); 1750 1751 this.RemoveAllChildren(); 1752 1753 ProjectParser.Parse(document, this); 1754 1755 MarkDirty("Project reloaded", null); 1756 } 1757 1758 [MethodImpl(MethodImplOptions.NoOptimization)] ThrowIfDocumentHasParsingErrors(XmlDocumentWithLocation document)1759 private static void ThrowIfDocumentHasParsingErrors(XmlDocumentWithLocation document) 1760 { 1761 // todo: rather than throw away, copy over the parse results 1762 var throwaway = new ProjectRootElement(document); 1763 } 1764 1765 /// <summary> 1766 /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. 1767 /// Uses the specified project root element cache. 1768 /// </summary> Create(ProjectRootElementCache projectRootElementCache)1769 internal static ProjectRootElement Create(ProjectRootElementCache projectRootElementCache) 1770 { 1771 return new ProjectRootElement(projectRootElementCache, Project.DefaultNewProjectTemplateOptions); 1772 } 1773 Create(ProjectRootElementCache projectRootElementCache, NewProjectFileOptions projectFileOptions)1774 internal static ProjectRootElement Create(ProjectRootElementCache projectRootElementCache, NewProjectFileOptions projectFileOptions) 1775 { 1776 return new ProjectRootElement(projectRootElementCache, projectFileOptions); 1777 } 1778 1779 /// <summary> 1780 /// Initialize a ProjectRootElement instance by loading from the specified file path. 1781 /// Assumes path is already normalized. 1782 /// Uses the specified project root element cache. 1783 /// May throw InvalidProjectFileException. 1784 /// </summary> Open(string path, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded, bool? preserveFormatting)1785 internal static ProjectRootElement Open(string path, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded, 1786 bool? preserveFormatting) 1787 { 1788 ErrorUtilities.VerifyThrowInternalRooted(path); 1789 1790 ProjectRootElement projectRootElement = projectRootElementCache.Get(path, 1791 preserveFormatting ?? false ? s_openLoaderPreserveFormattingDelegate : s_openLoaderDelegate, 1792 isExplicitlyLoaded, preserveFormatting); 1793 1794 return projectRootElement; 1795 } 1796 1797 /// <summary> 1798 /// Initialize a ProjectRootElement instance from an existing document. 1799 /// Uses the global project collection. 1800 /// May throw InvalidProjectFileException. 1801 /// </summary> 1802 /// <remarks> 1803 /// This is ultimately for unit testing. 1804 /// Do not make public: we do not wish to expose particular XML API's. 1805 /// </remarks> Open(XmlDocumentWithLocation document)1806 internal static ProjectRootElement Open(XmlDocumentWithLocation document) 1807 { 1808 ErrorUtilities.VerifyThrow(document.FullPath == null, "Only virtual documents supported"); 1809 1810 return new ProjectRootElement(document, ProjectCollection.GlobalProjectCollection.ProjectRootElementCache); 1811 } 1812 1813 /// <summary> 1814 /// Gets a ProjectRootElement representing an MSBuild file. 1815 /// Path provided must be a canonicalized full path. 1816 /// May throw InvalidProjectFileException or an IO-related exception. 1817 /// </summary> OpenProjectOrSolution(string fullPath, IDictionary<string, string> globalProperties, string toolsVersion, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded)1818 internal static ProjectRootElement OpenProjectOrSolution(string fullPath, IDictionary<string, string> globalProperties, string toolsVersion, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded) 1819 { 1820 ErrorUtilities.VerifyThrowInternalRooted(fullPath); 1821 1822 ProjectRootElement projectRootElement = projectRootElementCache.Get( 1823 fullPath, 1824 (path, cache) => CreateProjectFromPath(path, globalProperties, toolsVersion, cache, 1825 preserveFormatting: false), 1826 isExplicitlyLoaded, 1827 // don't care about formatting, reuse whatever is there 1828 preserveFormatting: null); 1829 1830 return projectRootElement; 1831 } 1832 1833 /// <summary> 1834 /// Creates a XmlElement with the specified name in the document 1835 /// containing this project. 1836 /// </summary> CreateElement(string name)1837 internal XmlElementWithLocation CreateElement(string name) 1838 { 1839 return (XmlElementWithLocation)XmlDocument.CreateElement(name, XmlNamespace); 1840 } 1841 1842 /// <summary> 1843 /// Overridden to verify that the potential parent and siblings 1844 /// are acceptable. Throws InvalidOperationException if they are not. 1845 /// </summary> VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling)1846 internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) 1847 { 1848 ErrorUtilities.ThrowInvalidOperation("OM_CannotAcceptParent"); 1849 } 1850 1851 /// <summary> 1852 /// Marks this project as dirty. 1853 /// Typically called by child elements to indicate that they themselves have become dirty. 1854 /// Accepts a reason for debugging purposes only, and optional reason parameter. 1855 /// </summary> 1856 /// <remarks> 1857 /// This is sealed because it is virtual and called in a constructor; by sealing it we 1858 /// satisfy FXCop that nobody will override it to do something that would rely on 1859 /// unconstructed state. 1860 /// Should be protected+internal. 1861 /// </remarks> MarkDirty(string reason, string param)1862 internal sealed override void MarkDirty(string reason, string param) 1863 { 1864 IncrementVersion(); 1865 1866 _dirtyReason = reason; 1867 _dirtyParameter = param; 1868 1869 _timeLastChangedUtc = DateTime.UtcNow; 1870 1871 var changedEventArgs = new ProjectXmlChangedEventArgs(this, reason, param); 1872 var projectXmlChanged = OnProjectXmlChanged; 1873 projectXmlChanged?.Invoke(this, changedEventArgs); 1874 1875 // Only bubble this event up if the cache knows about this PRE. 1876 if (this.IsMemberOfProjectCollection) 1877 { 1878 _projectRootElementCache.OnProjectRootElementDirtied(this, changedEventArgs); 1879 } 1880 } 1881 1882 /// <summary> 1883 /// Bubbles a Project dirty notification up to the ProjectRootElementCache and ultimately to the ProjectCollection. 1884 /// </summary> 1885 /// <param name="project">The dirtied project.</param> MarkProjectDirty(Project project)1886 internal void MarkProjectDirty(Project project) 1887 { 1888 ErrorUtilities.VerifyThrowArgumentNull(project, "project"); 1889 1890 // Only bubble this event up if the cache knows about this PRE, which is equivalent to 1891 // whether this PRE has a path. 1892 if (_projectFileLocation != null) 1893 { 1894 _projectRootElementCache.OnProjectDirtied(project, new ProjectChangedEventArgs(project)); 1895 } 1896 } 1897 1898 /// <summary> 1899 /// Sets the <see cref="IsExplicitlyLoaded"/> property to <c>true</c> to indicate that this PRE 1900 /// should not be removed from the cache until it is explicitly unloaded by some MSBuild client. 1901 /// </summary> MarkAsExplicitlyLoaded()1902 internal void MarkAsExplicitlyLoaded() 1903 { 1904 IsExplicitlyLoaded = true; 1905 } 1906 1907 /// <summary> 1908 /// Creates and returns a list of <see cref="ProjectImportElement"/> nodes which are implicitly 1909 /// referenced by the Project. 1910 /// </summary> 1911 /// <param name="currentProjectOrImport">Current project</param> 1912 /// <returns>An <see cref="IEnumerable{SdkReference}"/> containing details of the SDKs referenced by the project.</returns> GetImplicitImportNodes(ProjectRootElement currentProjectOrImport)1913 internal List<ProjectImportElement> GetImplicitImportNodes(ProjectRootElement currentProjectOrImport) 1914 { 1915 var nodes = new List<ProjectImportElement>(); 1916 1917 string sdk = Sdk; 1918 if (!string.IsNullOrWhiteSpace(sdk)) 1919 { 1920 foreach (var referencedSdk in ParseSdks(sdk, SdkLocation)) 1921 { 1922 nodes.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, referencedSdk)); 1923 nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk)); 1924 } 1925 } 1926 1927 foreach (var sdkNode in Children.OfType<ProjectSdkElement>()) 1928 { 1929 var referencedSdk = new SdkReference( 1930 sdkNode.XmlElement.GetAttribute("Name"), 1931 sdkNode.XmlElement.GetAttribute("Version"), 1932 sdkNode.XmlElement.GetAttribute("MinimumVersion")); 1933 1934 nodes.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, referencedSdk)); 1935 nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk)); 1936 } 1937 1938 return nodes; 1939 } 1940 ParseSdks(string sdks, IElementLocation sdkLocation)1941 private static IEnumerable<SdkReference> ParseSdks(string sdks, IElementLocation sdkLocation) 1942 { 1943 foreach (string sdk in sdks.Split(';').Select(i => i.Trim())) 1944 { 1945 SdkReference sdkReference; 1946 1947 if (!SdkReference.TryParse(sdk, out sdkReference)) 1948 { 1949 ProjectErrorUtilities.ThrowInvalidProject(sdkLocation, "InvalidSdkFormat", sdks); 1950 } 1951 1952 yield return sdkReference; 1953 } 1954 } 1955 1956 /// <summary> 1957 /// Determines if the specified file is an empty XML file meaning it has no contents, contains only whitespace, or 1958 /// only an XML declaration. If the file does not exist, it is not considered empty. 1959 /// </summary> 1960 /// <param name="path">The full path to a file to check.</param> 1961 /// <returns><code>true</code> if the file is an empty XML file, otherwise <code>false</code>.</returns> IsEmptyXmlFile(string path)1962 internal static bool IsEmptyXmlFile(string path) 1963 { 1964 // The maximum number of characters of the file to read to check if its empty or not. Ideally we 1965 // would only look at zero-length files but empty XML files can contain just an xml declaration: 1966 // 1967 // <? xml version="1.0" encoding="utf-8" standalone="yes" ?> 1968 // 1969 // And this should also be treated as if the file is empty. 1970 // 1971 const int maxSizeToConsiderEmpty = 100; 1972 1973 if (!File.Exists(path)) 1974 { 1975 // Non-existent files are not treated as empty 1976 // 1977 return false; 1978 } 1979 1980 try 1981 { 1982 FileInfo fileInfo = new FileInfo(path); 1983 1984 if (fileInfo.Length == 0) 1985 { 1986 // Zero length files are empty 1987 // 1988 return true; 1989 } 1990 1991 if (fileInfo.Length > maxSizeToConsiderEmpty) 1992 { 1993 // Files greater than the maximum bytes to check are not empty 1994 // 1995 return false; 1996 } 1997 1998 string contents = File.ReadAllText(path); 1999 2000 // If the file is only whitespace or the XML declaration then it empty 2001 // 2002 return String.IsNullOrEmpty(contents) || XmlDeclarationRegEx.Value.IsMatch(contents); 2003 } 2004 catch (Exception) 2005 { 2006 // ignored 2007 } 2008 2009 return false; 2010 } 2011 2012 /// <summary> 2013 /// Returns a new instance of ProjectRootElement that is affiliated with the same ProjectRootElementCache. 2014 /// </summary> 2015 /// <param name="owner">The factory to use for creating the new instance.</param> CreateNewInstance(ProjectRootElement owner)2016 protected override ProjectElement CreateNewInstance(ProjectRootElement owner) 2017 { 2018 return ProjectRootElement.Create(owner._projectRootElementCache); 2019 } 2020 2021 /// <summary> 2022 /// Creates a new ProjectRootElement for a specific PRE cache 2023 /// </summary> 2024 /// <param name="path">The path to the file to load.</param> 2025 /// <param name="projectRootElementCache">The cache to load the PRE into.</param> OpenLoader(string path, ProjectRootElementCache projectRootElementCache)2026 private static ProjectRootElement OpenLoader(string path, ProjectRootElementCache projectRootElementCache) 2027 { 2028 return OpenLoader(path, projectRootElementCache, preserveFormatting: false); 2029 } 2030 OpenLoaderPreserveFormatting(string path, ProjectRootElementCache projectRootElementCache)2031 private static ProjectRootElement OpenLoaderPreserveFormatting(string path, ProjectRootElementCache projectRootElementCache) 2032 { 2033 return OpenLoader(path, projectRootElementCache, preserveFormatting: true); 2034 } 2035 OpenLoader(string path, ProjectRootElementCache projectRootElementCache, bool preserveFormatting)2036 private static ProjectRootElement OpenLoader(string path, ProjectRootElementCache projectRootElementCache, bool preserveFormatting) 2037 { 2038 return new ProjectRootElement( 2039 path, 2040 projectRootElementCache, 2041 preserveFormatting); 2042 } 2043 2044 /// <summary> 2045 /// Creates a ProjectRootElement representing a file, where the file may be a .sln instead of 2046 /// an MSBuild format file. 2047 /// Assumes path is already normalized. 2048 /// If the file is in MSBuild format, may throw InvalidProjectFileException. 2049 /// If the file is a solution, will throw an IO-related exception if the file cannot be read. 2050 /// </summary> CreateProjectFromPath( string projectFile, IDictionary<string, string> globalProperties, string toolsVersion, ProjectRootElementCache projectRootElementCache, bool preserveFormatting )2051 private static ProjectRootElement CreateProjectFromPath 2052 ( 2053 string projectFile, 2054 IDictionary<string, string> globalProperties, 2055 string toolsVersion, 2056 ProjectRootElementCache projectRootElementCache, 2057 bool preserveFormatting 2058 ) 2059 { 2060 ErrorUtilities.VerifyThrowInternalRooted(projectFile); 2061 2062 try 2063 { 2064 if (FileUtilities.IsVCProjFilename(projectFile)) 2065 { 2066 ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(projectFile), "ProjectUpgradeNeededToVcxProj", projectFile); 2067 } 2068 2069 // OK it's a regular project file, load it normally. 2070 return new ProjectRootElement(projectFile, projectRootElementCache, preserveFormatting); 2071 } 2072 catch (InvalidProjectFileException) 2073 { 2074 throw; 2075 } 2076 catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex)) 2077 { 2078 ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(projectFile), ex, "InvalidProjectFile", ex.Message); 2079 throw; // Without this there's a spurious CS0161 because csc 1.2.0.60317 can't see that the above is an unconditional throw. 2080 } 2081 } 2082 2083 /// <summary> 2084 /// Constructor helper to load an XmlDocumentWithLocation from a path. 2085 /// Assumes path is already normalized. 2086 /// May throw InvalidProjectFileException. 2087 /// Never returns null. 2088 /// Does NOT add to the ProjectRootElementCache. Caller should add after verifying subsequent MSBuild parsing succeeds. 2089 /// </summary> 2090 /// <param name="fullPath">The full path to the document to load.</param> 2091 /// <param name="preserveFormatting"><code>true</code> to preserve the formatting of the document, otherwise <code>false</code>.</param> LoadDocument(string fullPath, bool preserveFormatting)2092 private XmlDocumentWithLocation LoadDocument(string fullPath, bool preserveFormatting) 2093 { 2094 ErrorUtilities.VerifyThrowInternalRooted(fullPath); 2095 2096 var document = new XmlDocumentWithLocation 2097 { 2098 FullPath = fullPath, 2099 PreserveWhitespace = preserveFormatting 2100 }; 2101 #if (!STANDALONEBUILD) 2102 using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectLoadFromFileBegin, CodeMarkerEvent.perfMSBuildProjectLoadFromFileEnd)) 2103 #endif 2104 { 2105 try 2106 { 2107 #if MSBUILDENABLEVSPROFILING 2108 string beginProjectLoad = String.Format(CultureInfo.CurrentCulture, "Load Project {0} From File - Start", fullPath); 2109 DataCollection.CommentMarkProfile(8806, beginProjectLoad); 2110 #endif 2111 using (var xtr = XmlReaderExtension.Create(fullPath)) 2112 { 2113 _encoding = xtr.Encoding; 2114 document.Load(xtr.Reader); 2115 } 2116 2117 _projectFileLocation = ElementLocation.Create(fullPath); 2118 _directory = Path.GetDirectoryName(fullPath); 2119 2120 if (XmlDocument != null) 2121 { 2122 XmlDocument.FullPath = fullPath; 2123 } 2124 2125 _lastWriteTimeWhenRead = FileUtilities.GetFileInfoNoThrow(fullPath).LastWriteTime; 2126 } 2127 catch (Exception ex) 2128 { 2129 if (ExceptionHandling.NotExpectedIoOrXmlException(ex)) 2130 { 2131 throw; 2132 } 2133 2134 XmlException xmlException = ex as XmlException; 2135 2136 BuildEventFileInfo fileInfo; 2137 2138 fileInfo = xmlException != null 2139 ? new BuildEventFileInfo(fullPath, xmlException) 2140 : new BuildEventFileInfo(fullPath); 2141 2142 ProjectFileErrorUtilities.ThrowInvalidProjectFile(fileInfo, ex, "InvalidProjectFile", ex.Message); 2143 } 2144 #if MSBUILDENABLEVSPROFILING 2145 finally 2146 { 2147 string endProjectLoad = String.Format(CultureInfo.CurrentCulture, "Load Project {0} From File - End", fullPath); 2148 DataCollection.CommentMarkProfile(8807, endProjectLoad); 2149 } 2150 #endif 2151 } 2152 2153 return document; 2154 } 2155 2156 /// <summary> 2157 /// Constructor helper to load an XmlDocumentWithLocation from an XmlReader. 2158 /// May throw InvalidProjectFileException. 2159 /// Never returns null. 2160 /// </summary> LoadDocument(XmlReader reader, bool preserveFormatting)2161 private XmlDocumentWithLocation LoadDocument(XmlReader reader, bool preserveFormatting) 2162 { 2163 var document = new XmlDocumentWithLocation {PreserveWhitespace = preserveFormatting}; 2164 2165 try 2166 { 2167 document.Load(reader); 2168 } 2169 catch (XmlException ex) 2170 { 2171 BuildEventFileInfo fileInfo = new BuildEventFileInfo(ex); 2172 2173 ProjectFileErrorUtilities.ThrowInvalidProjectFile(fileInfo, "InvalidProjectFile", ex.Message); 2174 } 2175 2176 return document; 2177 } 2178 2179 /// <summary> 2180 /// Boost the appdomain-unique version counter for this object. 2181 /// This is done when it is modified, and also when it is loaded. 2182 /// </summary> IncrementVersion()2183 private void IncrementVersion() 2184 { 2185 _version = Interlocked.Increment(ref s_globalVersionCounter); 2186 } 2187 ThrowIfUnsavedChanges(bool throwIfUnsavedChanges)2188 private void ThrowIfUnsavedChanges(bool throwIfUnsavedChanges) 2189 { 2190 if (HasUnsavedChanges && throwIfUnsavedChanges) 2191 { 2192 ErrorUtilities.ThrowInvalidOperation("NoReloadOnUnsavedChanges", null); 2193 } 2194 } 2195 } 2196 } 2197