500 строки
14 KiB
C#
500 строки
14 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
|
|
using Microsoft.Build.Framework;
|
|
|
|
using Xamarin.Localization.MSBuild;
|
|
using Xamarin.Utils;
|
|
|
|
namespace Xamarin.MacDev.Tasks
|
|
{
|
|
public abstract class CompileEntitlementsTaskBase : XamarinTask
|
|
{
|
|
bool warnedTeamIdentifierPrefix;
|
|
bool warnedAppIdentifierPrefix;
|
|
|
|
static readonly HashSet<string> macAllowedProvisioningKeys = new HashSet<string> {
|
|
"com.apple.application-identifier",
|
|
"com.apple.developer.aps-environment",
|
|
"com.apple.developer.default-data-protection",
|
|
//"com.apple.developer.icloud-container-development-container-identifiers",
|
|
//"com.apple.developer.icloud-container-identifiers",
|
|
//"com.apple.developer.icloud-container-environment",
|
|
//"com.apple.developer.icloud-services",
|
|
"com.apple.developer.pass-type-identifiers",
|
|
"com.apple.developer.team-identifier",
|
|
//"com.apple.developer.ubiquity-container-identifiers",
|
|
"get-task-allow",
|
|
};
|
|
|
|
static readonly HashSet<string> iOSAllowedProvisioningKeys = new HashSet<string> {
|
|
"application-identifier",
|
|
"aps-environment",
|
|
"beta-reports-active",
|
|
"com.apple.developer.default-data-protection",
|
|
|
|
"com.apple.developer.icloud-container-environment",
|
|
"com.apple.developer.icloud-container-identifiers",
|
|
"com.apple.developer.pass-type-identifiers",
|
|
"com.apple.developer.team-identifier",
|
|
"com.apple.developer.ubiquity-container-identifiers",
|
|
"get-task-allow"
|
|
};
|
|
|
|
|
|
#region Inputs
|
|
|
|
[Required]
|
|
public string AppBundleDir { get; set; }
|
|
|
|
[Required]
|
|
public string AppIdentifier { get; set; }
|
|
|
|
[Required]
|
|
public string BundleIdentifier { get; set; }
|
|
|
|
[Required]
|
|
public ITaskItem CompiledEntitlements { get; set; }
|
|
|
|
public bool Debug { get; set; }
|
|
|
|
public string Entitlements { get; set; }
|
|
|
|
[Required]
|
|
public bool IsAppExtension { get; set; }
|
|
|
|
public string ProvisioningProfile { get; set; }
|
|
|
|
[Required]
|
|
public string SdkDevPath { get; set; }
|
|
|
|
public bool SdkIsSimulator { get; set; }
|
|
|
|
[Required]
|
|
public string SdkPlatform { get; set; }
|
|
|
|
[Required]
|
|
public string SdkVersion { get; set; }
|
|
|
|
[Output]
|
|
public ITaskItem EntitlementsInExecutable { get; set; }
|
|
|
|
[Output]
|
|
public ITaskItem EntitlementsInSignature { get; set; }
|
|
|
|
#endregion
|
|
|
|
protected string ApplicationIdentifierKey {
|
|
get {
|
|
switch (Platform) {
|
|
case ApplePlatform.iOS:
|
|
case ApplePlatform.TVOS:
|
|
case ApplePlatform.WatchOS:
|
|
return "application-identifier";
|
|
case ApplePlatform.MacOSX:
|
|
case ApplePlatform.MacCatalyst:
|
|
return "com.apple.application-identifier";
|
|
default:
|
|
throw new InvalidOperationException (string.Format (MSBStrings.InvalidPlatform, Platform));
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual string DefaultEntitlementsPath {
|
|
get {
|
|
return Path.Combine (Sdks.GetAppleSdk (TargetFrameworkMoniker).GetSdkPath (SdkVersion, false), "Entitlements.plist");
|
|
}
|
|
}
|
|
|
|
protected HashSet<string> AllowedProvisioningKeys {
|
|
get {
|
|
switch (Platform) {
|
|
case ApplePlatform.iOS:
|
|
case ApplePlatform.TVOS:
|
|
case ApplePlatform.WatchOS:
|
|
return iOSAllowedProvisioningKeys;
|
|
case ApplePlatform.MacOSX:
|
|
case ApplePlatform.MacCatalyst:
|
|
return macAllowedProvisioningKeys;
|
|
default:
|
|
throw new InvalidOperationException (string.Format (MSBStrings.InvalidPlatform, Platform));
|
|
}
|
|
}
|
|
}
|
|
|
|
protected string EntitlementBundlePath {
|
|
get {
|
|
switch (Platform) {
|
|
case ApplePlatform.iOS:
|
|
case ApplePlatform.TVOS:
|
|
case ApplePlatform.WatchOS:
|
|
return AppBundleDir;
|
|
case ApplePlatform.MacOSX:
|
|
case ApplePlatform.MacCatalyst:
|
|
return Path.Combine (AppBundleDir, "Contents", "Resources");
|
|
default:
|
|
throw new InvalidOperationException (string.Format (MSBStrings.InvalidPlatform, Platform));
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual bool MergeProfileEntitlements {
|
|
get { return true; }
|
|
}
|
|
|
|
PString MergeEntitlementString (PString pstr, MobileProvision profile, bool expandWildcards)
|
|
{
|
|
string TeamIdentifierPrefix;
|
|
string AppIdentifierPrefix;
|
|
|
|
if (string.IsNullOrEmpty (pstr.Value))
|
|
return (PString) pstr.Clone ();
|
|
|
|
if (profile == null) {
|
|
if (!warnedTeamIdentifierPrefix && pstr.Value.Contains ("$(TeamIdentifierPrefix)")) {
|
|
Log.LogWarning (null, null, null, Entitlements, 0, 0, 0, 0, MSBStrings.W0108);
|
|
warnedTeamIdentifierPrefix = true;
|
|
}
|
|
|
|
if (!warnedAppIdentifierPrefix && pstr.Value.Contains ("$(AppIdentifierPrefix)")) {
|
|
Log.LogWarning (null, null, null, Entitlements, 0, 0, 0, 0, MSBStrings.W0109);
|
|
warnedAppIdentifierPrefix = true;
|
|
}
|
|
}
|
|
|
|
if (profile != null && profile.ApplicationIdentifierPrefix.Count > 0)
|
|
AppIdentifierPrefix = profile.ApplicationIdentifierPrefix[0] + ".";
|
|
else
|
|
AppIdentifierPrefix = string.Empty;
|
|
|
|
if (profile != null && profile.TeamIdentifierPrefix.Count > 0)
|
|
TeamIdentifierPrefix = profile.TeamIdentifierPrefix[0] + ".";
|
|
else
|
|
TeamIdentifierPrefix = AppIdentifierPrefix;
|
|
|
|
var customTags = new Dictionary<string, string> (StringComparer.OrdinalIgnoreCase) {
|
|
{ "TeamIdentifierPrefix", TeamIdentifierPrefix },
|
|
{ "AppIdentifierPrefix", AppIdentifierPrefix },
|
|
{ "CFBundleIdentifier", BundleIdentifier },
|
|
};
|
|
|
|
var expanded = StringParserService.Parse (pstr.Value, customTags);
|
|
|
|
if (expandWildcards && expanded.IndexOf ('*') != -1) {
|
|
int asterisk = expanded.IndexOf ('*');
|
|
string prefix;
|
|
|
|
if (expanded.StartsWith (TeamIdentifierPrefix, StringComparison.Ordinal))
|
|
prefix = TeamIdentifierPrefix;
|
|
else if (expanded.StartsWith (AppIdentifierPrefix, StringComparison.Ordinal))
|
|
prefix = AppIdentifierPrefix;
|
|
else
|
|
prefix = string.Empty;
|
|
|
|
var baseBundleIdentifier = expanded.Substring (prefix.Length, asterisk - prefix.Length);
|
|
|
|
if (!BundleIdentifier.StartsWith (baseBundleIdentifier, StringComparison.Ordinal))
|
|
expanded = expanded.Replace ("*", BundleIdentifier);
|
|
else
|
|
expanded = prefix + BundleIdentifier;
|
|
}
|
|
|
|
return new PString (expanded);
|
|
}
|
|
|
|
PArray MergeEntitlementArray (PArray array, MobileProvision profile)
|
|
{
|
|
var result = new PArray ();
|
|
|
|
foreach (var item in array) {
|
|
PObject value;
|
|
|
|
if (item is PDictionary)
|
|
value = MergeEntitlementDictionary ((PDictionary) item, profile);
|
|
else if (item is PString)
|
|
value = MergeEntitlementString ((PString) item, profile, false);
|
|
else if (item is PArray)
|
|
value = MergeEntitlementArray ((PArray) item, profile);
|
|
else
|
|
value = item.Clone ();
|
|
|
|
if (value != null)
|
|
result.Add (value);
|
|
}
|
|
|
|
if (result.Count > 0)
|
|
return result;
|
|
|
|
return null;
|
|
}
|
|
|
|
PDictionary MergeEntitlementDictionary (PDictionary dict, MobileProvision profile)
|
|
{
|
|
var result = new PDictionary ();
|
|
|
|
foreach (var item in dict) {
|
|
PObject value = item.Value;
|
|
|
|
if (value is PDictionary)
|
|
value = MergeEntitlementDictionary ((PDictionary) value, profile);
|
|
else if (value is PString)
|
|
value = MergeEntitlementString ((PString) value, profile, false);
|
|
else if (value is PArray)
|
|
value = MergeEntitlementArray ((PArray) value, profile);
|
|
else
|
|
value = value.Clone ();
|
|
|
|
if (value != null)
|
|
result.Add (item.Key, value);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static bool AreEqual (byte[] x, byte[] y)
|
|
{
|
|
if (x.Length != y.Length)
|
|
return false;
|
|
|
|
for (int i = 0; i < x.Length; i++) {
|
|
if (x[i] != y[i])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void WriteXcent (PObject doc, string path)
|
|
{
|
|
var buf = doc.ToByteArray (false);
|
|
|
|
using (var stream = new MemoryStream ()) {
|
|
stream.Write (buf, 0, buf.Length);
|
|
|
|
var src = stream.ToArray ();
|
|
bool save;
|
|
|
|
// Note: if the destination file already exists, only re-write it if the content will change
|
|
if (File.Exists (path)) {
|
|
var dest = File.ReadAllBytes (path);
|
|
|
|
save = !AreEqual (src, dest);
|
|
} else {
|
|
save = true;
|
|
}
|
|
|
|
if (save)
|
|
File.WriteAllBytes (path, src);
|
|
}
|
|
}
|
|
|
|
protected virtual PDictionary GetCompiledEntitlements (MobileProvision profile, PDictionary template)
|
|
{
|
|
var entitlements = new PDictionary ();
|
|
|
|
if (profile != null && MergeProfileEntitlements) {
|
|
// start off with the settings from the provisioning profile
|
|
foreach (var item in profile.Entitlements) {
|
|
if (!AllowedProvisioningKeys.Contains (item.Key))
|
|
continue;
|
|
|
|
var value = item.Value;
|
|
|
|
if (item.Key == "com.apple.developer.icloud-container-environment")
|
|
value = new PString ("Development");
|
|
else if (value is PDictionary)
|
|
value = MergeEntitlementDictionary ((PDictionary) value, profile);
|
|
else if (value is PString)
|
|
value = MergeEntitlementString ((PString) value, profile, item.Key == ApplicationIdentifierKey);
|
|
else if (value is PArray)
|
|
value = MergeEntitlementArray ((PArray) value, profile);
|
|
else
|
|
value = value.Clone ();
|
|
|
|
if (value != null)
|
|
entitlements.Add (item.Key, value);
|
|
}
|
|
}
|
|
|
|
// merge in the user's values
|
|
foreach (var item in template) {
|
|
var value = item.Value;
|
|
|
|
if (item.Key == "com.apple.developer.ubiquity-container-identifiers" ||
|
|
item.Key == "com.apple.developer.icloud-container-identifiers" ||
|
|
item.Key == "com.apple.developer.icloud-container-environment" ||
|
|
item.Key == "com.apple.developer.icloud-services") {
|
|
if (profile == null)
|
|
Log.LogWarning (null, null, null, Entitlements, 0, 0, 0, 0, MSBStrings.W0110, item.Key);
|
|
else if (!profile.Entitlements.ContainsKey (item.Key))
|
|
Log.LogWarning (null, null, null, Entitlements, 0, 0, 0, 0, MSBStrings.W0111, item.Key);
|
|
} else if (item.Key == ApplicationIdentifierKey) {
|
|
var str = value as PString;
|
|
|
|
// Ignore ONLY if it is empty, otherwise take the user's value
|
|
if (str == null || string.IsNullOrEmpty (str.Value))
|
|
continue;
|
|
}
|
|
|
|
if (value is PDictionary)
|
|
value = MergeEntitlementDictionary ((PDictionary) value, profile);
|
|
else if (value is PString)
|
|
value = MergeEntitlementString ((PString) value, profile, item.Key == ApplicationIdentifierKey);
|
|
else if (value is PArray)
|
|
value = MergeEntitlementArray ((PArray) value, profile);
|
|
else
|
|
value = value.Clone ();
|
|
|
|
if (value != null)
|
|
entitlements[item.Key] = value;
|
|
}
|
|
|
|
switch (Platform) {
|
|
case ApplePlatform.MacOSX:
|
|
case ApplePlatform.MacCatalyst:
|
|
if (Debug && entitlements.TryGetValue ("com.apple.security.app-sandbox", out PBoolean sandbox) && sandbox.Value)
|
|
entitlements ["com.apple.security.network.client"] = new PBoolean (true);
|
|
break;
|
|
}
|
|
|
|
return entitlements;
|
|
}
|
|
|
|
static PDictionary GetArchivedExpandedEntitlements (PDictionary template, PDictionary compiled)
|
|
{
|
|
var allowed = new HashSet<string> ();
|
|
|
|
// the template (user-supplied Entitlements.plist file) is used to create a whitelist of keys
|
|
allowed.Add ("com.apple.developer.icloud-container-environment");
|
|
foreach (var item in template)
|
|
allowed.Add (item.Key);
|
|
|
|
// now we duplicate the allowed keys from the compiled xcent file
|
|
var archived = new PDictionary ();
|
|
|
|
foreach (var item in compiled) {
|
|
if (allowed.Contains (item.Key))
|
|
archived.Add (item.Key, item.Value.Clone ());
|
|
}
|
|
|
|
return archived;
|
|
}
|
|
|
|
protected virtual MobileProvision GetMobileProvision (MobileProvisionPlatform platform, string name)
|
|
{
|
|
return MobileProvisionIndex.GetMobileProvision (platform, name);
|
|
}
|
|
|
|
public override bool Execute ()
|
|
{
|
|
MobileProvisionPlatform platform;
|
|
MobileProvision profile;
|
|
PDictionary template;
|
|
PDictionary compiled;
|
|
PDictionary archived;
|
|
string path;
|
|
|
|
switch (SdkPlatform) {
|
|
case "AppleTVSimulator":
|
|
case "AppleTVOS":
|
|
platform = MobileProvisionPlatform.tvOS;
|
|
break;
|
|
case "iPhoneSimulator":
|
|
case "WatchSimulator":
|
|
case "iPhoneOS":
|
|
case "WatchOS":
|
|
platform = MobileProvisionPlatform.iOS;
|
|
break;
|
|
case "MacOSX":
|
|
platform = MobileProvisionPlatform.MacOS;
|
|
break;
|
|
case "MacCatalyst":
|
|
platform = MobileProvisionPlatform.MacOS;
|
|
break;
|
|
default:
|
|
Log.LogError (MSBStrings.E0048, SdkPlatform);
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty (ProvisioningProfile)) {
|
|
if ((profile = GetMobileProvision (platform, ProvisioningProfile)) == null) {
|
|
Log.LogError (MSBStrings.E0049, ProvisioningProfile);
|
|
return false;
|
|
}
|
|
} else {
|
|
profile = null;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty (Entitlements)) {
|
|
if (!File.Exists (Entitlements)) {
|
|
Log.LogError (MSBStrings.E0112, Entitlements);
|
|
return false;
|
|
}
|
|
|
|
path = Entitlements;
|
|
} else {
|
|
path = DefaultEntitlementsPath;
|
|
}
|
|
|
|
try {
|
|
template = PDictionary.FromFile (path);
|
|
} catch (Exception ex) {
|
|
Log.LogError (MSBStrings.E0113, path, ex.Message);
|
|
return false;
|
|
}
|
|
|
|
compiled = GetCompiledEntitlements (profile, template);
|
|
archived = GetArchivedExpandedEntitlements (template, compiled);
|
|
|
|
try {
|
|
Directory.CreateDirectory (Path.GetDirectoryName (CompiledEntitlements.ItemSpec));
|
|
WriteXcent (compiled, CompiledEntitlements.ItemSpec);
|
|
} catch (Exception ex) {
|
|
Log.LogError (MSBStrings.E0114, CompiledEntitlements, ex.Message);
|
|
return false;
|
|
}
|
|
|
|
SaveArchivedExpandedEntitlements (archived);
|
|
|
|
if (SdkIsSimulator) {
|
|
if (compiled.Count > 0) {
|
|
EntitlementsInExecutable = CompiledEntitlements;
|
|
}
|
|
} else {
|
|
EntitlementsInSignature = CompiledEntitlements;
|
|
}
|
|
|
|
return !Log.HasLoggedErrors;
|
|
}
|
|
|
|
bool SaveArchivedExpandedEntitlements (PDictionary archived)
|
|
{
|
|
if (Platform == Utils.ApplePlatform.MacCatalyst) {
|
|
// I'm not sure if we need this in catalyst or not, but skip it until it's proven we actually need it.
|
|
return true;
|
|
}
|
|
|
|
var path = Path.Combine (EntitlementBundlePath, "archived-expanded-entitlements.xcent");
|
|
|
|
if (File.Exists (path)) {
|
|
var plist = PDictionary.FromFile (path);
|
|
var src = archived.ToXml ();
|
|
var dest = plist.ToXml ();
|
|
|
|
if (src == dest)
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
archived.Save (path, true);
|
|
} catch (Exception ex) {
|
|
Log.LogError (MSBStrings.E0115, ex.Message);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|