[msbuild] Improve the UnpackLibraryResources task a bit. (#20004)

* Unpack into a per-assembly directory. This way identically named resources from
  different assemblies won't overwrite eachother.

* Remove logic to detect overwriting of existing bundle resources, because it's broken.

     In particular this piece of code is missing a single character (there should be a `!` in the condition):

     ```cs
     if (string.IsNullOrEmpty (logicalName))
         ignore.Add (logicalName);
     ```

    This means we're only ignoring bundle resources without LogicalName, which shouldn't
    happen. So just remove the code, since it doesn't do anything useful. A proper fix
    for resource collision is in progress and will come in a separate pull request.

* Fix task to work for incremental builds. Previously we'd detect that any resources
  had been extracted and not re-extract, but we wouldn't add those extracted resources
  to the `_BundleResourceWithLogicalName` item group. The end result would be that
  on incremental builds, any bundled resources wouldn't be picked up at all. Typically
  this wouldn't be a problem (because the resources would be in the resulting app
  bundle), but doing anything out of the ordinary could cause problems. The fix is
  to write the unpacked items + their metadata to a file, and load those when we
  detect that no re-extraction is necessary (this file also does double duty as a
  stamp file in the code).

* Augment `WriteItemsToFile` to make its `Write` method accessible from different classes.

* Improve logging a bit.
This commit is contained in:
Rolf Bjarne Kvinge 2024-04-24 09:40:45 +02:00 коммит произвёл GitHub
Родитель 8b3ce01c2c
Коммит 70a5d0d09a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
3 изменённых файлов: 106 добавлений и 56 удалений

Просмотреть файл

@ -23,9 +23,6 @@ namespace Xamarin.MacDev.Tasks {
[Required]
public string Prefix { get; set; } = string.Empty;
[Required]
public ITaskItem [] NoOverwrite { get; set; } = Array.Empty<ITaskItem> ();
[Required]
public string IntermediateOutputPath { get; set; } = string.Empty;
@ -45,6 +42,10 @@ namespace Xamarin.MacDev.Tasks {
[Output]
public ITaskItem []? BundleResourcesWithLogicalNames { get; set; }
// This is required to copy the .items file back to Windows for remote builds.
[Output]
public ITaskItem [] ItemsFiles { get; set; } = Array.Empty<ITaskItem> ();
[Output]
public ITaskItem [] UnpackedResources { get; set; } = Array.Empty<ITaskItem> ();
@ -77,10 +78,7 @@ namespace Xamarin.MacDev.Tasks {
return result;
}
// TODO: give each assembly its own intermediate output directory
// TODO: use list file to avoid re-extracting assemblies but allow FileWrites to work
var results = new List<ITaskItem> ();
HashSet<string>? ignore = null;
foreach (var asm in ReferencedLibraries) {
// mscorlib.dll was not coming out with ResolvedFrom == {TargetFrameworkDirectory}
@ -88,29 +86,14 @@ namespace Xamarin.MacDev.Tasks {
if (IsFrameworkAssembly (asm)) {
Log.LogMessage (MessageImportance.Low, MSBStrings.M0168, asm.ItemSpec);
} else {
var extracted = ExtractContentAssembly (asm.ItemSpec, IntermediateOutputPath);
var perAssemblyOutputPath = Path.Combine (IntermediateOutputPath, "unpack", asm.GetMetadata ("Filename"));
var extracted = ExtractContentAssembly (asm.ItemSpec, perAssemblyOutputPath).ToArray ();
foreach (var bundleResource in extracted) {
string logicalName;
results.AddRange (extracted);
if (ignore is null) {
// Create a hashset of the bundle resources that should not be overwritten by extracted resources
// from the referenced assemblies.
//
// See https://bugzilla.xamarin.com/show_bug.cgi?id=8409 for details.
ignore = new HashSet<string> ();
foreach (var item in NoOverwrite) {
logicalName = item.GetMetadata ("LogicalName");
if (string.IsNullOrEmpty (logicalName))
ignore.Add (logicalName);
}
}
logicalName = bundleResource.GetMetadata ("LogicalName");
if (!ignore.Contains (logicalName))
results.Add (bundleResource);
}
var itemsFile = asm.GetMetadata ("ItemsFile");
itemsFile = itemsFile.Replace ('\\', Path.DirectorySeparatorChar);
WriteItemsToFile.Write (this, itemsFile, extracted, "_BundleResourceWithLogicalName", true, true);
}
}
@ -132,14 +115,18 @@ namespace Xamarin.MacDev.Tasks {
IEnumerable<ITaskItem> ExtractContentAssembly (string assembly, string intermediatePath)
{
Log.LogMessage (MessageImportance.Low, " Inspecting assembly: {0}", assembly);
if (!File.Exists (assembly))
if (!File.Exists (assembly)) {
Log.LogMessage (MessageImportance.Low, $"Not inspecting assembly because it doesn't exist: {assembly}");
yield break;
}
var asmWriteTime = File.GetLastWriteTimeUtc (assembly);
var manifestResources = GetAssemblyManifestResources (assembly).ToArray ();
if (!manifestResources.Any ())
yield break;
foreach (var embedded in GetAssemblyManifestResources (assembly)) {
Log.LogMessage (MessageImportance.Low, $"Inspecting assembly with {manifestResources.Length} resources: {assembly}");
foreach (var embedded in manifestResources) {
string rpath;
if (embedded.Name.StartsWith ("__" + Prefix + "_content_", StringComparison.Ordinal)) {
@ -243,15 +230,12 @@ namespace Xamarin.MacDev.Tasks {
if (item.IsFrameworkItem ())
return false;
if (NoOverwrite is not null && NoOverwrite.Contains (item))
return false;
return true;
}
public bool ShouldCreateOutputFile (ITaskItem item) => UnpackedResources.Contains (item) == true;
public IEnumerable<ITaskItem> GetAdditionalItemsToBeCopied () => Enumerable.Empty<ITaskItem> ();
public IEnumerable<ITaskItem> GetAdditionalItemsToBeCopied () => ItemsFiles;
IEnumerable<ManifestResource> GetAssemblyManifestResources (string fileName)
{

Просмотреть файл

@ -34,37 +34,41 @@ namespace Xamarin.MacDev.Tasks {
public override bool Execute ()
{
var items = this.Items;
if (items is null)
items = new ITaskItem [0];
Write (this, File?.ItemSpec, Items, ItemName, Overwrite, IncludeMetadata);
return true;
}
public static void Write (Task task, string? file, IEnumerable<ITaskItem> items, string itemName, bool overwrite, bool includeMetadata)
{
if (file is null) {
task.Log.LogWarning ($"No output file to write to for item {itemName}");
return;
}
var document = new XDocument (
new XElement (ProjectElementName,
new XElement (ItemGroupElementName,
items.Select (item => this.CreateElementFromItem (item)))));
items.Select (item => CreateElementFromItem (item, itemName, includeMetadata)))));
var file = this.File?.ItemSpec;
if (this.Overwrite && System.IO.File.Exists (file))
if (overwrite && System.IO.File.Exists (file))
System.IO.File.Delete (file);
if (!Directory.Exists (Path.GetDirectoryName (file)))
Directory.CreateDirectory (Path.GetDirectoryName (file));
document.Save (file);
return true;
}
private XElement CreateElementFromItem (ITaskItem item)
static XElement CreateElementFromItem (ITaskItem item, string itemName, bool includeMetadata)
{
return new XElement (XmlNs + ItemName,
return new XElement (XmlNs + itemName,
new XAttribute (IncludeAttributeName, item.ItemSpec),
this.CreateMetadataFromItem (item));
CreateMetadataFromItem (item, includeMetadata));
}
private IEnumerable<XElement> CreateMetadataFromItem (ITaskItem item)
static IEnumerable<XElement> CreateMetadataFromItem (ITaskItem item, bool includeMetadata)
{
if (this.IncludeMetadata) {
if (includeMetadata) {
var metadata = item.CloneCustomMetadata ();
return metadata.Keys

Просмотреть файл

@ -1899,13 +1899,28 @@ Copyright (C) 2018 Microsoft. All rights reserved.
</PackLibraryResources>
</Target>
<!-- Unpacking of resources from libraries -->
<PropertyGroup>
<_UnpackLibraryResourcesDependsOn>
$(_UnpackLibraryResourcesDependsOn);
ResolveReferences;
_DetectBuildType;
_CollectBundleResources;
_PrepareUnpackLibraryResources;
_BeforeUnpackLibraryResources;
_ReadUnpackedLibraryResources;
</_UnpackLibraryResourcesDependsOn>
</PropertyGroup>
<Target Name="_PrepareUnpackLibraryResources">
<PropertyGroup>
<_StampDirectory>$(IntermediateOutputPath)resourcestamps\</_StampDirectory>
<_StampDirectory>$(DeviceSpecificIntermediateOutputPath)resourcestamps\</_StampDirectory>
</PropertyGroup>
<ItemGroup>
<_UnpackLibraryResourceItems Include="@(ReferencePath);@(ReferenceDependencyPaths)">
<StampFile>%(FileName).stamp</StampFile>
<StampFile>$(_StampDirectory)%(FileName).stamp</StampFile>
<ItemsFile>$(_StampDirectory)%(FileName).items</ItemsFile>
</_UnpackLibraryResourceItems>
</ItemGroup>
</Target>
@ -1920,30 +1935,77 @@ Copyright (C) 2018 Microsoft. All rights reserved.
</_UnpackLibraryResourcesDependsOn>
</PropertyGroup>
<Target Name="_BeforeUnpackLibraryResources"
Inputs="@(_UnpackLibraryResourceItems)"
Outputs="@(_UnpackLibraryResourceItems -> '%(StampFile)')"
DependsOnTargets="_PrepareUnpackLibraryResources"
>
<!-- If any assemblies is newer than the stamp file, we delete the stamp file and items so that _UnpackLibraryResources runs again -->
<Delete
Condition="'$(IsMacEnabled)' == 'true' Or '$(IsHotRestartBuild)' == 'true'"
SessionId="$(BuildSessionId)"
Files="%(_UnpackLibraryResourceItems.StampFile);%(_UnpackLibraryResourceItems.ItemsFile)"
/>
</Target>
<Target Name="_ReadUnpackedLibraryResources"
DependsOnTargets="_BeforeUnpackLibraryResources">
<!-- If _BeforeUnpackLibraryResources did not delete the generated items lists from the stamp file, then we read them
since that target won't run and we need to the output items that are cached in those files which includes full metadata -->
<!-- This task is always executed on Windows (when building on Windows), because we copy the stamp file to Windows when we create it (no need to go back to the Mac to read it)-->
<ReadItemsFromFile
File="@(_UnpackLibraryResourceItems -> '%(ItemsFile)')"
Condition="Exists('%(ItemsFile)')"
>
<Output TaskParameter="Items" ItemName="_PreviouslyUnpackedBundleResourceWithLogicalName" />
</ReadItemsFromFile>
<ItemGroup>
<FileWrites Include="@(_PreviouslyUnpackedBundleResourceWithLogicalName)" />
<FileWrites Include="@(_PreviouslyUnpackedBundleResourceWithLogicalName -> '%(ItemsFile)')" />
<_BundleResourceWithLogicalName Include="@(_PreviouslyUnpackedBundleResourceWithLogicalName)" />
</ItemGroup>
</Target>
<Target Name="_UnpackLibraryResources" Condition="'$(_CanOutputAppBundle)' == 'true'" DependsOnTargets="$(_UnpackLibraryResourcesDependsOn)"
Inputs="@(_UnpackLibraryResourceItems)"
Outputs="@(_UnpackLibraryResourceItems->'$(_StampDirectory)%(StampFile)')">
<MakeDir SessionId="$(BuildSessionId)" Condition="'$(IsMacEnabled)' == 'true' Or '$(IsHotRestartBuild)' == 'true'" Directories="$(_StampDirectory)" />
Outputs="@(_UnpackLibraryResourceItems->'%(StampFile)')">
<MakeDir Directories="$(_StampDirectory)" />
<!-- This task may be executed locally on Windows (for Hot Restart) -->
<!-- Note that ReferenceAssemblies must always be passed all referenced assemblies, even for incremental builds when we're not necessarily unpacking from all assemblies -->
<UnpackLibraryResources
Condition="'$(IsMacEnabled)' == 'true' Or '$(IsHotRestartBuild)' == 'true'"
SessionId="$(BuildSessionId)"
Prefix="$(_EmbeddedResourcePrefix)"
NoOverwrite="@(_BundleResourceWithLogicalName)"
IntermediateOutputPath="$(DeviceSpecificIntermediateOutputPath)"
TargetFrameworkDirectory="$(TargetFrameworkDirectory)"
ReferenceAssemblies="@(ReferencePath);@(ReferenceDependencyPaths)"
ReferencedLibraries="@(_UnpackLibraryResourceItems)">
ReferencedLibraries="@(_UnpackLibraryResourceItems)"
>
<Output TaskParameter="BundleResourcesWithLogicalNames" ItemName="_BundleResourceWithLogicalName" />
<!-- Local items to be persisted to items files -->
<Output TaskParameter="BundleResourcesWithLogicalNames" ItemName="_UnpackedBundleResourceWithLogicalName" />
<Output TaskParameter="ItemsFiles" ItemName="FileWrites" />
</UnpackLibraryResources>
<Touch
SessionId="$(BuildSessionId)"
Condition="'$(IsMacEnabled)' == 'true' Or '$(IsHotRestartBuild)' == 'true'"
Files="@(_UnpackLibraryResourceItems->'$(_StampDirectory)%(StampFile)')"
Files="@(_UnpackLibraryResourceItems->'%(StampFile)')"
AlwaysCreate="True"
>
<Output TaskParameter="TouchedFiles" ItemName="FileWrites" />
</Touch>
<ItemGroup>
<FileWrites Include="@(_UnpackedBundleResourceWithLogicalName)" />
</ItemGroup>
</Target>
<Target Name="_ParseBundlerArguments">