[msbuild] Add a ComputeBundleLocation task

This commit is contained in:
Rolf Bjarne Kvinge 2021-08-12 12:30:57 +02:00
Родитель 3dc0fe881a
Коммит 19dc9ce0aa
5 изменённых файлов: 405 добавлений и 1 удалений

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

@ -1948,6 +1948,33 @@ namespace Xamarin.Localization.MSBuild {
}
}
/// <summary>
/// Looks up a localized string similar to The &apos;PublishFolderType&apos; metadata value &apos;{0}&apos; on the item &apos;{1}&apos; is not recognized. The file will not be copied to the app bundle..
/// </summary>
public static string E7088 {
get {
return ResourceManager.GetString("E7088", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The file &apos;{0}&apos; does not specify a &apos;PublishFolderType&apos; metadata, and a default value could not be calculated. The file will not be copied to the app bundle..
/// </summary>
public static string E7089 {
get {
return ResourceManager.GetString("E7089", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The &apos;PublishFolderType&apos; metadata value &apos;{0}&apos; on the item &apos;{1}&apos; 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 &apos;{2}&apos; metadata on the item..
/// </summary>
public static string E7090 {
get {
return ResourceManager.GetString("E7090", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to File &apos;{0}&apos; is not a valid framework: {1}.
/// </summary>
@ -1957,6 +1984,15 @@ namespace Xamarin.Localization.MSBuild {
}
}
/// <summary>
/// Looks up a localized string similar to The file or directory &apos;{0}&apos; is not a framework nor a file within a framework..
/// </summary>
public static string E7094 {
get {
return ResourceManager.GetString("E7094", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid framework: {0}.
/// </summary>
@ -2582,7 +2618,7 @@ namespace Xamarin.Localization.MSBuild {
return ResourceManager.GetString("W7091", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The binding resource package {0} does not exist..
/// </summary>

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

@ -1348,6 +1348,33 @@
<comment>The following are literal names and should not be translated: manifest</comment>
</data>
<data name="E7088" xml:space="preserve">
<value>The 'PublishFolderType' metadata value '{0}' on the item '{1}' is not recognized. The file will not be copied to the app bundle.</value>
<comment>
PublishFolderType: do not translate (name of metadata)
{0}: metadata value (read from a user file)
{1}: path to a file
</comment>
</data>
<data name="E7089" xml:space="preserve">
<value>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.</value>
<comment>
PublishFolderType: do not translate (name of metadata)
{0}: path to a file
</comment>
</data>
<data name="E7090" xml:space="preserve">
<value>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.</value>
<comment>
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')
</comment>
</data>
<data name="W7091" xml:space="preserve">
<value>The framework {0} is a framework of static libraries, and will not be copied to the app.</value>
</data>
@ -1361,4 +1388,7 @@
<value>The binding resource package {0} does not exist.</value>
</data>
<data name="E7094" xml:space="preserve">
<value>The file or directory '{0}' is not a framework nor a file within a framework.</value>
</data>
</root>

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

@ -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<string> resourceFilesSet = new HashSet<string> ();
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<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
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<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,
}
}
}

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

@ -0,0 +1,4 @@
namespace Xamarin.MacDev.Tasks {
public class ComputeBundleLocation : ComputeBundleLocationTaskBase {
}
}

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

@ -81,6 +81,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
<UsingTask TaskName="Xamarin.MacDev.Tasks.CollectBundleResources" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.CollectFrameworks" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.CompileEntitlements" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.ComputeBundleLocation" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.ComputeBundleResourceOutputPaths" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.CoreMLCompiler" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.CreateAssetPackManifest" AssemblyFile="$(_TaskAssemblyName)" />