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.Xml; 6 using System.Collections; 7 using System.Collections.Generic; 8 using System.IO; 9 using System.Text; 10 using System.Globalization; 11 using System.Diagnostics; 12 13 using Microsoft.Build.BuildEngine.Shared; 14 using Microsoft.Build.Framework; 15 16 namespace Microsoft.Build.BuildEngine 17 { 18 /// <summary> 19 /// This class represents a collection of items. It may be represented 20 /// physically by an <ItemGroup> element persisted in the project file, 21 /// or it may just be a virtual BuildItemGroup (e.g., the evaluated items). 22 /// </summary> 23 [DebuggerDisplay("BuildItemGroup (Count = { Count }, Condition = { Condition })")] 24 public class BuildItemGroup : IItemPropertyGrouping, IEnumerable 25 { 26 #region Member Data 27 28 // Object holding the backing Xml, if any 29 private BuildItemGroupXml xml; 30 31 // Whether there is backing Xml 32 // Can't use (xml==null) because it is consulted during construction of the backing Xml 33 // Can't use (parentProject!=null) because Clone() sets it to null, but expects to create a persisted group... 34 private bool isPersisted = false; 35 36 // If this is a persisted <ItemGroup>, this boolean tells us whether 37 // it came from the main project file, or an imported project file. 38 bool importedFromAnotherProject = false; 39 40 // These are the loose Items beneath this BuildItemGroup. This is 41 // valid for both persisted and virtual ItemGroups. 42 private List<BuildItem> items; 43 44 // If we are ever asked to back up our persisted items, we put the list 45 // in here, so we can restore them later. 46 private List<BuildItem> persistedItemBackup; 47 48 // Collection propertygroup belongs to. 49 private GroupingCollection parentCollection = null; 50 51 // If this is a persisted BuildItemGroup, it has a parent Project object. 52 private Project parentProject = null; 53 54 #endregion 55 56 #region Constructors 57 58 /// <summary> 59 /// Default constructor, which initializes a virtual (non-persisted) BuildItemGroup. 60 /// </summary> BuildItemGroup()61 public BuildItemGroup() 62 { 63 this.items = new List<BuildItem>(); 64 } 65 66 /// <summary> 67 /// This constructor initializes the BuildItemGroup from an <ItemGroup> element 68 /// in the project file. It might come from the main project file or an 69 /// imported project file. 70 /// </summary> BuildItemGroup(XmlElement itemGroupElement, bool importedFromAnotherProject, Project parentProject)71 internal BuildItemGroup(XmlElement itemGroupElement, bool importedFromAnotherProject, Project parentProject) 72 : this() 73 { 74 this.isPersisted = true; 75 this.xml = new BuildItemGroupXml(itemGroupElement); 76 this.importedFromAnotherProject = importedFromAnotherProject; 77 this.parentProject = parentProject; 78 79 List<XmlElement> children = xml.GetChildren(); 80 EnsureCapacity(children.Count); 81 for (int i = 0; i < children.Count; i++) 82 { 83 AddExistingItem(new BuildItem(children[i], importedFromAnotherProject, parentProject.ItemDefinitionLibrary)); 84 } 85 } 86 87 /// <summary> 88 /// Constructor, which creates a new <ItemGroup> element in the XML document 89 /// specified. 90 /// </summary> BuildItemGroup(XmlDocument ownerDocument, bool importedFromAnotherProject, Project parentProject)91 internal BuildItemGroup(XmlDocument ownerDocument, bool importedFromAnotherProject, Project parentProject) 92 : this() 93 { 94 this.isPersisted = true; 95 this.xml = new BuildItemGroupXml(ownerDocument); 96 this.importedFromAnotherProject = importedFromAnotherProject; 97 this.parentProject = parentProject; 98 } 99 100 #endregion 101 102 #region Properties 103 104 /// <summary> 105 /// This returns a boolean telling you whether this particular item 106 /// group was imported from another project, or whether it was defined 107 /// in the main project. For virtual item groups which have no 108 /// persistence, this is false. 109 /// </summary> 110 public bool IsImported 111 { 112 get { return importedFromAnotherProject; } 113 } 114 115 /// <summary> 116 /// Accessor for the condition on the item group. 117 /// </summary> 118 public string Condition 119 { 120 get 121 { 122 return (IsPersisted ? xml.Condition : String.Empty); 123 } 124 125 set 126 { 127 MustBePersisted("CannotSetCondition"); 128 MustNotBeImported(); 129 xml.Condition = value; 130 MarkItemGroupAsDirty(); 131 } 132 } 133 134 /// <summary> 135 /// Accessor for the XmlElement representing this item. This is internal 136 /// to MSBuild, and is read-only. 137 /// </summary> 138 internal XmlElement ItemGroupElement 139 { 140 get { return xml.Element; } 141 } 142 143 /// <summary> 144 /// Accessor for the parent Project object. 145 /// </summary> 146 internal Project ParentProject 147 { 148 get { return this.parentProject; } 149 } 150 151 /// <summary> 152 /// Setter for parent project field that makes explicit that's it's only for clearing it. 153 /// </summary> ClearParentProject()154 internal void ClearParentProject() 155 { 156 parentProject = null; 157 } 158 159 /// <summary> 160 /// Number of items in this group. 161 /// </summary> 162 public int Count 163 { 164 get { return items.Count; } 165 } 166 167 /// <summary> 168 /// Gets the item at the specified index. 169 /// </summary> 170 public BuildItem this[int index] 171 { 172 get { return items[index]; } 173 } 174 175 /// <summary> 176 /// Gets the actual list of items contained 177 /// in this group. 178 /// </summary> 179 internal List<BuildItem> Items 180 { 181 get { return items; } 182 } 183 184 /// <summary> 185 /// Accessor for the ParentCollection that the BuildPropertyGroup belongs to 186 /// </summary> 187 internal GroupingCollection ParentCollection 188 { 189 get { return parentCollection; } 190 set { parentCollection = value; } 191 } 192 193 /// <summary> 194 /// Accessor for the parent XML element 195 /// </summary> 196 internal XmlElement ParentElement 197 { 198 get { return xml.ParentElement; } 199 } 200 #endregion 201 202 #region Methods 203 204 /// <summary> 205 /// Copies the items in this group into a new array. 206 /// NOTE: the copies are NOT clones i.e. only the references are copied 207 /// </summary> ToArray()208 public BuildItem[] ToArray() 209 { 210 return items.ToArray(); 211 } 212 213 /// <summary> 214 /// This IEnumerable method returns an IEnumerator object, which allows 215 /// the caller to enumerate through the BuildItem objects contained in 216 /// this BuildItemGroup. 217 /// </summary> GetEnumerator()218 public IEnumerator GetEnumerator() 219 { 220 return items.GetEnumerator(); 221 } 222 223 /// <summary> 224 /// Import a bunch of items from another BuildItemGroup. This is an O(n) operation. 225 /// </summary> ImportItems(BuildItemGroup itemsToImport)226 internal void ImportItems(BuildItemGroup itemsToImport) 227 { 228 ErrorUtilities.VerifyThrow(itemsToImport != null, "Null BuildItemGroup passed in."); 229 ErrorUtilities.VerifyThrow(itemsToImport != this, "Can't import into self."); 230 231 EnsureCapacity(this.Count + itemsToImport.Count); // PERF: important to pre-size 232 233 // Loop through all the Items in the given BuildItemGroup, and add them to 234 // our own BuildItemGroup. 235 foreach (BuildItem itemToImport in itemsToImport) 236 { 237 AddItem(itemToImport); 238 } 239 } 240 241 /// <summary> 242 /// Remove a bunch of items. This is an O(n) operation. 243 /// </summary> RemoveItems(BuildItemGroup itemsToRemove)244 internal void RemoveItems(BuildItemGroup itemsToRemove) 245 { 246 ErrorUtilities.VerifyThrow(itemsToRemove != null, "Null BuildItemGroup passed in."); 247 ErrorUtilities.VerifyThrow(itemsToRemove != this, "Can't remove self."); 248 249 foreach (BuildItem itemToRemove in itemsToRemove) 250 { 251 RemoveItem(itemToRemove); 252 } 253 } 254 RemoveItemsWithBackup(BuildItemGroup itemsToRemove)255 internal void RemoveItemsWithBackup(BuildItemGroup itemsToRemove) 256 { 257 ErrorUtilities.VerifyThrow(itemsToRemove != null, "Null BuildItemGroup passed in."); 258 ErrorUtilities.VerifyThrow(itemsToRemove != this, "Can't remove self."); 259 260 foreach (BuildItem itemToRemove in itemsToRemove) 261 { 262 RemoveItemWithBackup(itemToRemove); 263 } 264 } 265 266 /// <summary> 267 /// Applies each of the item modifications in order. 268 /// Items are replaced with a virtual clone before they are modified. 269 /// If an item does not exist in this group, the modification is skipped. 270 /// If any modifications conflict, these modifications win. 271 /// Returns the cloned item made, or null if it does not exist in this group. 272 /// </summary> ModifyItemAfterCloningUsingVirtualMetadata(BuildItem item, Dictionary<string, string> metadata)273 internal BuildItem ModifyItemAfterCloningUsingVirtualMetadata(BuildItem item, Dictionary<string, string> metadata) 274 { 275 int index = items.IndexOf(item); 276 if (index > -1) 277 { 278 BuildItem clone = items[index].VirtualClone(); 279 items[index] = clone; 280 281 foreach (KeyValuePair<string, string> pair in metadata) 282 { 283 clone.SetVirtualMetadata(pair.Key, pair.Value); 284 } 285 286 return clone; 287 } 288 289 return null; 290 } 291 292 /// <summary> 293 /// Applies each of the item modifications in order. 294 /// Items are NOT cloned. 295 /// Metadata is set as virtual metadata, so it is reset by Project.ResetBuildStatus(). 296 /// If an item does not exist in this group, the modification is skipped. 297 /// If any modifications conflict, these modifications win. 298 /// </summary> ModifyItemsUsingVirtualMetadata(Dictionary<BuildItem, Dictionary<string, string>> modifies)299 internal void ModifyItemsUsingVirtualMetadata(Dictionary<BuildItem, Dictionary<string, string>> modifies) 300 { 301 foreach (KeyValuePair<BuildItem, Dictionary<string, string>> modify in modifies) 302 { 303 int index = items.IndexOf(modify.Key); 304 if (index > -1) 305 { 306 foreach (KeyValuePair<string, string> pair in modify.Value) 307 { 308 items[index].SetVirtualMetadata(pair.Key, pair.Value); 309 } 310 } 311 } 312 } 313 314 /// <summary> 315 /// Pre-allocates space in the item list. 316 /// PERF: Call this first before adding a known quantity of items to a group, to avoid 317 /// repeated expansions of the backing list. 318 /// </summary> EnsureCapacity(int capacity)319 internal void EnsureCapacity(int capacity) 320 { 321 // Don't reduce capacity here; that's a list copy, too 322 if (capacity > items.Capacity) 323 { 324 items.Capacity = capacity; 325 } 326 } 327 328 /// <summary> 329 /// Adds an existing BuildItem to the list of items, does not attempt to add 330 /// backing Xml for the item. 331 /// </summary> AddExistingItem(BuildItem itemToAdd)332 internal void AddExistingItem(BuildItem itemToAdd) 333 { 334 AddExistingItemAt(items.Count, itemToAdd); 335 } 336 337 /// <summary> 338 /// Adds an existing BuildItem to the list of items at the specified index. 339 /// Does not attempt to add backing Xml for the item. 340 /// </summary> AddExistingItemAt(int index, BuildItem itemToAdd)341 internal void AddExistingItemAt(int index, BuildItem itemToAdd) 342 { 343 ErrorUtilities.VerifyThrow(items != null, "BuildItemGroup has not been initialized."); 344 ErrorUtilities.VerifyThrow(index <= items.Count, "Index out of range"); 345 346 items.Insert(index, itemToAdd); 347 348 if (parentProject != null) 349 { 350 itemToAdd.ItemDefinitionLibrary = parentProject.ItemDefinitionLibrary; 351 } 352 353 // If this BuildItemGroup is a persisted <ItemGroup>, then we need the 354 // items to have a reference back to their parent BuildItemGroup. This 355 // makes it *much* easier to delete items through the object model. 356 if (IsPersisted) 357 { 358 itemToAdd.ParentPersistedItemGroup = this; 359 } 360 MarkItemGroupAsDirty(); 361 } 362 363 /// <summary> 364 /// Adds an BuildItem to this BuildItemGroup. If this is a persisted BuildItemGroup, then 365 /// this method also inserts the BuildItem's XML into the appropriate location 366 /// in the XML document. For persisted ItemGroups, the behavior is that 367 /// it tries to insert the new BuildItem such that it is "near" other items of the 368 /// same type. ("Near" is defined as just after the last existing item 369 /// of the same type, or at the end if none is found.) 370 /// </summary> AddItem(BuildItem itemToAdd)371 internal void AddItem(BuildItem itemToAdd) 372 { 373 MustBeInitialized(); 374 375 // If we are a persisted <ItemGroup>, add the item element as a new child. 376 if (IsPersisted) 377 { 378 MustNotBeImported(); 379 380 // Make sure the item to be added has an XML element backing it, 381 // and that its XML belongs to the same XML document as our BuildItemGroup. 382 ErrorUtilities.VerifyThrow(itemToAdd.IsBackedByXml, "Item does not have an XML element"); 383 ErrorUtilities.VerifyThrow(itemToAdd.ItemElement.OwnerDocument == xml.OwnerDocument, "Cannot add an Item with a different XML owner document."); 384 385 // Generally, the desired behavior is to keep items of the same Type physically 386 // contiguous within the BuildItemGroup. (It's just easier to read that way.) So we 387 // scan through the existing items in our BuildItemGroup, and try to find the spot where 388 // the new item would fit in alphabetically. This is nice because it helps 389 // source code control scenarios where multiple clients are adding items to 390 // the same list. By putting them in alphabetical order, there's less of a 391 // chance of merge conflicts. 392 int insertionIndex = items.Count; 393 for (int i = 0; i < items.Count; i++) 394 { 395 if ( 0 == String.Compare(itemToAdd.Name, items[i].Name, StringComparison.OrdinalIgnoreCase)) 396 { 397 insertionIndex = i + 1; 398 399 if ( 0 > String.Compare(itemToAdd.Include, items[i].Include, StringComparison.OrdinalIgnoreCase)) 400 { 401 insertionIndex = i; 402 break; 403 } 404 } 405 } 406 407 // If there is at least one item in this group, then insert this new item 408 // at the correct location based on our simple scan for similar item types. 409 if (items.Count > 0) 410 { 411 if (insertionIndex == items.Count) 412 { 413 XmlElement itemElementToInsertAfter = items[items.Count - 1].ItemElement; 414 xml.InsertAfter((XmlElement)itemElementToInsertAfter.ParentNode, itemToAdd.ItemElement, itemElementToInsertAfter); 415 } 416 else 417 { 418 XmlElement itemElementToInsertBefore = items[insertionIndex].ItemElement; 419 xml.InsertBefore((XmlElement)itemElementToInsertBefore.ParentNode, itemToAdd.ItemElement, itemElementToInsertBefore); 420 } 421 422 AddExistingItemAt(insertionIndex, itemToAdd); 423 } 424 else 425 { 426 // This BuildItemGroup is currently empty. 427 // add the new BuildItem as a child of the <ItemGroup> tag. 428 xml.AppendChild(itemToAdd.ItemElement); 429 430 // Add the item to the end of our list. 431 AddExistingItem(itemToAdd); 432 } 433 } 434 else 435 { 436 // Add the item to the end of our list. 437 AddExistingItem(itemToAdd); 438 } 439 } 440 441 /// <summary> 442 /// Creates a new BuildItem defined by the given "Type" and "Include", and 443 /// adds it to the end of this BuildItemGroup. 444 /// If the group is persisted, the item is persisted; otherwise it is virtual 445 /// </summary> AddNewItem(string itemName, string itemInclude)446 public BuildItem AddNewItem(string itemName, string itemInclude) 447 { 448 BuildItem newItem; 449 450 if (IsPersisted) 451 { 452 // We are a persisted <ItemGroup>, so create a new persisted item object. 453 newItem = new BuildItem(xml.OwnerDocument, itemName, itemInclude, parentProject.ItemDefinitionLibrary); 454 } 455 else 456 { 457 // Create a new virtual BuildItem. 458 newItem = new BuildItem(itemName, itemInclude); 459 } 460 461 AddItem(newItem); 462 return newItem; 463 } 464 465 /// <summary> 466 /// Adds a new item to the ItemGroup, optional treating the item Include as literal so that 467 /// any special characters will be escaped before persisting it. 468 /// </summary> AddNewItem(string itemName, string itemInclude, bool treatItemIncludeAsLiteral)469 public BuildItem AddNewItem(string itemName, string itemInclude, bool treatItemIncludeAsLiteral) 470 { 471 return AddNewItem(itemName, treatItemIncludeAsLiteral ? EscapingUtilities.Escape(itemInclude) : itemInclude); 472 } 473 474 /// <summary> 475 /// Removes the given BuildItem from this BuildItemGroup. 476 /// If the item is part of the project manifest (ie, it's declared outside of a target) then 477 /// makes a backup of persisted items so that later the item group can be reverted to that backup, 478 /// reversing this change. 479 /// </summary> RemoveItemWithBackup(BuildItem itemToRemove)480 internal void RemoveItemWithBackup(BuildItem itemToRemove) 481 { 482 MustBeInitialized(); 483 484 if (itemToRemove.IsPartOfProjectManifest) 485 { 486 // We're about to remove an item that's part of the project manifest; 487 // this must be reverted when we reset the project, so make sure we've got a backup 488 BackupPersistedItems(); 489 } 490 491 // Don't remove the XML node, or mark the itemgroup as dirty; this is 492 // strictly an operation on temporary items, because we'll be un-backing up the 493 // persisted items at the end of the build 494 495 items.Remove(itemToRemove); 496 } 497 498 /// <summary> 499 /// Removes the given BuildItem from this BuildItemGroup. 500 /// If item is not in this group, does nothing. 501 /// </summary> RemoveItem(BuildItem itemToRemove)502 public void RemoveItem(BuildItem itemToRemove) 503 { 504 MustBeInitialized(); 505 RemoveItemElement(itemToRemove); 506 items.Remove(itemToRemove); 507 MarkItemGroupAsDirty(); 508 } 509 510 /// <summary> 511 /// Removes the item at the specified index. 512 /// </summary> 513 /// <exception cref="ArgumentOutOfRangeException">If index is out of bounds</exception> RemoveItemAt(int index)514 public void RemoveItemAt(int index) 515 { 516 MustBeInitialized(); 517 BuildItem item = items[index]; 518 RemoveItemElement(item); 519 items.RemoveAt(index); 520 MarkItemGroupAsDirty(); 521 } 522 523 /// <summary> 524 /// If this is a persisted group, removes the XML element corresponding to the given item. 525 /// If this is not a persisted group, does nothing. 526 /// </summary> RemoveItemElement(BuildItem item)527 private void RemoveItemElement(BuildItem item) 528 { 529 if (IsPersisted) 530 { 531 MustNotBeImported(); 532 MustHaveThisParentElement(item); 533 MustHaveThisParentGroup(item); 534 xml.Element.RemoveChild(item.ItemElement); 535 item.ParentPersistedItemGroup = null; 536 } 537 } 538 539 /// <summary> 540 /// Clones the BuildItemGroup. A shallow clone here is one that references 541 /// the same BuildItem objects as the original, whereas a deep clone actually 542 /// clones the BuildItem objects as well. If this is a persisted BuildItemGroup, 543 /// only deep clones are allowed, because you can't have the same XML 544 /// element belonging to two parents. 545 /// </summary> Clone(bool deepClone)546 public BuildItemGroup Clone(bool deepClone) 547 { 548 BuildItemGroup clone; 549 550 if (IsPersisted) 551 { 552 // Only deep clones are permitted when dealing with a persisted <ItemGroup>. 553 // This is because a shallow clone would attempt to add the same item 554 // elements to two different parent <ItemGroup> elements, and this is 555 // not allowed. 556 ErrorUtilities.VerifyThrowInvalidOperation(deepClone, "ShallowCloneNotAllowed"); 557 558 // Do not set the ParentProject on the cloned BuildItemGroup, because it isn't really 559 // part of the project. 560 clone = new BuildItemGroup(xml.OwnerDocument, importedFromAnotherProject, null); 561 clone.Condition = Condition; 562 } 563 else 564 { 565 clone = new BuildItemGroup(); 566 } 567 568 // Loop through every BuildItem in our collection, and add those same Items 569 // to the cloned collection. 570 571 clone.EnsureCapacity(this.Count); // PERF: important to pre-size 572 573 foreach (BuildItem item in this) 574 { 575 // If the caller requested a deep clone, then clone the BuildItem object, 576 // and add the new BuildItem to the new BuildItemGroup. Otherwise, just add 577 // a reference to the existing BuildItem object to the new BuildItemGroup. 578 clone.AddItem(deepClone ? item.Clone() : item); 579 } 580 581 return clone; 582 } 583 584 /// <summary> 585 /// Does a shallow clone, creating a new group with pointers to the same items as the old group. 586 /// </summary> ShallowClone()587 internal BuildItemGroup ShallowClone() 588 { 589 return Clone(false /* shallow */); 590 } 591 592 /// <summary> 593 /// Removes all Items from this BuildItemGroup, and also deletes the Condition 594 /// and Name. 595 /// </summary> Clear()596 public void Clear() 597 { 598 MustBeInitialized(); 599 600 if (IsPersisted) 601 { 602 MustNotBeImported(); 603 604 foreach(BuildItem itemToRemove in items) 605 { 606 XmlElement itemElement = itemToRemove.ItemElement; 607 MustHaveThisParentElement(itemToRemove); 608 609 itemElement.ParentNode.RemoveChild(itemElement); 610 itemToRemove.ParentPersistedItemGroup = null; 611 } 612 613 xml.Condition = null; 614 } 615 616 items.Clear(); 617 MarkItemGroupAsDirty(); 618 } 619 620 /// <summary> 621 /// Removes all virtual (intermediate) items from this BuildItemGroup. This 622 /// is used to reset the state of the build back to the initial state, 623 /// when we only knew about the items that were actually declared in the 624 /// project XML. 625 /// </summary> 626 /// <owner>RGoel</owner> RemoveAllIntermediateItems()627 internal void RemoveAllIntermediateItems() 628 { 629 MustBeInitialized(); 630 MustBeVirtual("InvalidInPersistedItemGroup"); 631 632 if (IsBackedUp) 633 { 634 // Revert removes of persisted items 635 items = persistedItemBackup; 636 persistedItemBackup = null; 637 } 638 else 639 { 640 // Delete all virtual (those without XML backing) items. 641 List<BuildItem> itemsToKeep = new List<BuildItem>(items.Count); 642 for (int i = 0; i < items.Count; i++) 643 { 644 if (items[i].IsPartOfProjectManifest) 645 { 646 itemsToKeep.Add(items[i]); 647 } 648 } 649 items = itemsToKeep; 650 } 651 652 // Revert changes to persisted items' metadata 653 for (int i = 0; i < items.Count; i++) 654 { 655 items[i].RevertToPersistedMetadata(); 656 } 657 658 MarkItemGroupAsDirty(); 659 } 660 661 /// <summary> 662 /// Marks the parent project as dirty. 663 /// </summary> MarkItemGroupAsDirty()664 private void MarkItemGroupAsDirty() 665 { 666 if (parentProject != null) 667 { 668 parentProject.MarkProjectAsDirty(); 669 } 670 } 671 672 /// <summary> 673 /// Create a secret backup list of our persisted items only. 674 /// Then, we can revert back to this later when we're done with the build, 675 /// and we want to remove any virtual items and revert any removes of 676 /// persisted items. 677 /// </summary> BackupPersistedItems()678 internal void BackupPersistedItems() 679 { 680 if (!IsBackedUp) 681 { 682 persistedItemBackup = new List<BuildItem>(); 683 684 foreach (BuildItem item in items) 685 { 686 if (item.IsPartOfProjectManifest) 687 { 688 BuildItem itemClone = item.Clone(); 689 persistedItemBackup.Add(itemClone); 690 } 691 } 692 } 693 } 694 695 /// <summary> 696 /// Call this method to verify that this item group is a well-formed 697 /// virtual item group. 698 /// </summary> MustBeVirtual(string errorResourceName)699 private void MustBeVirtual(string errorResourceName) 700 { 701 ErrorUtilities.VerifyThrowInvalidOperation(!IsPersisted, errorResourceName); 702 } 703 704 /// <summary> 705 /// Returns whether this is a persisted group. 706 /// </summary> 707 internal bool IsPersisted 708 { 709 get { return isPersisted; } 710 } 711 712 /// <summary> 713 /// Returns whether the persisted items have been backed up for later 714 /// recovery. 715 /// </summary> 716 internal bool IsBackedUp 717 { 718 get { return (persistedItemBackup != null); } 719 } 720 721 /// <summary> 722 /// Verifies this is a persisted group. 723 /// </summary> MustBePersisted(string errorResourceName)724 private void MustBePersisted(string errorResourceName) 725 { 726 ErrorUtilities.VerifyThrowInvalidOperation(IsPersisted, errorResourceName); 727 } 728 729 /// <summary> 730 /// Verifies this is not an imported item group. 731 /// </summary> MustNotBeImported()732 private void MustNotBeImported() 733 { 734 ErrorUtilities.VerifyThrowInvalidOperation(!importedFromAnotherProject, "CannotModifyImportedProjects"); 735 } 736 737 /// <summary> 738 /// Verifies that the list of items has been created. 739 /// </summary> MustBeInitialized()740 private void MustBeInitialized() 741 { 742 ErrorUtilities.VerifyThrow(this.items != null, "BuildItemGroup has not been initialized."); 743 } 744 745 /// <summary> 746 /// Verifies that the item's parent element is indeed this item group's element. 747 /// </summary> MustHaveThisParentElement(BuildItem item)748 private void MustHaveThisParentElement(BuildItem item) 749 { 750 ErrorUtilities.VerifyThrowInvalidOperation(item != null && item.ItemElement != null && item.ItemElement.ParentNode == xml.Element, "ItemDoesNotBelongToItemGroup"); 751 } 752 753 /// <summary> 754 /// Verifies the parent item group is indeed this item group. 755 /// </summary> 756 /// <param name="item"></param> MustHaveThisParentGroup(BuildItem item)757 private void MustHaveThisParentGroup(BuildItem item) 758 { 759 ErrorUtilities.VerifyThrow(item.ParentPersistedItemGroup == this, "This item doesn't belong to this itemgroup"); 760 } 761 762 /// <summary> 763 /// Evaluates an item group that's *outside* of a Target. 764 /// Metadata is not allowed on conditions, and we against the parent project. 765 /// </summary> Evaluate( BuildPropertyGroup existingProperties, Hashtable existingItemsByName, bool collectItemsIgnoringCondition, bool collectItemsRespectingCondition, ProcessingPass pass )766 internal void Evaluate 767 ( 768 BuildPropertyGroup existingProperties, 769 Hashtable existingItemsByName, 770 bool collectItemsIgnoringCondition, 771 bool collectItemsRespectingCondition, 772 ProcessingPass pass 773 ) 774 { 775 ErrorUtilities.VerifyThrow(pass == ProcessingPass.Pass2, "Pass should be Pass2 for ItemGroups."); 776 ErrorUtilities.VerifyThrow(collectItemsIgnoringCondition || collectItemsRespectingCondition, "collectItemsIgnoringCondition and collectItemsRespectingCondition can't both be false."); 777 778 Expander expander = new Expander(existingProperties, existingItemsByName, ExpanderOptions.ExpandAll); 779 780 bool itemGroupCondition = Utilities.EvaluateCondition(Condition, 781 (IsPersisted ? xml.ConditionAttribute : null), 782 expander, 783 ParserOptions.AllowPropertiesAndItemLists, 784 parentProject); 785 786 if (!itemGroupCondition && !collectItemsIgnoringCondition) 787 { 788 // Neither list needs updating 789 return; 790 } 791 792 foreach (BuildItem currentItem in this) 793 { 794 bool itemCondition = Utilities.EvaluateCondition(currentItem.Condition, 795 currentItem.ConditionAttribute, 796 expander, 797 ParserOptions.AllowPropertiesAndItemLists, 798 parentProject); 799 800 if (!itemCondition && !collectItemsIgnoringCondition) 801 { 802 // Neither list needs updating 803 continue; 804 } 805 806 if (collectItemsIgnoringCondition) 807 { 808 // Since we're re-evaluating the project, clear out the previous list of child items 809 // for each persisted item tag. 810 currentItem.ChildItems.Clear(); 811 } 812 813 currentItem.EvaluateAllItemMetadata(expander, ParserOptions.AllowPropertiesAndItemLists, parentProject.ParentEngine.LoggingServices, parentProject.ProjectBuildEventContext); 814 BuildItemGroup items = BuildItemGroup.ExpandItemIntoItems(parentProject.ProjectDirectory, currentItem, expander, false /* do not expand metadata */); 815 816 foreach (BuildItem item in items) 817 { 818 BuildItem newItem = BuildItem.CreateClonedParentedItem(item, currentItem); 819 820 if (itemGroupCondition && itemCondition && collectItemsRespectingCondition) 821 { 822 parentProject.AddToItemListByName(newItem); 823 } 824 825 if (collectItemsIgnoringCondition) 826 { 827 parentProject.AddToItemListByNameIgnoringCondition(newItem); 828 829 // Set up the other half of the parent/child relationship. 830 newItem.ParentPersistedItem.ChildItems.AddItem(newItem); 831 } 832 } 833 } 834 } 835 836 /// <summary> 837 /// Processes the "include" list and the "exclude" list for an item element, and returns 838 /// the resultant virtual group of items. Ignores any condition on the item. 839 /// </summary> 840 /// <param name="baseDirectory">Where relative paths should be evaluated from</param> 841 /// <param name="originalItem">The "mother" item that's being expanded</param> 842 /// <param name="expander">Expander to evaluated items and properties</param> 843 /// <param name="expandMetadata">Whether metadata expressions should be expanded, or left as literals</param> ExpandItemIntoItems( string baseDirectory, BuildItem originalItem, Expander expander, bool expandMetadata )844 internal static BuildItemGroup ExpandItemIntoItems 845 ( 846 string baseDirectory, 847 BuildItem originalItem, 848 Expander expander, 849 bool expandMetadata 850 ) 851 { 852 ErrorUtilities.VerifyThrow(originalItem != null, "Can't add a null item to project."); 853 ErrorUtilities.VerifyThrow(expander != null, "expander can't be null."); 854 855 // Take the entire string specified in the "Include" attribute, and split 856 // it up by semi-colons. Then loop over the individual pieces. 857 // Expand only with properties first, so that expressions like Include="@(foo)" will transfer the metadata of the "foo" items as well, not just their item specs. 858 List<string> itemIncludePieces = (new Expander(expander, ExpanderOptions.ExpandProperties).ExpandAllIntoStringListLeaveEscaped(originalItem.Include, originalItem.IncludeAttribute)); 859 BuildItemGroup itemsToInclude = new BuildItemGroup(); 860 for (int i = 0; i < itemIncludePieces.Count; i++) 861 { 862 BuildItemGroup itemizedGroup = expander.ExpandSingleItemListExpressionIntoItemsLeaveEscaped(itemIncludePieces[i], originalItem.IncludeAttribute); 863 if (itemizedGroup == null) 864 { 865 // The expression did not represent a single @(...) item list reference. 866 if (expandMetadata) 867 { 868 // We're inside a target: metadata expressions like %(foo) are legal, so expand them now 869 itemIncludePieces[i] = expander.ExpandMetadataLeaveEscaped(itemIncludePieces[i]); 870 } 871 // Now it's a string constant, possibly with wildcards. 872 // Take each individual path or file expression, and expand any 873 // wildcards. Then loop through each file returned. 874 if (itemIncludePieces[i].Length > 0) 875 { 876 string[] includeFileList = EngineFileUtilities.GetFileListEscaped(baseDirectory, itemIncludePieces[i]); 877 for (int j = 0; j < includeFileList.Length; j++) 878 { 879 BuildItem newItem = itemsToInclude.AddNewItem(originalItem.Name, originalItem.Include); 880 newItem.SetEvaluatedItemSpecEscaped(itemIncludePieces[i]); // comes from XML include --- "arbitrarily escaped" 881 newItem.SetFinalItemSpecEscaped(includeFileList[j]); // comes from file system matcher -- "canonically escaped" 882 } 883 } 884 } 885 else 886 { 887 itemsToInclude.ImportItems(itemizedGroup); 888 } 889 } 890 891 List<BuildItem> matchingItems = FindItemsMatchingSpecification(itemsToInclude, originalItem.Exclude, originalItem.ExcludeAttribute, expander, baseDirectory); 892 893 if (matchingItems != null) 894 { 895 foreach (BuildItem item in matchingItems) 896 { 897 itemsToInclude.RemoveItem(item); 898 } 899 } 900 901 return itemsToInclude; 902 } 903 904 /// <summary> 905 /// Returns a list of all items in the provided item group whose itemspecs match the specification, after it is split and any wildcards are expanded. 906 /// If not items match, returns null. 907 /// </summary> FindItemsMatchingSpecification(BuildItemGroup items, string specification, XmlAttribute attribute, Expander expander, string baseDirectory)908 internal static List<BuildItem> FindItemsMatchingSpecification(BuildItemGroup items, string specification, XmlAttribute attribute, Expander expander, string baseDirectory) 909 { 910 if (items.Count == 0 || specification.Length == 0) 911 { 912 return null; 913 } 914 915 // This is a hashtable whose key is the filename for the individual items 916 // in the Exclude list, after wildcard expansion. The value in the hash table 917 // is just an empty string. 918 Hashtable specificationsToFind = new Hashtable(StringComparer.OrdinalIgnoreCase); 919 920 // Split by semicolons 921 List<string> specificationPieces = expander.ExpandAllIntoStringListLeaveEscaped(specification, attribute); 922 923 foreach (string piece in specificationPieces) 924 { 925 // Take each individual path or file expression, and expand any 926 // wildcards. Then loop through each file returned, and add it 927 // to our hashtable. 928 929 // Don't unescape wildcards just yet - if there were any escaped, the caller wants to treat them 930 // as literals. Everything else is safe to unescape at this point, since we're only matching 931 // against the file system. 932 string[] fileList = EngineFileUtilities.GetFileListEscaped(baseDirectory, piece); 933 934 foreach (string file in fileList) 935 { 936 // Now unescape everything, because this is the end of the road for this filename. 937 // We're just going to compare it to the unescaped include path to filter out the 938 // file excludes. 939 specificationsToFind[EscapingUtilities.UnescapeAll(file)] = String.Empty; 940 } 941 } 942 943 if (specificationsToFind.Count == 0) 944 { 945 return null; 946 } 947 948 // Now loop through our list and filter out any that match a 949 // filename in the remove list. 950 List<BuildItem> itemsRemoved = new List<BuildItem>(); 951 952 foreach (BuildItem item in items) 953 { 954 // Even if the case for the excluded files is different, they 955 // will still get excluded, as expected. However, if the excluded path 956 // references the same file in a different way, such as by relative 957 // path instead of absolute path, we will not realize that they refer 958 // to the same file, and thus we will not exclude it. 959 if (specificationsToFind.ContainsKey(item.FinalItemSpec)) 960 { 961 itemsRemoved.Add(item); 962 } 963 } 964 965 return itemsRemoved; 966 } 967 968 #endregion 969 } 970 } 971