xamarin-macios/msbuild/Xamarin.MacDev.Tasks/Tasks/ComputeBundleLocationTaskBa...

371 строка
14 KiB
C#

#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 bool BundlerDebug { get; set; }
public string PackageDebugSymbols { 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<string> resourceFilesSet = new HashSet<string> ();
// We package the symbols if the PackageDebugSymbols is set to 'true', we don't if set to anything else, and if set to
// nothing, then we package symbols unless we're doing a release build.
bool PackageSymbols {
get {
if (!string.IsNullOrEmpty (PackageDebugSymbols))
return string.Equals ("true", PackageDebugSymbols, StringComparison.OrdinalIgnoreCase);
return BundlerDebug;
}
}
void AddResourceFiles (ITaskItem []? items)
{
if (items is null || items.Length == 0)
return;
var resources = items.
// Remove any items with PublishFolderType set
Where (v => string.IsNullOrEmpty (v.GetMetadata ("PublishFolderType"))).
// Get the full path
Select (v => Path.GetFullPath (v.ItemSpec));
resourceFilesSet.UnionWith (resources);
}
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.
AddResourceFiles (BundleResource);
AddResourceFiles (Content);
AddResourceFiles (EmbeddedResource);
var appleFrameworks = new Dictionary<string, List<ITaskItem>> ();
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<ITaskItem> ();
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<ITaskItem> 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
var assemblyExtensions = new string [] {
".dll", ".exe", ".config",
};
foreach (var extension in assemblyExtensions) {
if (filename.EndsWith (extension, StringComparison.OrdinalIgnoreCase))
return PublishFolderType.Assembly;
}
// Assemblies and their related files
var assemblyDebugExtensions = new string [] {
".pdb", ".dll.mdb", ".exe.mdb",
};
foreach (var extension in assemblyDebugExtensions) {
if (filename.EndsWith (extension, StringComparison.OrdinalIgnoreCase))
return PackageSymbols ? PublishFolderType.Assembly : PublishFolderType.None;
}
// 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, ...?)
var resourceExtensions = new string [] {
".jpg",
".png",
};
foreach (var extension in resourceExtensions) {
if (filename.EndsWith (extension, StringComparison.OrdinalIgnoreCase))
return PublishFolderType.Resource;
}
// *.framework.zip, *.xcframework.zip
var compressedAppleFrameworksExtensions = new string [] {
".framework.zip",
".xcframework.zip",
};
foreach (var extension in compressedAppleFrameworksExtensions) {
if (filename.EndsWith (extension, 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<PublishFolderType> (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,
}
}
}