Skip to content

Commit

Permalink
[msbuild] Add support for bundling original resources in libraries. F…
Browse files Browse the repository at this point in the history
…ixes #19028.

If a library references resources, until now we've pre-compile/pre-processed
some of those before embedding them the library. This applies to resources of
the following item groups:

* AtlasTexture
* BundleResource
* Collada
* CoreMLModel
* ImageAsset
* InterfaceDefinition
* SceneKitAsset

However, pre-processing resources as a few problems:

* It requires a native (Xcode) toolchain.
    * This is unfortunate when building from Windows: the current approach is
      that when building a library as a referenced project, the remoting part
      is skipped, so all such resources are just dropped.
    * It also means building on Linux doesn't work.
* It makes it impossible to merge resources with the same name, if we wanted
  to do that.

So I'm adding support for bundling the original resources in library projects.

This is enabled using the MSBuild property `BundleOriginalResources=true`,
which is turned off by default for .NET 8 and turned on by default for .NET 9.

Fixes #19028.
  • Loading branch information
rolfbjarne committed Jul 5, 2024
1 parent 71d6803 commit 65f6828
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 31 deletions.
1 change: 1 addition & 0 deletions dotnet/targets/Xamarin.Shared.Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@
<!-- outer build for multi-rid build -->
<BuildDependsOn Condition="'$(RuntimeIdentifiers)' != ''">
_ErrorRuntimeIdentifiersClash;
BuildOnlySettings;
_CollectBundleResources;
_RunRidSpecificBuild;
_DetectAppManifest;
Expand Down
6 changes: 6 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/CollectBundleResources.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Collections.Generic;
using System.Linq;

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
Expand Down Expand Up @@ -31,6 +33,8 @@ public class CollectBundleResources : XamarinTask, ICancelableTask {
[Output]
public ITaskItem [] BundleResourcesWithLogicalNames { get; set; } = Array.Empty<ITaskItem> ();

public ITaskItem [] UnpackedResources { get; set; } = Array.Empty<ITaskItem> ();

#endregion

static bool CanOptimize (string path)
Expand Down Expand Up @@ -93,6 +97,8 @@ bool ExecuteImpl ()
bundleResources.Add (bundleResource);
}

bundleResources.AddRange (UnpackedResources);

BundleResourcesWithLogicalNames = bundleResources.ToArray ();

return !Log.HasLoggedErrors;
Expand Down
83 changes: 83 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/CollectPackLibraryResources.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.IO;
using System.Collections.Generic;

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Xamarin.Localization.MSBuild;
using Xamarin.Messaging.Build.Client;

namespace Xamarin.MacDev.Tasks {
// This task will collect several item groups with various types of assets/resources,
// add/compute the LogicalName value for each of them, and then add them to the
// ItemsWithLogicalNames item group. The items in this item group will have the
// 'OriginalItemGroup' metadata set indicating where they came from.
public class CollectPackLibraryResources : XamarinTask {
#region Inputs

public ITaskItem [] AtlasTextures { get; set; } = Array.Empty<ITaskItem> ();

public ITaskItem [] BundleResources { get; set; } = Array.Empty<ITaskItem> ();

public ITaskItem [] ImageAssets { get; set; } = Array.Empty<ITaskItem> ();

public ITaskItem [] InterfaceDefinitions { get; set; } = Array.Empty<ITaskItem> ();

public ITaskItem [] ColladaAssets { get; set; } = Array.Empty<ITaskItem> ();

public ITaskItem [] CoreMLModels { get; set; } = Array.Empty<ITaskItem> ();

public ITaskItem [] SceneKitAssets { get; set; } = Array.Empty<ITaskItem> ();

[Required]
public string ProjectDir { get; set; } = string.Empty;

[Required]
public string ResourcePrefix { get; set; } = string.Empty;

#endregion

#region Outputs

// These items will have the following metadata set:
// * LogicalName
// * OriginalItemGroup: the name of the originating item group
[Output]
public ITaskItem [] ItemsWithLogicalNames { get; set; } = Array.Empty<ITaskItem> ();

#endregion

public override bool Execute ()
{
var prefixes = BundleResource.SplitResourcePrefixes (ResourcePrefix);
var rv = new List<ITaskItem> ();

var resources = new [] {
new { Name = "AtlasTexture", Items = AtlasTextures },
new { Name = "BundleResource", Items = BundleResources },
new { Name = "Collada", Items = ColladaAssets },
new { Name = "CoreMLModel", Items = CoreMLModels },
new { Name = "ImageAsset", Items = ImageAssets },
new { Name = "InterfaceDefinition", Items = InterfaceDefinitions },
new { Name = "SceneKitAsset", Items = SceneKitAssets },
};

foreach (var kvp in resources) {
var itemName = kvp.Name;
var items = kvp.Items;

foreach (var item in items) {
if (!CollectBundleResources.TryCreateItemWithLogicalName (this, item, ProjectDir, prefixes, SessionId, out var itemWithLogicalName))
continue;

itemWithLogicalName.SetMetadata ("OriginalItemGroup", itemName);
rv.Add (itemWithLogicalName);
}
}

ItemsWithLogicalNames = rv.ToArray ();

return !Log.HasLoggedErrors;
}
}
}
24 changes: 24 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/PackLibraryResources.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Collections.Generic;
Expand All @@ -17,6 +18,8 @@ public class PackLibraryResources : XamarinTask, ITaskCallback, ICancelableTask

public ITaskItem [] BundleResourcesWithLogicalNames { get; set; } = Array.Empty<ITaskItem> ();

public ITaskItem [] BundleOriginalResourcesWithLogicalNames { get; set; } = Array.Empty<ITaskItem> ();

#endregion

#region Outputs
Expand Down Expand Up @@ -96,11 +99,32 @@ public override bool Execute ()
results.Add (embedded);
}

foreach (var item in BundleOriginalResourcesWithLogicalNames) {
var originalItemGroup = item.GetMetadata ("OriginalItemGroup");
if (!TryGetMangledLogicalName (item, originalItemGroup, out var mangledLogicalName))
continue;
var embedded = new TaskItem (item);
embedded.SetMetadata ("LogicalName", mangledLogicalName);
results.Add (embedded);
}

EmbeddedResources = results.ToArray ();

return !Log.HasLoggedErrors;
}

bool TryGetMangledLogicalName (ITaskItem item, string itemName, [NotNullWhen (true)] out string? mangled)
{
var logicalName = item.GetMetadata ("LogicalName");
if (string.IsNullOrEmpty (logicalName)) {
Log.LogError (null, null, null, item.ItemSpec, 0, 0, 0, 0, MSBStrings.E0161);
mangled = null;
return false;
}
mangled = "__" + Prefix + "_item_" + itemName + "_" + EscapeMangledResource (logicalName);
return true;
}

public void Cancel ()
{
if (ShouldExecuteRemotely ())
Expand Down
161 changes: 141 additions & 20 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/UnpackLibraryResources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,36 @@ public class UnpackLibraryResources : XamarinTask, ITaskCallback, ICancelableTas
[Output]
public ITaskItem [] UnpackedResources { get; set; } = Array.Empty<ITaskItem> ();

[Output]
public ITaskItem [] AtlasTextures { get; set; } = Array.Empty<ITaskItem> ();

[Output]
public ITaskItem [] ColladaAssets { get; set; } = Array.Empty<ITaskItem> ();

[Output]
public ITaskItem [] CoreMLModels { get; set; } = Array.Empty<ITaskItem> ();

[Output]
public ITaskItem [] ImageAssets { get; set; } = Array.Empty<ITaskItem> ();

[Output]
public ITaskItem [] InterfaceDefinitions { get; set; } = Array.Empty<ITaskItem> ();

[Output]
public ITaskItem [] SceneKitAssets { get; set; } = Array.Empty<ITaskItem> ();

#endregion

enum ResourceType {
AtlasTexture,
BundleResource,
ColladaAsset,
CoreMLModel,
ImageAsset,
InterfaceDefinition,
SceneKitAsset,
}

public override bool Execute ()
{
try {
Expand Down Expand Up @@ -78,7 +106,13 @@ bool ExecuteImpl ()
return result;
}

var results = new List<ITaskItem> ();
var bundleResources = new List<ITaskItem> ();
var atlasTextures = new List<ITaskItem> ();
var colladaAssets = new List<ITaskItem> ();
var coreMLModels = new List<ITaskItem> ();
var imageAssets = new List<ITaskItem> ();
var interfaceDefinitions = new List<ITaskItem> ();
var sceneKitAssets = new List<ITaskItem> ();

foreach (var asm in ReferencedLibraries) {
// mscorlib.dll was not coming out with ResolvedFrom == {TargetFrameworkDirectory}
Expand All @@ -89,15 +123,46 @@ bool ExecuteImpl ()
var perAssemblyOutputPath = Path.Combine (IntermediateOutputPath, "unpack", asm.GetMetadata ("Filename"));
var extracted = ExtractContentAssembly (asm.ItemSpec, perAssemblyOutputPath).ToArray ();

results.AddRange (extracted);

var itemsFile = asm.GetMetadata ("ItemsFile");
itemsFile = itemsFile.Replace ('\\', Path.DirectorySeparatorChar);
WriteItemsToFile.Write (this, itemsFile, extracted, "_BundleResourceWithLogicalName", true, true);
foreach (var tuple in extracted) {
var resourceType = tuple.Type;
var item = tuple.Item;
switch (resourceType) {
case ResourceType.AtlasTexture:
atlasTextures.Add (item);
break;
case ResourceType.BundleResource:
bundleResources.Add (item);
break;
case ResourceType.ColladaAsset:
colladaAssets.Add (item);
break;
case ResourceType.CoreMLModel:
coreMLModels.Add (item);
break;
case ResourceType.ImageAsset:
imageAssets.Add (item);
break;
case ResourceType.InterfaceDefinition:
interfaceDefinitions.Add (item);
break;
case ResourceType.SceneKitAsset:
sceneKitAssets.Add (item);
break;
default:
Log.LogError ($"Unknown resource type: {resourceType}"); // FIXME: better error.
break;
}
}
}
}

BundleResourcesWithLogicalNames = results.ToArray ();
BundleResourcesWithLogicalNames = bundleResources.ToArray ();
AtlasTextures = atlasTextures.ToArray ();
ColladaAssets = colladaAssets.ToArray ();
CoreMLModels = coreMLModels.ToArray ();
ImageAssets = imageAssets.ToArray ();
InterfaceDefinitions = interfaceDefinitions.ToArray ();
SceneKitAssets = sceneKitAssets.ToArray ();
UnpackedResources = unpackedResources.ToArray ();

return !Log.HasLoggedErrors;
Expand All @@ -113,7 +178,7 @@ bool IsFrameworkAssembly (ITaskItem asm)
return false;
}

IEnumerable<ITaskItem> ExtractContentAssembly (string assembly, string intermediatePath)
IEnumerable<(ResourceType Type, ITaskItem Item)> ExtractContentAssembly (string assembly, string intermediatePath)
{
if (!File.Exists (assembly)) {
Log.LogMessage (MessageImportance.Low, $"Not inspecting assembly because it doesn't exist: {assembly}");
Expand All @@ -122,20 +187,75 @@ IEnumerable<ITaskItem> ExtractContentAssembly (string assembly, string intermedi

var asmWriteTime = File.GetLastWriteTimeUtc (assembly);
var manifestResources = GetAssemblyManifestResources (assembly).ToArray ();
if (!manifestResources.Any ())
// Log.LogMessage (MessageImportance.Low, $"Inspecting assembly with {manifestResources.Length} resources: {assembly}");
if (!manifestResources.Any ()) {
Log.LogMessage (MessageImportance.Low, $" No resources found in: {assembly}");
yield break;
}

Log.LogMessage (MessageImportance.Low, $"Inspecting assembly with {manifestResources.Length} resources: {assembly}");
// Log.LogMessage (MessageImportance.Low, " Searching resources in assembly: {0}", assembly);
foreach (var embedded in manifestResources) {
string rpath;

if (embedded.Name.StartsWith ("__" + Prefix + "_content_", StringComparison.Ordinal)) {
var mangled = embedded.Name.Substring (("__" + Prefix + "_content_").Length);
rpath = UnmangleResource (mangled);
} else if (embedded.Name.StartsWith ("__" + Prefix + "_page_", StringComparison.Ordinal)) {
var mangled = embedded.Name.Substring (("__" + Prefix + "_page_").Length);
rpath = UnmangleResource (mangled);
} else {
var resourceName = embedded.Name;
var startsWith = "__" + Prefix + "_";
if (!resourceName.StartsWith (startsWith, StringComparison.Ordinal)) {
Log.LogMessage (MessageImportance.Low, $" Not applicable resource (does not match prefix): {resourceName}");
continue;
}

var underscoreIndex = resourceName.IndexOf ('_', startsWith.Length);
if (underscoreIndex == -1) {
Log.LogMessage (MessageImportance.Low, $" Not applicable resource (no content type found): {resourceName}");
continue;
}
var contentType = resourceName.Substring (startsWith.Length, underscoreIndex - startsWith.Length);
var contentValue = resourceName.Substring (underscoreIndex + 1);
ResourceType resourceType;
switch (contentType) {
case "content":
case "page":
rpath = UnmangleResource (contentValue);
resourceType = ResourceType.BundleResource;
break;
case "item":
var itemUnderscoreIndex = contentValue.IndexOf ('_');
if (itemUnderscoreIndex == -1) {
Log.LogMessage (MessageImportance.Low, $" Not applicable resource (no item type in '{contentValue}'): {resourceName}");
continue;
}
var itemType = contentValue.Substring (0, itemUnderscoreIndex);
var itemValue = contentValue.Substring (itemUnderscoreIndex + 1);
rpath = UnmangleResource (itemValue);
switch (itemType) {
case "AtlasTexture":
resourceType = ResourceType.AtlasTexture;
break;
case "BundleResource":
resourceType = ResourceType.BundleResource;
break;
case "Collada":
resourceType = ResourceType.ColladaAsset;
break;
case "CoreMLModel":
resourceType = ResourceType.CoreMLModel;
break;
case "ImageAsset":
resourceType = ResourceType.ImageAsset;
break;
case "InterfaceDefinition":
resourceType = ResourceType.InterfaceDefinition;
break;
case "SceneKitAsset":
resourceType = ResourceType.SceneKitAsset;
break;
default:
Log.LogMessage (MessageImportance.Low, $" Not applicable resource (unknown item type in '{itemType}'): {resourceName}");
continue;
}
break;
default:
Log.LogMessage (MessageImportance.Low, $" Not applicable resource (unknown content type '{contentType}'): {resourceName}");
continue;
}

Expand All @@ -145,11 +265,12 @@ IEnumerable<ITaskItem> ExtractContentAssembly (string assembly, string intermedi
var item = new TaskItem (path);
item.SetMetadata ("LogicalName", rpath);
item.SetMetadata ("Optimize", "false");
item.SetMetadata ("BundledInAssembly", assembly);

if (file.Exists && file.LastWriteTimeUtc >= asmWriteTime) {
Log.LogMessage (" Up to date: {0}", rpath);
Log.LogMessage ($" Up to date (contentType: {contentType} resourceType: {resourceType}: {path}");
} else {
Log.LogMessage (" Unpacking: {0}", rpath);
Log.LogMessage ($" Unpacking (contentType: {contentType} resourceType: {resourceType}: {path}");

Directory.CreateDirectory (Path.GetDirectoryName (path));

Expand All @@ -161,7 +282,7 @@ IEnumerable<ITaskItem> ExtractContentAssembly (string assembly, string intermedi
unpackedResources.Add (item);
}

yield return item;
yield return (resourceType, item);
}

yield break;
Expand Down
Loading

0 comments on commit 65f6828

Please sign in to comment.