xamarin-macios/msbuild/Xamarin.iOS.Tasks.Core/Tasks/ValidateAppBundleTaskBase.cs

452 строки
21 KiB
C#

using System;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Xamarin.MacDev;
using Xamarin.MacDev.Tasks;
namespace Xamarin.iOS.Tasks
{
public abstract class ValidateAppBundleTaskBase : Task
{
#region Inputs
public string SessionId { get; set; }
[Required]
public string AppBundlePath { get; set; }
[Required]
public bool SdkIsSimulator { get; set; }
[Required]
public string TargetFrameworkIdentifier { get; set; }
#endregion
public PlatformFramework Framework {
get { return PlatformFrameworkHelper.GetFramework (TargetFrameworkIdentifier); }
}
void ValidateAppExtension (string path, string mainBundleIdentifier, string mainShortVersionString, string mainVersion)
{
var name = Path.GetFileNameWithoutExtension (path);
var info = Path.Combine (path, "Info.plist");
if (!File.Exists (info)) {
Log.LogError (7003, path, $"The App Extension '{name}' does not contain an Info.plist.");
return;
}
var plist = PDictionary.FromFile (info);
var bundleIdentifier = plist.GetCFBundleIdentifier ();
if (string.IsNullOrEmpty (bundleIdentifier)) {
Log.LogError (7004, info, $"The App Extension '{name}' does not specify a CFBundleIdentifier.");
return;
}
// The filename of the extension path is the extension's bundle identifier, which turns out ugly
// in error messages. Try to get something more friendly-looking.
name = plist.GetCFBundleDisplayName () ?? name;
var executable = plist.GetCFBundleExecutable ();
if (string.IsNullOrEmpty (executable))
Log.LogError (7005, info, $"The App Extension '{name}' does not specify a CFBundleExecutable.");
if (!bundleIdentifier.StartsWith (mainBundleIdentifier + ".", StringComparison.Ordinal))
Log.LogError (7006, info, $"The App Extension '{name}' has an invalid CFBundleIdentifier ({bundleIdentifier}), it does not begin with the main app bundle's CFBundleIdentifier ({mainBundleIdentifier}).");
if (bundleIdentifier.EndsWith (".key", StringComparison.Ordinal))
Log.LogError (7007, info, $"The App Extension '{name}' has a CFBundleIdentifier ({bundleIdentifier}) that ends with the illegal suffix \".key\".");
var shortVersionString = plist.GetCFBundleShortVersionString ();
if (string.IsNullOrEmpty (shortVersionString))
Log.LogError (7008, info, $"The App Extension '{name}' does not specify a CFBundleShortVersionString.");
if (shortVersionString != mainShortVersionString)
Log.LogWarning ("The App Extension '{0}' has a CFBundleShortVersionString ({1}) that does not match the main app bundle's CFBundleShortVersionString ({2})", name, shortVersionString, mainShortVersionString);
var version = plist.GetCFBundleVersion ();
if (string.IsNullOrEmpty (version))
Log.LogWarning ("The App Extension '{0}' does not specify a CFBundleVersion", name);
if (version != mainVersion)
Log.LogWarning ("The App Extension '{0}' has a CFBundleVersion ({1}) that does not match the main app bundle's CFBundleVersion ({2})", name, version, mainVersion);
var extension = plist.Get<PDictionary> ("NSExtension");
if (extension == null) {
Log.LogError (7009, info, $"The App Extension '{name}' has an invalid Info.plist: it does not contain an NSExtension dictionary.");
return;
}
var extensionPointIdentifier = extension.GetString ("NSExtensionPointIdentifier").Value;
if (string.IsNullOrEmpty (extensionPointIdentifier)) {
Log.LogError (7010, info, $"The App Extension '{name}' has an invalid Info.plist: the NSExtension dictionary does not contain an NSExtensionPointIdentifier value.");
return;
}
// https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/SystemExtensionKeys.html#//apple_ref/doc/uid/TP40014212-SW9
switch (extensionPointIdentifier) {
case "com.apple.ui-services": // iOS+OSX
case "com.apple.services": // iOS
case "com.apple.keyboard-service": // iOS
case "com.apple.fileprovider-ui": // iOS
case "com.apple.fileprovider-nonui": // iOS
case "com.apple.FinderSync": // OSX
case "com.apple.photo-editing": // iOS
case "com.apple.share-services": // iOS+OSX
case "com.apple.widget-extension": // iOS+OSX
case "com.apple.Safari.content-blocker": // iOS
case "com.apple.Safari.sharedlinks-service": // iOS
case "com.apple.spotlight.index": // iOS
case "com.apple.AudioUnit": // iOS
case "com.apple.AudioUnit-UI": // iOS
case "com.apple.tv-services": // tvOS
case "com.apple.broadcast-services": // iOS+tvOS
case "com.apple.callkit.call-directory": // iOS
case "com.apple.message-payload-provider": // iOS
case "com.apple.intents-service": // iOS
case "com.apple.intents-ui-service": // iOS
case "com.apple.usernotifications.content-extension": // iOS
case "com.apple.usernotifications.service": // iOS
case "com.apple.networkextension.packet-tunnel": // iOS+OSX
break;
case "com.apple.watchkit": // iOS8.2
var attributes = extension.Get<PDictionary> ("NSExtensionAttributes");
if (attributes == null) {
Log.LogError (7011, info, $"The WatchKit Extension '{name}' has an invalid Info.plist: the NSExtension dictionary does not contain an NSExtensionAttributes dictionary.");
return;
}
var wkAppBundleIdentifier = attributes.GetString ("WKAppBundleIdentifier").Value;
var apps = Directory.GetDirectories (path, "*.app");
if (apps.Length == 0) {
Log.LogError (7012, info, $"The WatchKit Extension '{name}' does not contain any watch apps.");
} else if (apps.Length > 1) {
Log.LogError (7012, info, $"The WatchKit Extension '{name}' contain more than one watch apps.");
} else {
PObject requiredDeviceCapabilities;
if (plist.TryGetValue ("UIRequiredDeviceCapabilities", out requiredDeviceCapabilities)) {
var requiredDeviceCapabilitiesDictionary = requiredDeviceCapabilities as PDictionary;
var requiredDeviceCapabilitiesArray = requiredDeviceCapabilities as PArray;
if (requiredDeviceCapabilitiesDictionary != null) {
PBoolean watchCompanion;
if (!requiredDeviceCapabilitiesDictionary.TryGetValue ("watch-companion", out watchCompanion) || !watchCompanion.Value)
Log.LogError (7013, info, $"The WatchKit Extension '{name}' has an invalid Info.plist: the UIRequiredDeviceCapabilities dictionary must contain the 'watch-companion' capability with a value of 'true'.");
} else if (requiredDeviceCapabilitiesArray != null) {
if (!requiredDeviceCapabilitiesArray.OfType<PString> ().Any (x => x.Value == "watch-companion"))
Log.LogError (7013, info, $"The WatchKit Extension '{name}' has an invalid Info.plist: the UIRequiredDeviceCapabilities array must contain the 'watch-companion' capability.");
} else {
Log.LogError (7013, info, $"The WatchKit Extension '{name}' has an invalid Info.plist: the UIRequiredDeviceCapabilities key must be present and contain the 'watch-companion' capability.");
}
} else {
Log.LogError (7013, info, $"The WatchKit Extension '{name}' has an invalid Info.plist: the UIRequiredDeviceCapabilities key must be present and contain the 'watch-companion' capability.");
}
ValidateWatchOS1App (apps[0], name, mainBundleIdentifier, wkAppBundleIdentifier);
}
break;
default:
Log.LogWarning ("The App Extension '{0}' has an unrecognized NSExtensionPointIdentifier value ('{1}').", name, extensionPointIdentifier);
break;
}
}
void ValidateWatchApp (string path, string mainBundleIdentifier, string mainShortVersionString, string mainVersion)
{
var name = Path.GetFileNameWithoutExtension (path);
var info = Path.Combine (path, "Info.plist");
if (!File.Exists (info)) {
Log.LogError (7014, path, $"The Watch App '{name}' does not contain an Info.plist.");
return;
}
var plist = PDictionary.FromFile (info);
var bundleIdentifier = plist.GetCFBundleIdentifier ();
if (string.IsNullOrEmpty (bundleIdentifier)) {
Log.LogError (7015, info, $"The Watch App '{name}' does not specify a CFBundleIdentifier.");
return;
}
if (!bundleIdentifier.StartsWith (mainBundleIdentifier + ".", StringComparison.Ordinal))
Log.LogError (7016, info, $"The Watch App '{name}' has an invalid CFBundleIdentifier ({bundleIdentifier}), it does not begin with the main app bundle's CFBundleIdentifier ({mainBundleIdentifier}).");
var shortVersionString = plist.GetCFBundleShortVersionString ();
if (string.IsNullOrEmpty (shortVersionString))
Log.LogWarning ("The Watch App '{0}' does not specify a CFBundleShortVersionString", name);
if (shortVersionString != mainShortVersionString)
Log.LogWarning ("The Watch App '{0}' has a CFBundleShortVersionString ({1}) that does not match the main app bundle's CFBundleShortVersionString ({2})", name, shortVersionString, mainShortVersionString);
var version = plist.GetCFBundleVersion ();
if (string.IsNullOrEmpty (version))
Log.LogWarning ("The Watch App '{0}' does not specify a CFBundleVersion", name);
if (version != mainVersion)
Log.LogWarning ("The Watch App '{0}' has a CFBundleVersion ({1}) that does not match the main app bundle's CFBundleVersion ({2})", name, version, mainVersion);
var watchDeviceFamily = plist.GetUIDeviceFamily ();
if (watchDeviceFamily != IPhoneDeviceType.Watch)
Log.LogError (7017, info, $"The Watch App '{name}' does not have a valid UIDeviceFamily value. Expected 'Watch (4)' but found '{watchDeviceFamily.ToString ()} ({(int)watchDeviceFamily})'.");
var watchExecutable = plist.GetCFBundleExecutable ();
if (string.IsNullOrEmpty (watchExecutable))
Log.LogError (7018, info, $"The Watch App '{name}' does not specify a CFBundleExecutable");
var wkCompanionAppBundleIdentifier = plist.GetString ("WKCompanionAppBundleIdentifier").Value;
if (wkCompanionAppBundleIdentifier != mainBundleIdentifier)
Log.LogError (7019, info, $"The Watch App '{name}' has an invalid WKCompanionAppBundleIdentifier value ('{wkCompanionAppBundleIdentifier}'), it does not match the main app bundle's CFBundleIdentifier ('{mainBundleIdentifier}').");
PBoolean watchKitApp;
if (!plist.TryGetValue ("WKWatchKitApp", out watchKitApp) || !watchKitApp.Value)
Log.LogError (7020, info, $"The Watch App '{name}' has an invalid Info.plist: the WKWatchKitApp key must be present and have a value of 'true'.");
if (plist.ContainsKey ("LSRequiresIPhoneOS"))
Log.LogError (7021, info, $"The Watch App '{name}' has an invalid Info.plist: the LSRequiresIPhoneOS key must not be present.");
var pluginsDir = Path.Combine (path, "PlugIns");
if (!Directory.Exists (pluginsDir)) {
Log.LogError (7022, path, $"The Watch App '{name}' does not contain any Watch Extensions.");
return;
}
int count = 0;
foreach (var plugin in Directory.EnumerateDirectories (pluginsDir, "*.appex")) {
ValidateWatchExtension (plugin, bundleIdentifier, shortVersionString, version);
count++;
}
if (count == 0)
Log.LogError (7022, pluginsDir, $"The Watch App '{name}' does not contain a Watch Extension.");
}
void ValidateWatchExtension (string path, string watchAppBundleIdentifier, string mainShortVersionString, string mainVersion)
{
var name = Path.GetFileNameWithoutExtension (path);
var info = Path.Combine (path, "Info.plist");
if (!File.Exists (info)) {
Log.LogError (7023, path, $"The Watch Extension '{name}' does not contain an Info.plist.");
return;
}
var plist = PDictionary.FromFile (info);
var bundleIdentifier = plist.GetCFBundleIdentifier ();
if (string.IsNullOrEmpty (bundleIdentifier)) {
Log.LogError (7024, info, $"The Watch Extension '{name}' does not specify a CFBundleIdentifier.");
return;
}
// The filename of the extension path is the extension's bundle identifier, which turns out ugly
// in error messages. Try to get something more friendly-looking.
name = plist.GetCFBundleDisplayName () ?? name;
var executable = plist.GetCFBundleExecutable ();
if (string.IsNullOrEmpty (executable))
Log.LogError (7025, info, $"The Watch Extension '{name}' does not specify a CFBundleExecutable.");
if (!bundleIdentifier.StartsWith (watchAppBundleIdentifier + ".", StringComparison.Ordinal))
Log.LogError (7026, info, $"The Watch Extension '{name}' has an invalid CFBundleIdentifier ({bundleIdentifier}), it does not begin with the main app bundle's CFBundleIdentifier ({watchAppBundleIdentifier}).");
if (bundleIdentifier.EndsWith (".key", StringComparison.Ordinal))
Log.LogError (7027, info, $"The Watch Extension '{name}' has a CFBundleIdentifier ({bundleIdentifier}) that ends with the illegal suffix \".key\".");
var shortVersionString = plist.GetCFBundleShortVersionString ();
if (string.IsNullOrEmpty (shortVersionString))
Log.LogWarning ("The Watch Extension '{0}' does not specify a CFBundleShortVersionString", name);
if (shortVersionString != mainShortVersionString)
Log.LogWarning ("The Watch Extension '{0}' has a CFBundleShortVersionString ({1}) that does not match the main app bundle's CFBundleShortVersionString ({2})", name, shortVersionString, mainShortVersionString);
var version = plist.GetCFBundleVersion ();
if (string.IsNullOrEmpty (version))
Log.LogWarning ("The Watch Extension '{0}' does not specify a CFBundleVersion", name);
if (version != mainVersion)
Log.LogWarning ("The Watch Extension '{0}' has a CFBundleVersion ({1}) that does not match the main app bundle's CFBundleVersion ({2})", name, version, mainVersion);
var extension = plist.Get<PDictionary> ("NSExtension");
if (extension == null) {
Log.LogError (7028, info, $"The Watch Extension '{name}' has an invalid Info.plist: it does not contain an NSExtension dictionary.");
return;
}
var extensionPointIdentifier = extension.Get<PString> ("NSExtensionPointIdentifier");
if (extensionPointIdentifier != null) {
if (extensionPointIdentifier.Value != "com.apple.watchkit")
Log.LogError (7029, info, $"The Watch Extension '{name}' has an invalid Info.plist: the NSExtensionPointIdentifier must be \"com.apple.watchkit\".");
} else {
Log.LogError (7029, info, $"The Watch Extension '{name}' has an invalid Info.plist: the NSExtension dictionary must contain an NSExtensionPointIdentifier.");
}
PDictionary attributes;
if (!extension.TryGetValue ("NSExtensionAttributes", out attributes)) {
Log.LogError (7030, info, $"The Watch Extension '{name}' has an invalid Info.plist: the NSExtension dictionary must contain NSExtensionAttributes.");
return;
}
var appBundleIdentifier = attributes.Get<PString> ("WKAppBundleIdentifier");
if (appBundleIdentifier != null) {
if (appBundleIdentifier.Value != watchAppBundleIdentifier)
Log.LogError (7031, info, $"The Watch Extension '{name}' has an invalid WKAppBundleIdentifier value ('{appBundleIdentifier.Value}'), it does not match the parent Watch App bundle's CFBundleIdentifier ('{watchAppBundleIdentifier}').");
} else {
Log.LogError (7031, info, $"The Watch Extension '{name}' has an invalid Info.plist: the NSExtensionAttributes dictionary must contain a WKAppBundleIdentifier.");
}
PObject requiredDeviceCapabilities;
if (plist.TryGetValue ("UIRequiredDeviceCapabilities", out requiredDeviceCapabilities)) {
var requiredDeviceCapabilitiesDictionary = requiredDeviceCapabilities as PDictionary;
var requiredDeviceCapabilitiesArray = requiredDeviceCapabilities as PArray;
if (requiredDeviceCapabilitiesDictionary != null) {
PBoolean watchCompanion;
if (requiredDeviceCapabilitiesDictionary.TryGetValue ("watch-companion", out watchCompanion))
Log.LogError (7032, info, $"The WatchKit Extension '{name}' has an invalid Info.plist: the UIRequiredDeviceCapabilities dictionary should not contain the 'watch-companion' capability.");
} else if (requiredDeviceCapabilitiesArray != null) {
if (requiredDeviceCapabilitiesArray.OfType<PString> ().Any (x => x.Value == "watch-companion"))
Log.LogError (7032, info, $"The WatchKit Extension '{name}' has an invalid Info.plist: the UIRequiredDeviceCapabilities array should not contain the 'watch-companion' capability.");
}
}
}
void ValidateWatchOS1App (string path, string extensionName, string mainBundleIdentifier, string wkAppBundleIdentifier)
{
var name = Path.GetFileNameWithoutExtension (path);
var info = Path.Combine (path, "Info.plist");
if (!File.Exists (info)) {
Log.LogError (7033, path, $"The Watch App '{name}' does not contain an Info.plist.");
return;
}
var plist = PDictionary.FromFile (info);
var bundleIdentifier = plist.GetCFBundleIdentifier ();
if (string.IsNullOrEmpty (bundleIdentifier)) {
Log.LogError (7034, info, $"The Watch App '{name}' does not specify a CFBundleIdentifier.");
return;
}
var deviceFamily = plist.GetUIDeviceFamily ();
IPhoneDeviceType expectedDeviceFamily;
string expectedDeviceFamilyString;
if (SdkIsSimulator) {
expectedDeviceFamily = IPhoneDeviceType.Watch | IPhoneDeviceType.IPhone;
expectedDeviceFamilyString = "IPhone, Watch (1, 4)";
} else {
expectedDeviceFamily = IPhoneDeviceType.Watch;
expectedDeviceFamilyString = "Watch (4)";
}
if (deviceFamily != expectedDeviceFamily)
Log.LogError (7035, info, $"The Watch App '{name}' does not have a valid UIDeviceFamily value. Expected '{expectedDeviceFamilyString}' but found '{deviceFamily.ToString ()} ({(int)deviceFamily})'.");
var executable = plist.GetCFBundleExecutable ();
if (string.IsNullOrEmpty (executable))
Log.LogError (7036, info, $"The Watch App '{name}' does not specify a CFBundleExecutable.");
if (bundleIdentifier != wkAppBundleIdentifier)
Log.LogError (7037, info, $"The WatchKit Extension '{extensionName}' has an invalid WKAppBundleIdentifier value ('{wkAppBundleIdentifier}'), it does not match the Watch App's CFBundleIdentifier ('{bundleIdentifier}').");
var companionAppBundleIdentifier = plist.Get<PString> ("WKCompanionAppBundleIdentifier");
if (companionAppBundleIdentifier != null) {
if (companionAppBundleIdentifier.Value != mainBundleIdentifier)
Log.LogError (7038, info, $"The Watch App '{name}' has an invalid WKCompanionAppBundleIdentifier value ('{companionAppBundleIdentifier.Value}'), it does not match the main app bundle's CFBundleIdentifier ('{mainBundleIdentifier}').");
} else {
Log.LogError (7038, info, $"The Watch App '{name}' has an invalid Info.plist: the WKCompanionAppBundleIdentifier must exist and must match the main app bundle's CFBundleIdentifier.");
}
if (plist.ContainsKey ("LSRequiresIPhoneOS"))
Log.LogError (7039, info, $"The Watch App '{name}' has an invalid Info.plist: the LSRequiresIPhoneOS key must not be present.");
}
public override bool Execute ()
{
var mainInfoPath = Path.Combine (AppBundlePath, "Info.plist");
if (!File.Exists (mainInfoPath)) {
Log.LogError (7040, AppBundlePath, $"The app bundle {AppBundlePath} does not contain an Info.plist.");
return false;
}
var plist = PDictionary.FromFile (mainInfoPath);
var bundleIdentifier = plist.GetCFBundleIdentifier ();
if (string.IsNullOrEmpty (bundleIdentifier)) {
Log.LogError (7041, mainInfoPath, $"{mainInfoPath} does not specify a CFBundleIdentifier.");
return false;
}
var executable = plist.GetCFBundleExecutable ();
if (string.IsNullOrEmpty (executable))
Log.LogError (7042, mainInfoPath, $"{mainInfoPath} does not specify a CFBundleExecutable.");
var supportedPlatforms = plist.GetArray (ManifestKeys.CFBundleSupportedPlatforms);
var platform = string.Empty;
if (supportedPlatforms == null || supportedPlatforms.Count == 0) {
Log.LogError (7043, mainInfoPath, $"{mainInfoPath} does not specify a CFBundleSupportedPlatforms.");
} else {
platform = (PString) supportedPlatforms[0];
}
// Validate UIDeviceFamily
var deviceTypes = plist.GetUIDeviceFamily ();
var deviceFamilies = deviceTypes.ToDeviceFamily ();
AppleDeviceFamily[] validFamilies = null;
switch (Framework) {
case PlatformFramework.iOS:
validFamilies = new AppleDeviceFamily[] {
AppleDeviceFamily.IPhone,
AppleDeviceFamily.IPad,
AppleDeviceFamily.Watch
};
break;
case PlatformFramework.WatchOS:
validFamilies = new AppleDeviceFamily[] { AppleDeviceFamily.Watch };
break;
case PlatformFramework.TVOS:
validFamilies = new AppleDeviceFamily[] { AppleDeviceFamily.TV };
break;
default:
Log.LogError ("Invalid framework: {0}", Framework);
break;
}
if (validFamilies != null) {
if (validFamilies.Length == 0) {
Log.LogError (7044, mainInfoPath, $"{mainInfoPath} does not specify a UIDeviceFamily.");
} else {
foreach (var family in deviceFamilies) {
if (Array.IndexOf (validFamilies, family) == -1) {
Log.LogError (7044, mainInfoPath, $"{mainInfoPath} is invalid: the UIDeviceFamily key must contain a value for '{family}'.");
}
}
}
}
var mainShortVersionString = plist.GetCFBundleShortVersionString ();
var mainVersion = plist.GetCFBundleVersion ();
if (Directory.Exists (Path.Combine (AppBundlePath, "PlugIns"))) {
foreach (var plugin in Directory.GetDirectories (Path.Combine (AppBundlePath, "PlugIns"), "*.appex"))
ValidateAppExtension (plugin, bundleIdentifier, mainShortVersionString, mainVersion);
}
if (Directory.Exists (Path.Combine (AppBundlePath, "Watch"))) {
foreach (var watchApp in Directory.GetDirectories (Path.Combine (AppBundlePath, "Watch"), "*.app"))
ValidateWatchApp (watchApp, bundleIdentifier, mainShortVersionString, mainVersion);
}
return !Log.HasLoggedErrors;
}
}
}