519 строки
16 KiB
C#
519 строки
16 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
|
|
using Microsoft.Build.Framework;
|
|
using Microsoft.Build.Utilities;
|
|
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
using Xamarin.MacDev.Tasks;
|
|
using Xamarin.MacDev;
|
|
|
|
namespace Xamarin.MacDev.Tasks
|
|
{
|
|
public abstract class ACToolTaskBase : XcodeCompilerToolTask
|
|
{
|
|
ITaskItem partialAppManifest;
|
|
string outputSpecs;
|
|
PDictionary plist;
|
|
|
|
#region Inputs
|
|
|
|
public string DeviceModel { get; set; }
|
|
|
|
public string DeviceOSVersion { get; set; }
|
|
|
|
[Required]
|
|
public ITaskItem[] ImageAssets { get; set; }
|
|
|
|
public bool IsWatchApp { get; set; }
|
|
|
|
[Required]
|
|
public bool OptimizePNGs { get; set; }
|
|
|
|
[Required]
|
|
public string OutputPath { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Outputs
|
|
|
|
[Output]
|
|
public ITaskItem PartialAppManifest { get; set; }
|
|
|
|
#endregion
|
|
|
|
protected override string DefaultBinDir {
|
|
get { return DeveloperRootBinDir; }
|
|
}
|
|
|
|
protected override string ToolName {
|
|
get { return "actool"; }
|
|
}
|
|
|
|
static bool IsWatchExtension (PDictionary plist)
|
|
{
|
|
PDictionary extension;
|
|
PString id;
|
|
|
|
if (!plist.TryGetValue ("NSExtension", out extension))
|
|
return false;
|
|
|
|
if (!extension.TryGetValue ("NSExtensionPointIdentifier", out id))
|
|
return false;
|
|
|
|
return id.Value == "com.apple.watchkit";
|
|
}
|
|
|
|
static bool IsMessagesExtension (PDictionary plist)
|
|
{
|
|
PDictionary extension;
|
|
PString id;
|
|
|
|
if (!plist.TryGetValue ("NSExtension", out extension))
|
|
return false;
|
|
|
|
if (!extension.TryGetValue ("NSExtensionPointIdentifier", out id))
|
|
return false;
|
|
|
|
return id.Value == "com.apple.message-payload-provider";
|
|
}
|
|
|
|
protected override void AppendCommandLineArguments (IDictionary<string, string> environment, ProcessArgumentBuilder args, ITaskItem[] items)
|
|
{
|
|
string minimumDeploymentTarget;
|
|
|
|
if (plist != null) {
|
|
PString value;
|
|
|
|
if (!plist.TryGetValue (MinimumDeploymentTargetKey, out value) || string.IsNullOrEmpty (value.Value))
|
|
minimumDeploymentTarget = SdkVersion;
|
|
else
|
|
minimumDeploymentTarget = value.Value;
|
|
|
|
var assetDirs = new HashSet<string> (items.Select (x => BundleResource.GetVirtualProjectPath (ProjectDir, x, !string.IsNullOrEmpty (SessionId))));
|
|
|
|
if (plist.TryGetValue (ManifestKeys.XSAppIconAssets, out value) && !string.IsNullOrEmpty (value.Value)) {
|
|
int index = value.Value.IndexOf (".xcassets" + Path.DirectorySeparatorChar, StringComparison.Ordinal);
|
|
string assetDir = null;
|
|
var rpath = value.Value;
|
|
|
|
if (index != -1)
|
|
assetDir = rpath.Substring (0, index + ".xcassets".Length);
|
|
|
|
if (assetDirs != null && assetDirs.Contains (assetDir)) {
|
|
var assetName = Path.GetFileNameWithoutExtension (rpath);
|
|
|
|
if (PartialAppManifest == null) {
|
|
args.Add ("--output-partial-info-plist");
|
|
args.AddQuoted (partialAppManifest.GetMetadata ("FullPath"));
|
|
|
|
PartialAppManifest = partialAppManifest;
|
|
}
|
|
|
|
args.Add ("--app-icon");
|
|
args.AddQuoted (assetName);
|
|
|
|
if (IsMessagesExtension (plist))
|
|
args.Add ("--product-type com.apple.product-type.app-extension.messages");
|
|
}
|
|
}
|
|
|
|
if (plist.TryGetValue (ManifestKeys.XSLaunchImageAssets, out value) && !string.IsNullOrEmpty (value.Value)) {
|
|
int index = value.Value.IndexOf (".xcassets" + Path.DirectorySeparatorChar, StringComparison.Ordinal);
|
|
string assetDir = null;
|
|
var rpath = value.Value;
|
|
|
|
if (index != -1)
|
|
assetDir = rpath.Substring (0, index + ".xcassets".Length);
|
|
|
|
if (assetDirs != null && assetDirs.Contains (assetDir)) {
|
|
var assetName = Path.GetFileNameWithoutExtension (rpath);
|
|
|
|
if (PartialAppManifest == null) {
|
|
args.Add ("--output-partial-info-plist");
|
|
args.AddQuoted (partialAppManifest.GetMetadata ("FullPath"));
|
|
|
|
PartialAppManifest = partialAppManifest;
|
|
}
|
|
|
|
args.Add ("--launch-image");
|
|
args.AddQuoted (assetName);
|
|
}
|
|
}
|
|
|
|
if (plist.TryGetValue (ManifestKeys.CLKComplicationGroup, out value) && !string.IsNullOrEmpty (value.Value))
|
|
args.Add ("--complication", value);
|
|
} else {
|
|
minimumDeploymentTarget = SdkVersion;
|
|
}
|
|
|
|
if (OptimizePNGs)
|
|
args.Add ("--compress-pngs");
|
|
|
|
if (AppleSdkSettings.XcodeVersion.Major >= 7) {
|
|
if (!string.IsNullOrEmpty (outputSpecs))
|
|
args.Add ("--enable-on-demand-resources", "YES");
|
|
|
|
if (!string.IsNullOrEmpty (DeviceModel))
|
|
args.Add ("--filter-for-device-model", DeviceModel);
|
|
|
|
if (!string.IsNullOrEmpty (DeviceOSVersion))
|
|
args.Add ("--filter-for-device-os-version", DeviceOSVersion);
|
|
|
|
if (!string.IsNullOrEmpty (outputSpecs)) {
|
|
args.Add ("--asset-pack-output-specifications");
|
|
args.AddQuoted (Path.GetFullPath (outputSpecs));
|
|
}
|
|
}
|
|
|
|
if (plist != null) {
|
|
foreach (var targetDevice in GetTargetDevices (plist))
|
|
args.Add ("--target-device", targetDevice);
|
|
}
|
|
|
|
args.Add ("--minimum-deployment-target", minimumDeploymentTarget);
|
|
|
|
switch (SdkPlatform) {
|
|
case "iPhoneSimulator":
|
|
args.Add ("--platform", IsWatchApp ? "watchsimulator" : "iphonesimulator");
|
|
break;
|
|
case "iPhoneOS":
|
|
args.Add ("--platform", IsWatchApp ? "watchos" : "iphoneos");
|
|
break;
|
|
case "MacOSX":
|
|
args.Add ("--platform", "macosx");
|
|
break;
|
|
case "WatchSimulator":
|
|
args.Add ("--platform", "watchsimulator");
|
|
break;
|
|
case "WatchOS":
|
|
args.Add ("--platform", "watchos");
|
|
break;
|
|
case "AppleTVSimulator":
|
|
args.Add ("--platform", "appletvsimulator");
|
|
break;
|
|
case "AppleTVOS":
|
|
args.Add ("--platform", "appletvos");
|
|
break;
|
|
}
|
|
}
|
|
|
|
IEnumerable<ITaskItem> GetCompiledBundleResources (PDictionary output, string intermediateBundleDir)
|
|
{
|
|
var pwd = PathUtils.ResolveSymbolicLinks (Environment.CurrentDirectory);
|
|
PDictionary dict;
|
|
PArray array;
|
|
|
|
if (output.TryGetValue (string.Format ("com.apple.{0}.compilation-results", ToolName), out dict) && dict.TryGetValue ("output-files", out array)) {
|
|
foreach (var path in array.OfType<PString> ().Select (x => x.Value)) {
|
|
// don't include the generated plist files as BundleResources
|
|
if (path.EndsWith ("partial-info.plist", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
var vpath = PathUtils.AbsoluteToRelative (pwd, PathUtils.ResolveSymbolicLinks (path));
|
|
var item = new TaskItem (vpath);
|
|
|
|
// Note: the intermediate bundle dir functions as a top-level bundle dir
|
|
var logicalName = PathUtils.AbsoluteToRelative (intermediateBundleDir, path);
|
|
|
|
if (logicalName.StartsWith ("../OnDemandResources/", StringComparison.Ordinal)) {
|
|
logicalName = logicalName.Substring (3);
|
|
|
|
var outputPath = Path.Combine (OutputPath, logicalName);
|
|
|
|
item.SetMetadata ("OutputPath", outputPath);
|
|
}
|
|
|
|
item.SetMetadata ("LogicalName", logicalName);
|
|
item.SetMetadata ("Optimize", "false");
|
|
|
|
yield return item;
|
|
}
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
public override bool Execute ()
|
|
{
|
|
var intermediate = Path.Combine (IntermediateOutputPath, ToolName);
|
|
var intermediateBundleDir = Path.Combine (intermediate, "bundle");
|
|
var intermediateCloneDir = Path.Combine (intermediate, "cloned-assets");
|
|
var manifest = new TaskItem (Path.Combine (intermediate, "asset-manifest.plist"));
|
|
var bundleResources = new List<ITaskItem> ();
|
|
var outputManifests = new List<ITaskItem> ();
|
|
var catalogs = new List<ITaskItem> ();
|
|
var unique = new HashSet<string> ();
|
|
string bundleIdentifier = null;
|
|
var knownSpecs = new HashSet<string> ();
|
|
var clones = new HashSet<string> ();
|
|
var items = new List<ITaskItem> ();
|
|
var specs = new PArray ();
|
|
|
|
Log.LogTaskName ("ACTool");
|
|
Log.LogTaskProperty ("AppManifest", AppManifest);
|
|
Log.LogTaskProperty ("DeviceModel", DeviceModel);
|
|
Log.LogTaskProperty ("DeviceOSVersion", DeviceOSVersion);
|
|
Log.LogTaskProperty ("ImageAssets", ImageAssets);
|
|
Log.LogTaskProperty ("IntermediateOutputPath", IntermediateOutputPath);
|
|
Log.LogTaskProperty ("IsWatchApp", IsWatchApp);
|
|
Log.LogTaskProperty ("OptimizePNGs", OptimizePNGs);
|
|
Log.LogTaskProperty ("OutputPath", OutputPath);
|
|
Log.LogTaskProperty ("ProjectDir", ProjectDir);
|
|
Log.LogTaskProperty ("ResourcePrefix", ResourcePrefix);
|
|
Log.LogTaskProperty ("SdkBinPath", SdkBinPath);
|
|
Log.LogTaskProperty ("SdkPlatform", SdkPlatform);
|
|
Log.LogTaskProperty ("SdkVersion", SdkVersion);
|
|
|
|
switch (SdkPlatform) {
|
|
case "iPhoneSimulator":
|
|
case "iPhoneOS":
|
|
case "MacOSX":
|
|
case "WatchSimulator":
|
|
case "WatchOS":
|
|
case "AppleTVSimulator":
|
|
case "AppleTVOS":
|
|
break;
|
|
default:
|
|
Log.LogError ("Unrecognized platform: {0}", SdkPlatform);
|
|
return false;
|
|
}
|
|
|
|
if (AppManifest != null) {
|
|
try {
|
|
plist = PDictionary.FromFile (AppManifest.ItemSpec);
|
|
} catch (Exception ex) {
|
|
Log.LogError (null, null, null, AppManifest.ItemSpec, 0, 0, 0, 0, "{0}", ex.Message);
|
|
return false;
|
|
}
|
|
|
|
bundleIdentifier = plist.GetCFBundleIdentifier ();
|
|
}
|
|
|
|
for (int i = 0; i < ImageAssets.Length; i++) {
|
|
var vpath = BundleResource.GetVirtualProjectPath (ProjectDir, ImageAssets[i], !string.IsNullOrEmpty (SessionId));
|
|
|
|
// Ignore MacOS .DS_Store files...
|
|
if (Path.GetFileName (vpath).Equals (".DS_Store", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
// get the parent (which will typically be .appiconset, .launchimage, .imageset, .iconset, etc)
|
|
var catalog = Path.GetDirectoryName (vpath);
|
|
|
|
// keep walking up the directory structure until we get to the .xcassets directory
|
|
while (!string.IsNullOrEmpty (catalog) && Path.GetExtension (catalog) != ".xcassets")
|
|
catalog = Path.GetDirectoryName (catalog);
|
|
|
|
if (string.IsNullOrEmpty (catalog)) {
|
|
Log.LogWarning (null, null, null, ImageAssets[i].ItemSpec, 0, 0, 0, 0, "Asset not part of an asset catalog: {0}", ImageAssets[i].ItemSpec);
|
|
continue;
|
|
}
|
|
|
|
if (ImageAssets[i].GetMetadata ("Link") != null) {
|
|
// Note: if any of the files within a catalog are linked, we'll have to clone the *entire* catalog
|
|
clones.Add (catalog);
|
|
continue;
|
|
}
|
|
|
|
// filter out everything except paths containing a Contents.json file since our main processing loop only cares about these
|
|
if (Path.GetFileName (vpath) != "Contents.json")
|
|
continue;
|
|
|
|
items.Add (ImageAssets[i]);
|
|
}
|
|
|
|
// clone any *.xcassets dirs that need cloning
|
|
if (clones.Count > 0) {
|
|
if (Directory.Exists (intermediateCloneDir))
|
|
Directory.Delete (intermediateCloneDir, true);
|
|
|
|
Directory.CreateDirectory (intermediateCloneDir);
|
|
|
|
items.Clear ();
|
|
|
|
for (int i = 0; i < ImageAssets.Length; i++) {
|
|
var vpath = BundleResource.GetVirtualProjectPath (ProjectDir, ImageAssets[i], !string.IsNullOrEmpty (SessionId));
|
|
var clone = false;
|
|
ITaskItem item;
|
|
|
|
// Ignore MacOS .DS_Store files...
|
|
if (Path.GetFileName (vpath).Equals (".DS_Store", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
foreach (var catalog in clones) {
|
|
if (vpath.Length > catalog.Length && vpath[catalog.Length] == '/' && vpath.StartsWith (catalog, StringComparison.Ordinal)) {
|
|
clone = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (clone) {
|
|
var src = ImageAssets[i].GetMetadata ("FullPath");
|
|
|
|
if (!File.Exists (src)) {
|
|
Log.LogError (null, null, null, src, 0, 0, 0, 0, "File not found: {0}", src);
|
|
return false;
|
|
}
|
|
|
|
var dest = Path.Combine (intermediateCloneDir, vpath);
|
|
var dir = Path.GetDirectoryName (dest);
|
|
|
|
Directory.CreateDirectory (dir);
|
|
|
|
File.Copy (src, dest, true);
|
|
|
|
// filter out everything except paths containing a Contents.json file since our main processing loop only cares about these
|
|
if (Path.GetFileName (vpath) != "Contents.json")
|
|
continue;
|
|
|
|
item = new TaskItem (dest);
|
|
ImageAssets[i].CopyMetadataTo (item);
|
|
item.SetMetadata ("Link", vpath);
|
|
} else {
|
|
// filter out everything except paths containing a Contents.json file since our main processing loop only cares about these
|
|
if (Path.GetFileName (vpath) != "Contents.json")
|
|
continue;
|
|
|
|
item = ImageAssets[i];
|
|
}
|
|
|
|
items.Add (item);
|
|
}
|
|
}
|
|
|
|
// Note: `items` contains only the Contents.json files at this point
|
|
for (int i = 0; i < items.Count; i++) {
|
|
var vpath = BundleResource.GetVirtualProjectPath (ProjectDir, items[i], !string.IsNullOrEmpty (SessionId));
|
|
var path = items[i].GetMetadata ("FullPath");
|
|
|
|
// get the parent (which will typically be .appiconset, .launchimage, .imageset, .iconset, etc)
|
|
var catalog = Path.GetDirectoryName (vpath);
|
|
path = Path.GetDirectoryName (path);
|
|
|
|
// keep walking up the directory structure until we get to the .xcassets directory
|
|
while (!string.IsNullOrEmpty (catalog) && Path.GetExtension (catalog) != ".xcassets") {
|
|
catalog = Path.GetDirectoryName (catalog);
|
|
path = Path.GetDirectoryName (path);
|
|
}
|
|
|
|
if (unique.Add (catalog)) {
|
|
var item = new TaskItem (path);
|
|
item.SetMetadata ("Link", catalog);
|
|
|
|
catalogs.Add (item);
|
|
}
|
|
|
|
if (AppleSdkSettings.XcodeVersion.Major >= 7 && !string.IsNullOrEmpty (bundleIdentifier) && SdkPlatform != "WatchSimulator") {
|
|
var text = File.ReadAllText (items[i].ItemSpec);
|
|
|
|
if (string.IsNullOrEmpty (text))
|
|
continue;
|
|
|
|
var json = JsonConvert.DeserializeObject (text) as JObject;
|
|
|
|
if (json == null)
|
|
continue;
|
|
|
|
var properties = json.Property ("properties");
|
|
|
|
if (properties == null)
|
|
continue;
|
|
|
|
var resourceTags = properties.Value.ToObject<JObject> ().Property ("on-demand-resource-tags");
|
|
|
|
if (resourceTags == null || resourceTags.Value.Type != JTokenType.Array)
|
|
continue;
|
|
|
|
var tagArray = resourceTags.Value.ToObject<JArray> ();
|
|
var tags = new HashSet<string> ();
|
|
string hash;
|
|
|
|
foreach (var tag in tagArray.Select (token => token.ToObject<string> ()))
|
|
tags.Add (tag);
|
|
|
|
var tagList = tags.ToList ();
|
|
tagList.Sort ();
|
|
|
|
var assetDir = AssetPackUtils.GetAssetPackDirectory (intermediate, bundleIdentifier, tagList, out hash);
|
|
|
|
if (knownSpecs.Add (hash)) {
|
|
var assetpack = new PDictionary ();
|
|
var ptags = new PArray ();
|
|
|
|
Directory.CreateDirectory (assetDir);
|
|
|
|
for (int j = 0; j < tagList.Count; j++)
|
|
ptags.Add (new PString (tagList[j]));
|
|
|
|
assetpack.Add ("bundle-id", new PString (string.Format ("{0}.asset-pack-{1}", bundleIdentifier, hash)));
|
|
assetpack.Add ("bundle-path", new PString (Path.GetFullPath (assetDir)));
|
|
assetpack.Add ("tags", ptags);
|
|
specs.Add (assetpack);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (catalogs.Count == 0) {
|
|
// There are no (supported?) asset catalogs
|
|
return true;
|
|
}
|
|
|
|
partialAppManifest = new TaskItem (Path.Combine (intermediate, "partial-info.plist"));
|
|
|
|
if (specs.Count > 0) {
|
|
outputSpecs = Path.Combine (intermediate, "output-specifications.plist");
|
|
specs.Save (outputSpecs, true);
|
|
}
|
|
|
|
Directory.CreateDirectory (intermediateBundleDir);
|
|
|
|
// Note: Compile() will set the PartialAppManifest property if it is used...
|
|
if ((Compile (catalogs.ToArray (), intermediateBundleDir, manifest)) != 0)
|
|
return false;
|
|
|
|
if (PartialAppManifest != null && !File.Exists (PartialAppManifest.GetMetadata ("FullPath")))
|
|
Log.LogError ("Partial Info.plist file was not generated: {0}", PartialAppManifest.GetMetadata ("FullPath"));
|
|
|
|
try {
|
|
var manifestOutput = PDictionary.FromFile (manifest.ItemSpec);
|
|
|
|
LogWarningsAndErrors (manifestOutput, catalogs[0]);
|
|
|
|
bundleResources.AddRange (GetCompiledBundleResources (manifestOutput, intermediateBundleDir));
|
|
outputManifests.Add (manifest);
|
|
} catch (Exception ex) {
|
|
Log.LogError ("Failed to load output manifest for {0} for the file {2}: {1}", ToolName, ex.Message, manifest.ItemSpec);
|
|
}
|
|
|
|
foreach (var assetpack in specs.OfType<PDictionary> ()) {
|
|
var path = Path.Combine (assetpack.GetString ("bundle-path").Value, "Info.plist");
|
|
var bundlePath = PathUtils.AbsoluteToRelative (intermediate, path);
|
|
var outputPath = Path.Combine (OutputPath, bundlePath);
|
|
var rpath = Path.Combine (intermediate, bundlePath);
|
|
var dict = new PDictionary ();
|
|
|
|
dict.SetCFBundleIdentifier (assetpack.GetString ("bundle-id").Value);
|
|
dict.Add ("Tags", assetpack.GetArray ("tags").Clone ());
|
|
|
|
dict.Save (path, true, true);
|
|
|
|
var item = new TaskItem (rpath);
|
|
item.SetMetadata ("LogicalName", bundlePath);
|
|
item.SetMetadata ("OutputPath", outputPath);
|
|
item.SetMetadata ("Optimize", "false");
|
|
|
|
bundleResources.Add (item);
|
|
}
|
|
|
|
BundleResources = bundleResources.ToArray ();
|
|
OutputManifests = outputManifests.ToArray ();
|
|
|
|
return !Log.HasLoggedErrors;
|
|
}
|
|
}
|
|
}
|