diff --git a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.Designer.cs b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.Designer.cs index 3e62a91f91..8d0d80d6b0 100644 --- a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.Designer.cs +++ b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.Designer.cs @@ -2636,5 +2636,32 @@ namespace Xamarin.Localization.MSBuild { return ResourceManager.GetString("W7093", resourceCulture); } } + + /// + /// Looks up a localized string similar to Code signing has been requested multiple times for '{0}', with different metadata. The metadata for one are: '{1}', while the metadata for the other are: '{2}'. + /// + public static string W7095 { + get { + return ResourceManager.GetString("W7095", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code signing has been requested multiple times for '{0}', with different metadata. The metadata '{1}={2}' has been set for one item, but not the other.. + /// + public static string W7096 { + get { + return ResourceManager.GetString("W7096", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code signing has been requested multiple times for '{0}', with different metadata. The metadata '{1}' has been values for each item (once it's '{2}', another time it's '{3}').. + /// + public static string W7097 { + get { + return ResourceManager.GetString("W7097", resourceCulture); + } + } } } diff --git a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx index 3935d1365d..e1f7331997 100644 --- a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx +++ b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx @@ -1395,4 +1395,16 @@ The file or directory '{0}' is not a framework nor a file within a framework. + + + Code signing has been requested multiple times for '{0}', with different metadata. The metadata for one are: '{1}', while the metadata for the other are: '{2}' + + + + Code signing has been requested multiple times for '{0}', with different metadata. The metadata '{1}={2}' has been set for one item, but not the other. + + + + Code signing has been requested multiple times for '{0}', with different metadata. The metadata '{1}' has been values for each item (once it's '{2}', another time it's '{3}'). + diff --git a/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ComputeCodesignItemsTaskBase.cs b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ComputeCodesignItemsTaskBase.cs index b17f77a6a0..38deef5328 100644 --- a/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ComputeCodesignItemsTaskBase.cs +++ b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ComputeCodesignItemsTaskBase.cs @@ -135,6 +135,36 @@ namespace Xamarin.MacDev.Tasks { output.Add (item); } + // We may be asked to sign the same item multiple times (in particular for universal .NET builds) + // Here we de-duplicate (based on itemspec). We also verify that the metadata is the same between + // all deduplicated items, and if not, we show a warning. + var grouped = output.GroupBy (v => v.ItemSpec); + foreach (var group in grouped) { + if (group.Count () < 2) + continue; + + var all = group.ToArray (); + var firstMetadata = all [0].CloneCustomMetadataToDictionary (); + for (var i = 1; i < all.Length; i++) { + var nextMetadata = all [i].CloneCustomMetadataToDictionary (); + if (nextMetadata.Count != firstMetadata.Count) { + Log.LogWarning (MSBStrings.W7095, /* Code signing has been requested multiple times for '{0}', with different metadata. The metadata for one are: '{1}', while the metadata for the other are: '{2}' */ group.Key, string.Join (", ", firstMetadata.Keys), string.Join (", ", nextMetadata.Keys)); + } else { + foreach (var kvp in firstMetadata) { + if (!nextMetadata.TryGetValue (kvp.Key, out var nextValue)) { + Log.LogWarning (MSBStrings.W7096, /* Code signing has been requested multiple times for '{0}', with different metadata. The metadata '{1}={2}' has been set for one item, but not the other. */ group.Key, kvp.Key, kvp.Value); + continue; + } + if (nextValue != kvp.Value) { + Log.LogWarning (MSBStrings.W7097, /* Code signing has been requested multiple times for '{0}', with different metadata. The metadata '{1}' has been values for each item (once it's '{2}', another time it's '{3}'). */ group.Key, kvp.Key, kvp.Value, nextValue); + continue; + } + } + } + output.Remove (all [i]); + } + } + OutputCodesignItems = output.ToArray (); return !Log.HasLoggedErrors; diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ComputeCodesignItemsTaskTests.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ComputeCodesignItemsTaskTests.cs index d7b23d1125..9d006de1c7 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ComputeCodesignItemsTaskTests.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ComputeCodesignItemsTaskTests.cs @@ -355,68 +355,220 @@ namespace Xamarin.MacDev.Tasks { task.NativeStripItems = nativeStripItems.ToArray (); task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform, isDotNet).ToString (); Assert.IsTrue (task.Execute (), "Execute"); + Assert.AreEqual (0, Engine.Logger.WarningsEvents.Count, "Warning Count"); - var outputCodesignItems = task.OutputCodesignItems; - Assert.That (outputCodesignItems.Select (v => v.ItemSpec), Is.Unique, "Uniqueness"); - - var failures = new List (); - var itemsFound = new List (); - foreach (var info in infos) { - var item = outputCodesignItems.SingleOrDefault (v => string.Equals (v.ItemSpec, info.ItemSpec, StringComparison.OrdinalIgnoreCase)); - info.CodesignItem = item; - if (IsPlatform (info.SignedOn, platform)) { - if (item is null) { - failures.Add ($"Expected '{info.ItemSpec}' to be signed."); - continue; - } - } else { - if (item is not null) { - failures.Add ($"Did not expect '{info.ItemSpec}' to be signed."); - continue; - } - } - - if (item is null) - continue; - itemsFound.Add (item); - - foreach (var kvp in info.Metadata) { - var metadata = item.GetMetadata (kvp.Key); - if (metadata == string.Empty && kvp.Value != string.Empty) { - failures.Add ($"Item '{info.ItemSpec}': Expected metadata '{kvp.Key}' not found (with value '{kvp.Value}')."); - } else if (!string.Equals (metadata, kvp.Value)) { - failures.Add ($"Item '{info.ItemSpec}': Expected value '{kvp.Value}' for metadata '{kvp.Key}', but got '{metadata}' instead.\nExpected: {kvp.Value}\nActual: {metadata}"); - } - } - - var customMetadata = item.CopyCustomMetadata (); - var unexpectedMetadata = customMetadata.Keys.ToHashSet (); - unexpectedMetadata.ExceptWith (info.Metadata.Keys); - unexpectedMetadata.Remove ("OriginalItemSpec"); - foreach (var unexpected in unexpectedMetadata) { - if (string.IsNullOrEmpty (customMetadata [unexpected])) - continue; - failures.Add ($"Item '{info.ItemSpec}': Unexpected metadata '{unexpected}' with value '{customMetadata [unexpected]}'."); - } - } - - var itemsNotFound = outputCodesignItems.Where (v => !itemsFound.Contains (v)).ToArray (); - foreach (var itemNotFound in itemsNotFound) { - failures.Add ($"Did not expect '{itemNotFound.ItemSpec}' to be signed."); - } - - if (failures.Count > 0) { - Console.WriteLine ($"{failures.Count} failures"); - foreach (var f in failures) - Console.WriteLine (f); - Console.WriteLine ($"{failures.Count} failures"); - } - Assert.That (failures, Is.Empty, "Failures"); + VerifyCodesigningResults (infos, task.OutputCodesignItems, platform); } finally { Environment.CurrentDirectory = currentDir; } } + [Test] + [TestCase (ApplePlatform.iOS, true)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, true)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.WatchOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + [TestCase (ApplePlatform.MacOSX, false)] + [TestCase (ApplePlatform.MacCatalyst, true)] + public void Duplicated (ApplePlatform platform, bool isDotNet) + { + var tmpdir = Cache.CreateTemporaryDirectory (); + + var currentDir = Environment.CurrentDirectory; + try { + Environment.CurrentDirectory = tmpdir; + var codesignItems = new List (); + var codesignBundle = new List (); + + string codeSignatureSubdirectory = string.Empty; + switch (platform) { + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + codeSignatureSubdirectory = "Contents/"; + break; + } + + var bundleAppMetadata = new Dictionary { + { "RequireCodeSigning", "true" }, + }; + + var createDumpMetadata = new Dictionary { + { "RequireCodeSigning", "true" }, + }; + + codesignItems = new List { + new TaskItem ("Bundle.app/Contents/MonoBundle/createdump", createDumpMetadata), + new TaskItem ("Bundle.app/Contents/MonoBundle/createdump", createDumpMetadata), + }; + + codesignBundle = new List { + new TaskItem ("Bundle.app", bundleAppMetadata), + }; + + var infos = new CodesignInfo [] { + new CodesignInfo ("Bundle.app", Platforms.All, bundleAppMetadata.Set ("CodesignStampFile", $"Bundle.app/{codeSignatureSubdirectory}_CodeSignature/CodeResources")), + new CodesignInfo ("Bundle.app/Contents/MonoBundle/createdump", Platforms.All, createDumpMetadata.Set ("CodesignStampFile", "codesign-stamp-path/Bundle.app/Contents/MonoBundle/createdump")), + }; + + var allFiles = infos.Select (v => v.ItemSpec).ToArray (); + Touch (tmpdir, allFiles); + + var task = CreateTask (); + task.AppBundleDir = "Bundle.app"; + task.CodesignBundle = codesignBundle.ToArray (); + task.CodesignItems = codesignItems.ToArray (); + task.CodesignStampPath = "codesign-stamp-path/"; + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform, isDotNet).ToString (); + Assert.IsTrue (task.Execute (), "Execute"); + Assert.AreEqual (0, Engine.Logger.WarningsEvents.Count, "Warning Count"); + + VerifyCodesigningResults (infos, task.OutputCodesignItems, platform); + } finally { + Environment.CurrentDirectory = currentDir; + } + } + + [Test] + [TestCase (ApplePlatform.iOS, true)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, true)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.WatchOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + [TestCase (ApplePlatform.MacOSX, false)] + [TestCase (ApplePlatform.MacCatalyst, true)] + public void DuplicatedWithDifferentMetadata (ApplePlatform platform, bool isDotNet) + { + var tmpdir = Cache.CreateTemporaryDirectory (); + + var currentDir = Environment.CurrentDirectory; + try { + Environment.CurrentDirectory = tmpdir; + var codesignItems = new List (); + var codesignBundle = new List (); + + string codeSignatureSubdirectory = string.Empty; + switch (platform) { + case ApplePlatform.MacCatalyst: + case ApplePlatform.MacOSX: + codeSignatureSubdirectory = "Contents/"; + break; + } + + var bundleAppMetadata = new Dictionary { + { "RequireCodeSigning", "true" }, + }; + + var createDumpMetadata1 = new Dictionary { + { "RequireCodeSigning", "true" }, + { "OnlyIn1", "true" }, + { "InOneAndTwoWithDifferentValues", "1" }, + }; + var createDumpMetadata2 = new Dictionary { + { "RequireCodeSigning", "true" }, + { "OnlyIn2", "true" }, + { "InOneAndTwoWithDifferentValues", "2" }, + }; + var createDumpMetadata3 = new Dictionary { + { "RequireCodeSigning", "true" }, + }; + + codesignItems = new List { + new TaskItem ("Bundle.app/Contents/MonoBundle/createdump", createDumpMetadata1), + new TaskItem ("Bundle.app/Contents/MonoBundle/createdump", createDumpMetadata2), + new TaskItem ("Bundle.app/Contents/MonoBundle/createdump", createDumpMetadata3), + }; + + codesignBundle = new List { + new TaskItem ("Bundle.app", bundleAppMetadata), + }; + + var infos = new CodesignInfo [] { + new CodesignInfo ("Bundle.app", Platforms.All, bundleAppMetadata.Set ("CodesignStampFile", $"Bundle.app/{codeSignatureSubdirectory}_CodeSignature/CodeResources")), + new CodesignInfo ("Bundle.app/Contents/MonoBundle/createdump", Platforms.All, createDumpMetadata1.Set ("CodesignStampFile", "codesign-stamp-path/Bundle.app/Contents/MonoBundle/createdump")), + }; + + var allFiles = infos.Select (v => v.ItemSpec).ToArray (); + Touch (tmpdir, allFiles); + + var task = CreateTask (); + task.AppBundleDir = "Bundle.app"; + task.CodesignBundle = codesignBundle.ToArray (); + task.CodesignItems = codesignItems.ToArray (); + task.CodesignStampPath = "codesign-stamp-path/"; + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform, isDotNet).ToString (); + Assert.IsTrue (task.Execute (), "Execute"); + Assert.AreEqual (3, Engine.Logger.WarningsEvents.Count, "Warning Count"); + Assert.AreEqual ("Code signing has been requested multiple times for 'Bundle.app/Contents/MonoBundle/createdump', with different metadata. The metadata 'OnlyIn1=true' has been set for one item, but not the other.", Engine.Logger.WarningsEvents [0].Message, "Message #0"); + Assert.AreEqual ("Code signing has been requested multiple times for 'Bundle.app/Contents/MonoBundle/createdump', with different metadata. The metadata 'InOneAndTwoWithDifferentValues' has been values for each item (once it's '1', another time it's '2').", Engine.Logger.WarningsEvents [1].Message, "Message #1"); + Assert.AreEqual ("Code signing has been requested multiple times for 'Bundle.app/Contents/MonoBundle/createdump', with different metadata. The metadata for one are: 'RequireCodeSigning, OnlyIn1, InOneAndTwoWithDifferentValues, CodesignStampFile', while the metadata for the other are: 'RequireCodeSigning, CodesignStampFile'", Engine.Logger.WarningsEvents [2].Message, "Message #2"); + + VerifyCodesigningResults (infos, task.OutputCodesignItems, platform); + } finally { + Environment.CurrentDirectory = currentDir; + } + } + void VerifyCodesigningResults (CodesignInfo [] infos, ITaskItem[] outputCodesignItems, ApplePlatform platform) + { + Assert.That (outputCodesignItems.Select (v => v.ItemSpec), Is.Unique, "Uniqueness"); + + var failures = new List (); + var itemsFound = new List (); + foreach (var info in infos) { + var item = outputCodesignItems.SingleOrDefault (v => string.Equals (v.ItemSpec, info.ItemSpec, StringComparison.OrdinalIgnoreCase)); + info.CodesignItem = item; + if (IsPlatform (info.SignedOn, platform)) { + if (item is null) { + failures.Add ($"Expected '{info.ItemSpec}' to be signed."); + continue; + } + } else { + if (item is not null) { + failures.Add ($"Did not expect '{info.ItemSpec}' to be signed."); + continue; + } + } + + if (item is null) + continue; + itemsFound.Add (item); + + foreach (var kvp in info.Metadata) { + var metadata = item.GetMetadata (kvp.Key); + if (metadata == string.Empty && kvp.Value != string.Empty) { + failures.Add ($"Item '{info.ItemSpec}': Expected metadata '{kvp.Key}' not found (with value '{kvp.Value}')."); + } else if (!string.Equals (metadata, kvp.Value)) { + failures.Add ($"Item '{info.ItemSpec}': Expected value '{kvp.Value}' for metadata '{kvp.Key}', but got '{metadata}' instead.\nExpected: {kvp.Value}\nActual: {metadata}"); + } + } + + var customMetadata = item.CopyCustomMetadata (); + var unexpectedMetadata = customMetadata.Keys.ToHashSet (); + unexpectedMetadata.ExceptWith (info.Metadata.Keys); + unexpectedMetadata.Remove ("OriginalItemSpec"); + foreach (var unexpected in unexpectedMetadata) { + if (string.IsNullOrEmpty (customMetadata [unexpected])) + continue; + failures.Add ($"Item '{info.ItemSpec}': Unexpected metadata '{unexpected}' with value '{customMetadata [unexpected]}'."); + } + } + + var itemsNotFound = outputCodesignItems.Where (v => !itemsFound.Contains (v)).ToArray (); + foreach (var itemNotFound in itemsNotFound) { + failures.Add ($"Did not expect '{itemNotFound.ItemSpec}' to be signed."); + } + + if (failures.Count > 0) { + Console.WriteLine ($"{failures.Count} failures"); + foreach (var f in failures) + Console.WriteLine (f); + Console.WriteLine ($"{failures.Count} failures"); + } + Assert.That (failures, Is.Empty, "Failures"); + } + bool IsPlatform (Platforms platforms, ApplePlatform platform) { switch (platform) {