From 19dc9ce0aa28aecb7f07f8322371405e1adc0dfa Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 12 Aug 2021 12:30:57 +0200 Subject: [PATCH] [msbuild] Add a ComputeBundleLocation task --- .../MSBStrings.Designer.cs | 38 +- .../MSBStrings.resx | 30 ++ .../Tasks/ComputeBundleLocationTaskBase.cs | 333 ++++++++++++++++++ .../Tasks/ComputeBundleLocation.cs | 4 + msbuild/Xamarin.Shared/Xamarin.Shared.targets | 1 + 5 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ComputeBundleLocationTaskBase.cs create mode 100644 msbuild/Xamarin.MacDev.Tasks/Tasks/ComputeBundleLocation.cs diff --git a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.Designer.cs b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.Designer.cs index 4922d9ebe9..c59a6b0bff 100644 --- a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.Designer.cs +++ b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.Designer.cs @@ -1948,6 +1948,33 @@ namespace Xamarin.Localization.MSBuild { } } + /// + /// Looks up a localized string similar to The 'PublishFolderType' metadata value '{0}' on the item '{1}' is not recognized. The file will not be copied to the app bundle.. + /// + public static string E7088 { + get { + return ResourceManager.GetString("E7088", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The file '{0}' does not specify a 'PublishFolderType' metadata, and a default value could not be calculated. The file will not be copied to the app bundle.. + /// + public static string E7089 { + get { + return ResourceManager.GetString("E7089", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The 'PublishFolderType' metadata value '{0}' on the item '{1}' is not recognized. The file will not be copied to the app bundle. If the file is not supposed to be copied to the app bundle, remove the '{2}' metadata on the item.. + /// + public static string E7090 { + get { + return ResourceManager.GetString("E7090", resourceCulture); + } + } + /// /// Looks up a localized string similar to File '{0}' is not a valid framework: {1}. /// @@ -1957,6 +1984,15 @@ namespace Xamarin.Localization.MSBuild { } } + /// + /// Looks up a localized string similar to The file or directory '{0}' is not a framework nor a file within a framework.. + /// + public static string E7094 { + get { + return ResourceManager.GetString("E7094", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid framework: {0}. /// @@ -2582,7 +2618,7 @@ namespace Xamarin.Localization.MSBuild { return ResourceManager.GetString("W7091", resourceCulture); } } - + /// /// Looks up a localized string similar to The binding resource package {0} does not exist.. /// diff --git a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx index 5a271a4f61..d7c039c9d8 100644 --- a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx +++ b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx @@ -1348,6 +1348,33 @@ The following are literal names and should not be translated: manifest + + The 'PublishFolderType' metadata value '{0}' on the item '{1}' is not recognized. The file will not be copied to the app bundle. + + PublishFolderType: do not translate (name of metadata) + {0}: metadata value (read from a user file) + {1}: path to a file + + + + + The file '{0}' does not specify a 'PublishFolderType' metadata, and a default value could not be calculated. The file will not be copied to the app bundle. + + PublishFolderType: do not translate (name of metadata) + {0}: path to a file + + + + + The 'PublishFolderType' metadata value '{0}' on the item '{1}' is not recognized. The file will not be copied to the app bundle. If the file is not supposed to be copied to the app bundle, remove the '{2}' metadata on the item. + + PublishFolderType: do not translate (name of metadata) + {0}: metadata value (read from a user file) + {1}: path to a file + {2}: name of metadata (either 'CopyToOutputDirectory' or 'CopyToPublishDirectory') + + + The framework {0} is a framework of static libraries, and will not be copied to the app. @@ -1361,4 +1388,7 @@ The binding resource package {0} does not exist. + + The file or directory '{0}' is not a framework nor a file within a framework. + diff --git a/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ComputeBundleLocationTaskBase.cs b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ComputeBundleLocationTaskBase.cs new file mode 100644 index 0000000000..af0a397fdc --- /dev/null +++ b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ComputeBundleLocationTaskBase.cs @@ -0,0 +1,333 @@ +#nullable enable + +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +using Xamarin.MacDev; +using Xamarin.Utils; +using Xamarin.Localization.MSBuild; + +namespace Xamarin.MacDev.Tasks { + public abstract class ComputeBundleLocationTaskBase : XamarinTask { + // not required because this can be the root directory (so an empty string) + public string AssemblyDirectory { get; set; } = string.Empty; + + public ITaskItem []? BundleResource { get; set; } + public ITaskItem []? Content { get; set; } + public ITaskItem []? EmbeddedResource { get; set; } + + [Required] + public string FrameworksDirectory { get; set; } = string.Empty; + + [Required] + public string PlugInsDirectory { get; set; } = string.Empty; + + [Required] + public string ProjectDir { get; set; } = string.Empty; + + // not required because this can be the root directory (so an empty string) + public string ResourceDirectory { get; set; } = string.Empty; + + [Required] + public ITaskItem []? ResolvedFileToPublish { get; set; } + + [Output] + public ITaskItem []? UpdatedResolvedFileToPublish { get; set; } + + HashSet resourceFilesSet = new HashSet (); + + public override bool Execute () + { + if (ResolvedFileToPublish is null || ResolvedFileToPublish.Length == 0) + return !Log.HasLoggedErrors; + + // Make sure we use the correct path separator, these are relative paths, so it doesn't look + // like MSBuild does the conversion automatically. + FrameworksDirectory = FrameworksDirectory.Replace ('\\', Path.DirectorySeparatorChar); + PlugInsDirectory = PlugInsDirectory.Replace ('\\', Path.DirectorySeparatorChar); + ResourceDirectory = ResourceDirectory.Replace ('\\', Path.DirectorySeparatorChar); + + // Collect all our BundleResource, Content and EmbeddedResource paths into one big dictionary for later lookup. + if (BundleResource?.Length > 0) + resourceFilesSet.UnionWith (BundleResource.Select (v => Path.GetFullPath (v.ItemSpec))); + if (Content?.Length > 0) + resourceFilesSet.UnionWith (Content.Select (v => Path.GetFullPath (v.ItemSpec))); + if (EmbeddedResource?.Length > 0) + resourceFilesSet.UnionWith (EmbeddedResource.Select (v => Path.GetFullPath (v.ItemSpec))); + + var appleFrameworks = new Dictionary> (); + var list = ResolvedFileToPublish.ToList (); + foreach (var item in list.ToArray ()) { // iterate over a copy of the list, because we might modify the original list + // Compute the publish folder type if it's not specified + var publishFolderType = ParsePublishFolderType (item); + if (publishFolderType == PublishFolderType.Unset) { + publishFolderType = ComputePublishFolderType (list, item); + item.SetMetadata ("PublishFolderType", publishFolderType.ToString ()); + } + + // Figure out the relative directory inside the app bundle where the item is supposed to be placed. + var relativePath = string.Empty; + switch (publishFolderType) { + case PublishFolderType.Assembly: + relativePath = AssemblyDirectory; + break; + case PublishFolderType.Resource: + relativePath = ResourceDirectory; + break; + case PublishFolderType.AppleFramework: + if (TryGetFrameworkDirectory (item.ItemSpec, out var frameworkDirectory)) { + if (!appleFrameworks.TryGetValue (frameworkDirectory!, out var items)) + appleFrameworks [frameworkDirectory!] = items = new List (); + items.Add (item); + // Remove AppleFramework entries, we'll add back one entry per framework at the end + list.Remove (item); + continue; + } + Log.LogError (7094, item.ItemSpec, MSBStrings.E7094 /* The file or directory '{0}' is not a framework nor a file within a framework. */, item.ItemSpec); + continue; + case PublishFolderType.CompressedAppleFramework: + relativePath = FrameworksDirectory; + break; + case PublishFolderType.AppleBindingResourcePackage: + case PublishFolderType.CompressedAppleBindingResourcePackage: + // Nothing to do here, this is handled fully in the targets file + break; + case PublishFolderType.PlugIns: + relativePath = PlugInsDirectory; + break; + case PublishFolderType.CompressedPlugIns: + relativePath = PlugInsDirectory; + break; + case PublishFolderType.RootDirectory: + break; + case PublishFolderType.DynamicLibrary: + relativePath = AssemblyDirectory; + break; + case PublishFolderType.StaticLibrary: + // Nothing to do here. + continue; + case PublishFolderType.None: + continue; + case PublishFolderType.Unknown: + default: + ReportUnknownPublishFolderType (item); + item.SetMetadata ("PublishFolderType", "None"); + continue; + } + + // Compute the relative path of the item relative to the root of the app bundle + var virtualProjectPath = GetVirtualAppBundlePath (item); + relativePath = Path.Combine (relativePath, virtualProjectPath); + item.SetMetadata ("RelativePath", relativePath); + } + + // We may have multiple input items for each framework, but we only want to return a single + // entry per framework. In the loop above we removed all input items corresponding with a + // framework, so add back a single item here. + foreach (var entry in appleFrameworks) { + var items = entry.Value; + var item = new TaskItem (entry.Key); + item.SetMetadata ("PublishFolderType", "AppleFramework"); + item.SetMetadata ("RelativePath", Path.Combine (FrameworksDirectory, Path.GetFileName (entry.Key))); + list.Add (item); + } + + UpdatedResolvedFileToPublish = list.ToArray (); + + return !Log.HasLoggedErrors; + } + + // Check if the input, or any of it's parent directories is either an *.xcframework, or a *.framework + static bool TryGetFrameworkDirectory (string path, out string? frameworkDirectory) + { + if (string.IsNullOrEmpty (path)) { + frameworkDirectory = null; + return false; + } + + if (path.EndsWith (".xcframework", StringComparison.OrdinalIgnoreCase)) { + frameworkDirectory = path; + return true; + } + + if (path.EndsWith (".framework", StringComparison.OrdinalIgnoreCase)) { + // We might be inside a .xcframework, so check for that first + if (TryGetFrameworkDirectory (Path.GetDirectoryName (path), out var xcframeworkDirectory) && xcframeworkDirectory!.EndsWith (".xcframework", StringComparison.OrdinalIgnoreCase)) { + frameworkDirectory = xcframeworkDirectory; + return true; + } + + frameworkDirectory = path; + return true; + } + + return TryGetFrameworkDirectory (Path.GetDirectoryName (path), out frameworkDirectory); + } + + // Check if the input, or any of it's parent directories is a *.resources directory or a *.resources.zip file next to a *.dll. + static bool IsBindingResourcePackage (string path, out PublishFolderType type) + { + type = PublishFolderType.None; + if (string.IsNullOrEmpty (path)) + return false; + + if (path.EndsWith (".resources", StringComparison.OrdinalIgnoreCase) && File.Exists (Path.ChangeExtension (path, "dll"))) { + type = PublishFolderType.AppleBindingResourcePackage; + return true; + } + + if (path.EndsWith (".resources.zip", StringComparison.OrdinalIgnoreCase) && File.Exists (Path.ChangeExtension (Path.GetFileNameWithoutExtension (path), "dll"))) { + type = PublishFolderType.CompressedAppleBindingResourcePackage; + return true; + } + + return IsBindingResourcePackage (Path.GetDirectoryName (path), out type); + } + + static string GetVirtualAppBundlePath (ITaskItem item) + { + // We need to take "TargetPath" into account - this is path of the file relative to the output directory, and may also change the filename itself (it's for instance used to rename 'app.config' to the 'mainassembly.exe.config'). + // If "TargetPath" is specified, we rename the item to have "TargetPath" as the file name (the rest of the path is kept). + // This value takes precedence over the "Link" metadata (https://github.com/dotnet/msbuild/issues/2795) + var targetPath = item.GetMetadata ("TargetPath"); + if (!string.IsNullOrEmpty (targetPath)) + return targetPath; + + // If there's no "TargetPath" metadata, then we check the "Link" metadata, which works the same way as "TargetPath" otherwise. + var link = item.GetMetadata ("Link"); + if (!string.IsNullOrEmpty (link)) + return link; + + var virtualPath = Path.GetFileName (item.ItemSpec); + + // If neither "TargetPath" nor "Link" is set, we need to take "DestinationSubDirectory" into account - this is used to specify the subdirectory for resource assemblies for instance. + // Ref: https://github.com/dotnet/sdk/blob/0fc72ddb758dd136182972c2aea1d504ea046cfd/src/Tasks/Common/ItemUtilities.cs#L126-L128 + // Contrary to the "TargetPath" and "Link" metadata, this value doesn't specify the filename itself, only the containing directory name. + var destinationSubDirectory = item.GetMetadata ("DestinationSubDirectory"); + if (!string.IsNullOrEmpty (destinationSubDirectory)) + virtualPath = Path.Combine (destinationSubDirectory, virtualPath); + + return virtualPath; + } + + void ReportUnknownPublishFolderType (ITaskItem item) + { + var publishFolderType = item.GetMetadata ("PublishFolderType"); + + var metadata = item.GetMetadata ("CopyToOutputDirectory"); + if (!string.IsNullOrEmpty (metadata)) { + Log.LogWarning (MSBStrings.E7090 /* The 'PublishFolderType' metadata value '{0}' on the item '{1}' is not recognized. The file will not be copied to the app bundle. If the file is not supposed to be copied to the app bundle, remove the '{2}' metadata on the item. */, publishFolderType, item.ItemSpec, "CopyToOutputDirectory"); + return; + } + + metadata = item.GetMetadata ("CopyToPublishDirectory"); + if (!string.IsNullOrEmpty (metadata)) { + Log.LogWarning (MSBStrings.E7090 /* The 'PublishFolderType' metadata value '{0}' on the item '{1}' is not recognized. The file will not be copied to the app bundle. If the file is not supposed to be copied to the app bundle, remove the '{2}' metadata on the item. */, publishFolderType, item.ItemSpec, "CopyToPublishDirectory"); + return; + } + + Log.LogWarning (MSBStrings.E7088 /* The 'PublishFolderType' metadata value '{0}' on the item '{1}' is not recognized. The file will not be copied to the app bundle. */, publishFolderType, item.ItemSpec); + } + + // 'item' is not supposed to have a PublishFolderType set + PublishFolderType ComputePublishFolderType (IList items, ITaskItem item) + { + var filename = item.ItemSpec; + var targetPath = item.GetMetadata ("TargetPath"); + if (!string.IsNullOrEmpty (targetPath)) + filename = Path.Combine (Path.GetDirectoryName (filename), Path.GetFileName (targetPath)); + + // Check if the item came from @(BundleResource), @(Content) or @(EmbeddedResource) + if (resourceFilesSet.Contains (Path.GetFullPath (item.ItemSpec))) + return PublishFolderType.Resource; + + // Assemblies and their related files + if (filename.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.Assembly; + } else if (filename.EndsWith (".exe", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.Assembly; + } else if (filename.EndsWith (".pdb", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.Assembly; + } else if (filename.EndsWith (".dll.mdb", StringComparison.OrdinalIgnoreCase) || filename.EndsWith (".exe.mdb", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.Assembly; + } else if (filename.EndsWith (".config", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.Assembly; + } + + // Binding resource package (*.resources / *.resources.zip) + if (IsBindingResourcePackage (filename, out var type)) + return type; + + // Native (xc)frameworks. + // We do this after checking for binding resource packages, because those might contain frameworks. + if (TryGetFrameworkDirectory (filename, out _)) + return PublishFolderType.AppleFramework; + + // resources (png, jpg, ...?) + if (filename.EndsWith (".jpg", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.Resource; + } else if (filename.EndsWith (".png", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.Resource; + } + + // *.framework.zip, *.xcframework.zip + if (filename.EndsWith (".framework.zip", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.CompressedAppleFramework; + } else if (filename.EndsWith (".xcframework.zip", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.CompressedAppleFramework; + } + + // *.a and *.dylib + if (filename.EndsWith (".a", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.StaticLibrary; + } else if (filename.EndsWith (".dylib", StringComparison.OrdinalIgnoreCase)) { + return PublishFolderType.DynamicLibrary; + } + + // no other files are copied + + Log.LogWarning (MSBStrings.E7089 /* The file '{0}' does not specify a 'PublishFolderType' metadata, and a default value could not be calculated. The file will not be copied to the app bundle. */, item.ItemSpec); + + return PublishFolderType.None; + } + + static PublishFolderType ParsePublishFolderType (ITaskItem item) + { + return ParsePublishFolderType (item.GetMetadata ("PublishFolderType")); + } + + static PublishFolderType ParsePublishFolderType (string value) + { + if (string.IsNullOrEmpty (value)) + return PublishFolderType.Unset; + + if (!Enum.TryParse (value, out var result)) + result = PublishFolderType.Unknown; + + return result; + } + + enum PublishFolderType { + Unset, + None, + RootDirectory, + Assembly, + Resource, + AppleBindingResourcePackage, + CompressedAppleBindingResourcePackage, + AppleFramework, + CompressedAppleFramework, + PlugIns, + CompressedPlugIns, + DynamicLibrary, // link with + copy to app bundle + StaticLibrary, // link with (but not copy to app bundle) + Unknown, + } + } +} diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ComputeBundleLocation.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ComputeBundleLocation.cs new file mode 100644 index 0000000000..92952ffe10 --- /dev/null +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ComputeBundleLocation.cs @@ -0,0 +1,4 @@ +namespace Xamarin.MacDev.Tasks { + public class ComputeBundleLocation : ComputeBundleLocationTaskBase { + } +} diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index ebf51ca417..e9be7d97c6 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -81,6 +81,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. +