diff --git a/docs/building-apps/build-items.md b/docs/building-apps/build-items.md index ff18c071b9e6..2fd81202cf18 100644 --- a/docs/building-apps/build-items.md +++ b/docs/building-apps/build-items.md @@ -186,7 +186,8 @@ Only applicable to iOS and tvOS projects. ## ImageAsset -An item group that contains image assets. +An item group that contains image assets, including files inside asset catalogs +(\*.xcassets) and Icon Composer directories (\*.icon). ## InterfaceDefinition diff --git a/dotnet/DefaultCompilationIncludes.md b/dotnet/DefaultCompilationIncludes.md index 3dec5cdb0a5f..9f14799e6634 100644 --- a/dotnet/DefaultCompilationIncludes.md +++ b/dotnet/DefaultCompilationIncludes.md @@ -33,6 +33,13 @@ All \*.pdf, \*.jpg, \*.png and \*.json files inside asset catalogs (\*.xcassets) in the project directory or any subdirectory are included by default (as `ImageAsset` items). +## Icon Composer files + +All files inside Icon Composer directories (\*.icon) in the project directory +or any subdirectory are included by default (as `ImageAsset` items). Icon +Composer files are created by Xcode's Icon Composer tool (Xcode 26+) and +contain layered app icons with `icon.json` metadata. + ## Atlas Textures All \*.png files inside \*.atlas directories in the project directory or any @@ -52,7 +59,7 @@ included by default (as `Metal` items). All files in the Resources/ subdirectory, except any items in the `Compile` or `EmbeddedResource` item groups, and except the ones mentioned above -(\*.scnassets, \*.storyboard, \*.xib, \*.xcassets, \*.atlas, \*.mlmodel, +(\*.scnassets, \*.storyboard, \*.xib, \*.xcassets, \*.icon, \*.atlas, \*.mlmodel, \*.metal) are included by default (as `BundleResource` items). [1]: https://docs.microsoft.com/en-us/dotnet/core/tools/csproj#default-compilation-includes-in-net-core-projects diff --git a/dotnet/targets/Microsoft.Sdk.DefaultItems.template.props b/dotnet/targets/Microsoft.Sdk.DefaultItems.template.props index d705ae3f56c2..afc08fe18531 100644 --- a/dotnet/targets/Microsoft.Sdk.DefaultItems.template.props +++ b/dotnet/targets/Microsoft.Sdk.DefaultItems.template.props @@ -24,7 +24,7 @@ - + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) false true @@ -65,6 +65,7 @@ $(DefaultItemExcludes); $(DefaultExcludesInProjectFolder); $(_ResourcePrefix)\**\*.xcassets\**\*.*; + $(_ResourcePrefix)\**\*.icon\**\*.*; $(_ResourcePrefix)\**\*.storyboard;**\*.xib; $(_ResourcePrefix)\**\*.atlas\*.png; $(_ResourcePrefix)\**\*.mlmodel; diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ACTool.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ACTool.cs index 38d87d918f9f..7ecd1d033efb 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/ACTool.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ACTool.cs @@ -253,7 +253,7 @@ void FindXCAssetsDirectory (string main, string secondary, out string mainResult mainResult = main; secondaryResult = secondary; - while (!string.IsNullOrEmpty (mainResult) && !mainResult.EndsWith (".xcassets", StringComparison.OrdinalIgnoreCase)) { + while (!string.IsNullOrEmpty (mainResult) && !mainResult.EndsWith (".xcassets", StringComparison.OrdinalIgnoreCase) && !mainResult.EndsWith (".icon", StringComparison.OrdinalIgnoreCase)) { mainResult = Path.GetDirectoryName (mainResult)!; if (!string.IsNullOrEmpty (secondaryResult)) secondaryResult = Path.GetDirectoryName (secondaryResult)!; @@ -292,13 +292,13 @@ public override bool Execute () var vpath = BundleResource.GetVirtualProjectPath (this, imageAsset); var catalogFullPath = imageAsset.GetMetadata ("FullPath"); - // get the parent (which will typically be .appiconset, .launchimage, .imageset, .iconset, etc) + // get the parent (which will typically be .appiconset, .launchimage, .imageset, .iconset, .icon, etc) var catalog = Path.GetDirectoryName (vpath)!; catalogFullPath = Path.GetDirectoryName (catalogFullPath)!; var assetType = Path.GetExtension (catalog).TrimStart ('.'); - // keep walking up the directory structure until we get to the .xcassets directory + // keep walking up the directory structure until we get to the .xcassets or .icon directory FindXCAssetsDirectory (catalog, catalogFullPath, out var catalog2, out var catalogFullPath2); catalog = catalog2; catalogFullPath = catalogFullPath2; @@ -325,11 +325,11 @@ public override bool Execute () 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 (asset); + // Handle both Contents.json (for .xcassets) and icon.json (for .icon folders) + var fileName = Path.GetFileName (vpath); + if (fileName == "Contents.json" || fileName == "icon.json") { + items.Add (asset); + } } // clone any *.xcassets dirs that need cloning @@ -370,8 +370,9 @@ public override bool Execute () 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") + // Handle both Contents.json (for .xcassets) and icon.json (for .icon folders) + var fileName = Path.GetFileName (vpath); + if (fileName != "Contents.json" && fileName != "icon.json") continue; item = new TaskItem (dest); @@ -380,8 +381,9 @@ public override bool Execute () FindXCAssetsDirectory (Path.GetFullPath (dest), "", out var catalogFullPath, out var _); items.Add (new AssetInfo (item, vpath, asset.Catalog, catalogFullPath, asset.AssetType)); } 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") + // Handle both Contents.json (for .xcassets) and icon.json (for .icon folders) + var fileName = Path.GetFileName (vpath); + if (fileName != "Contents.json" && fileName != "icon.json") continue; items.Add (asset); @@ -389,7 +391,7 @@ public override bool Execute () } } - // Note: `items` contains only the Contents.json files at this point + // Note: `items` contains only the Contents.json and icon.json files at this point for (int i = 0; i < items.Count; i++) { var asset = items [i]; var assetItem = asset.Item; @@ -397,16 +399,19 @@ public override bool Execute () var catalog = asset.Catalog; var path = assetItem.GetMetadata ("FullPath"); var assetType = asset.AssetType; + var vpathDirNameWithoutExtension = Path.GetFileNameWithoutExtension (Path.GetDirectoryName (vpath)!); if (Platform == ApplePlatform.TVOS) { - if (assetType.Equals ("imagestack", StringComparison.OrdinalIgnoreCase)) { - imageStacksInAssets.Add (Path.GetFileNameWithoutExtension (Path.GetDirectoryName (vpath)!)); - } else if (assetType.Equals ("brandassets", StringComparison.OrdinalIgnoreCase)) { - brandAssetsInAssets.Add (Path.GetFileNameWithoutExtension (Path.GetDirectoryName (vpath)!)); + if (assetType.Equals ("imagestack", StringComparison.OrdinalIgnoreCase) || assetType.Equals ("icon", StringComparison.OrdinalIgnoreCase)) { + imageStacksInAssets.Add (vpathDirNameWithoutExtension); + } + if (assetType.Equals ("brandassets", StringComparison.OrdinalIgnoreCase) || assetType.Equals ("icon", StringComparison.OrdinalIgnoreCase)) { + brandAssetsInAssets.Add (vpathDirNameWithoutExtension); } } else { - if (assetType.Equals ("appiconset", StringComparison.OrdinalIgnoreCase)) - appIconsInAssets.Add (Path.GetFileNameWithoutExtension (Path.GetDirectoryName (vpath)!)); + if (assetType.Equals ("appiconset", StringComparison.OrdinalIgnoreCase) || assetType.Equals ("icon", StringComparison.OrdinalIgnoreCase)) { + appIconsInAssets.Add (vpathDirNameWithoutExtension); + } } if (unique.Add (catalog)) { @@ -416,7 +421,8 @@ public override bool Execute () catalogs.Add (item); } - if (SdkPlatform != "WatchSimulator") { + // Only process Contents.json files for on-demand resources (not icon.json files) + if (SdkPlatform != "WatchSimulator" && Path.GetFileName (vpath) == "Contents.json") { var text = File.ReadAllText (assetItem.ItemSpec); if (string.IsNullOrEmpty (text)) diff --git a/tests/dotnet/AppWithComposerIcon/AppDelegate.cs b/tests/dotnet/AppWithComposerIcon/AppDelegate.cs new file mode 100644 index 000000000000..128f4f61dcb4 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/AppDelegate.cs @@ -0,0 +1,35 @@ +using System; +using Foundation; + +#if !__MACOS__ +using UIKit; +#endif + +#nullable enable + +namespace AppWithComposerIcon { +#if !(__MACCATALYST__ || __MACOS__) + public class AppDelegate : UIApplicationDelegate { + public override bool FinishedLaunching (UIApplication app, NSDictionary options) + { + return true; + } + } +#endif + + public class Program { + static int Main (string [] args) + { +#if __MACCATALYST__ || __MACOS__ +GC.KeepAlive (typeof (NSObject)); // prevent linking away the platform assembly + +Console.WriteLine (Environment.GetEnvironmentVariable ("MAGIC_WORD")); + +return args.Length; +#else + UIApplication.Main (args, null, typeof (AppDelegate)); + return 0; +#endif + } + } +} diff --git a/tests/dotnet/AppWithComposerIcon/MacCatalyst/AppWithComposerIcon.csproj b/tests/dotnet/AppWithComposerIcon/MacCatalyst/AppWithComposerIcon.csproj new file mode 100644 index 000000000000..6b0e2c773180 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/MacCatalyst/AppWithComposerIcon.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst + + + diff --git a/tests/dotnet/AppWithComposerIcon/MacCatalyst/Makefile b/tests/dotnet/AppWithComposerIcon/MacCatalyst/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/MacCatalyst/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/AppWithComposerIcon/MacCatalyst/Resources/AppIcon.icon/Assets/back.png b/tests/dotnet/AppWithComposerIcon/MacCatalyst/Resources/AppIcon.icon/Assets/back.png new file mode 100644 index 000000000000..00c6bc6118f0 Binary files /dev/null and b/tests/dotnet/AppWithComposerIcon/MacCatalyst/Resources/AppIcon.icon/Assets/back.png differ diff --git a/tests/dotnet/AppWithComposerIcon/MacCatalyst/Resources/AppIcon.icon/Assets/front.png b/tests/dotnet/AppWithComposerIcon/MacCatalyst/Resources/AppIcon.icon/Assets/front.png new file mode 100644 index 000000000000..6dc4283df45a Binary files /dev/null and b/tests/dotnet/AppWithComposerIcon/MacCatalyst/Resources/AppIcon.icon/Assets/front.png differ diff --git a/tests/dotnet/AppWithComposerIcon/MacCatalyst/Resources/AppIcon.icon/icon.json b/tests/dotnet/AppWithComposerIcon/MacCatalyst/Resources/AppIcon.icon/icon.json new file mode 100644 index 000000000000..9cb9eee03bd1 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/MacCatalyst/Resources/AppIcon.icon/icon.json @@ -0,0 +1,16 @@ +{ + "groups" : [ + { + "layers" : [ + { + "image-name" : "back.png", + "name" : "back" + }, + { + "image-name" : "front.png", + "name" : "front" + } + ] + } + ] +} diff --git a/tests/dotnet/AppWithComposerIcon/iOS/AppWithComposerIcon.csproj b/tests/dotnet/AppWithComposerIcon/iOS/AppWithComposerIcon.csproj new file mode 100644 index 000000000000..86d408734aa8 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/iOS/AppWithComposerIcon.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-ios + + + diff --git a/tests/dotnet/AppWithComposerIcon/iOS/Makefile b/tests/dotnet/AppWithComposerIcon/iOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/iOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/AppWithComposerIcon/iOS/Resources/AppIcon.icon/Assets/back.png b/tests/dotnet/AppWithComposerIcon/iOS/Resources/AppIcon.icon/Assets/back.png new file mode 100644 index 000000000000..00c6bc6118f0 Binary files /dev/null and b/tests/dotnet/AppWithComposerIcon/iOS/Resources/AppIcon.icon/Assets/back.png differ diff --git a/tests/dotnet/AppWithComposerIcon/iOS/Resources/AppIcon.icon/Assets/front.png b/tests/dotnet/AppWithComposerIcon/iOS/Resources/AppIcon.icon/Assets/front.png new file mode 100644 index 000000000000..6dc4283df45a Binary files /dev/null and b/tests/dotnet/AppWithComposerIcon/iOS/Resources/AppIcon.icon/Assets/front.png differ diff --git a/tests/dotnet/AppWithComposerIcon/iOS/Resources/AppIcon.icon/icon.json b/tests/dotnet/AppWithComposerIcon/iOS/Resources/AppIcon.icon/icon.json new file mode 100644 index 000000000000..9cb9eee03bd1 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/iOS/Resources/AppIcon.icon/icon.json @@ -0,0 +1,16 @@ +{ + "groups" : [ + { + "layers" : [ + { + "image-name" : "back.png", + "name" : "back" + }, + { + "image-name" : "front.png", + "name" : "front" + } + ] + } + ] +} diff --git a/tests/dotnet/AppWithComposerIcon/macOS/AppWithComposerIcon.csproj b/tests/dotnet/AppWithComposerIcon/macOS/AppWithComposerIcon.csproj new file mode 100644 index 000000000000..a77287b9ba00 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/macOS/AppWithComposerIcon.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-macos + + + diff --git a/tests/dotnet/AppWithComposerIcon/macOS/Makefile b/tests/dotnet/AppWithComposerIcon/macOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/macOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/AppWithComposerIcon/macOS/Resources/AppIcon.icon/Assets/back.png b/tests/dotnet/AppWithComposerIcon/macOS/Resources/AppIcon.icon/Assets/back.png new file mode 100644 index 000000000000..00c6bc6118f0 Binary files /dev/null and b/tests/dotnet/AppWithComposerIcon/macOS/Resources/AppIcon.icon/Assets/back.png differ diff --git a/tests/dotnet/AppWithComposerIcon/macOS/Resources/AppIcon.icon/Assets/front.png b/tests/dotnet/AppWithComposerIcon/macOS/Resources/AppIcon.icon/Assets/front.png new file mode 100644 index 000000000000..6dc4283df45a Binary files /dev/null and b/tests/dotnet/AppWithComposerIcon/macOS/Resources/AppIcon.icon/Assets/front.png differ diff --git a/tests/dotnet/AppWithComposerIcon/macOS/Resources/AppIcon.icon/icon.json b/tests/dotnet/AppWithComposerIcon/macOS/Resources/AppIcon.icon/icon.json new file mode 100644 index 000000000000..9cb9eee03bd1 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/macOS/Resources/AppIcon.icon/icon.json @@ -0,0 +1,16 @@ +{ + "groups" : [ + { + "layers" : [ + { + "image-name" : "back.png", + "name" : "back" + }, + { + "image-name" : "front.png", + "name" : "front" + } + ] + } + ] +} diff --git a/tests/dotnet/AppWithComposerIcon/shared.csproj b/tests/dotnet/AppWithComposerIcon/shared.csproj new file mode 100644 index 000000000000..702960b9dd8e --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/shared.csproj @@ -0,0 +1,23 @@ + + + + Exe + + AppWithComposerIcon + com.xamarin.appwithcomposericon + + true + + + + + + AppIcon + + + + + + + + diff --git a/tests/dotnet/AppWithComposerIcon/shared.mk b/tests/dotnet/AppWithComposerIcon/shared.mk new file mode 100644 index 000000000000..de0244100278 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/shared.mk @@ -0,0 +1,3 @@ +TOP=../../../.. +TESTNAME=AppWithComposerIcon +include $(TOP)/tests/common/shared-dotnet.mk diff --git a/tests/dotnet/AppWithComposerIcon/tvOS/AppWithComposerIcon.csproj b/tests/dotnet/AppWithComposerIcon/tvOS/AppWithComposerIcon.csproj new file mode 100644 index 000000000000..bd487ddcd88d --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/tvOS/AppWithComposerIcon.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + diff --git a/tests/dotnet/AppWithComposerIcon/tvOS/Makefile b/tests/dotnet/AppWithComposerIcon/tvOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/tvOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/AppWithComposerIcon/tvOS/Resources/AppIcon.icon/Assets/back.png b/tests/dotnet/AppWithComposerIcon/tvOS/Resources/AppIcon.icon/Assets/back.png new file mode 100644 index 000000000000..00c6bc6118f0 Binary files /dev/null and b/tests/dotnet/AppWithComposerIcon/tvOS/Resources/AppIcon.icon/Assets/back.png differ diff --git a/tests/dotnet/AppWithComposerIcon/tvOS/Resources/AppIcon.icon/Assets/front.png b/tests/dotnet/AppWithComposerIcon/tvOS/Resources/AppIcon.icon/Assets/front.png new file mode 100644 index 000000000000..6dc4283df45a Binary files /dev/null and b/tests/dotnet/AppWithComposerIcon/tvOS/Resources/AppIcon.icon/Assets/front.png differ diff --git a/tests/dotnet/AppWithComposerIcon/tvOS/Resources/AppIcon.icon/icon.json b/tests/dotnet/AppWithComposerIcon/tvOS/Resources/AppIcon.icon/icon.json new file mode 100644 index 000000000000..9cb9eee03bd1 --- /dev/null +++ b/tests/dotnet/AppWithComposerIcon/tvOS/Resources/AppIcon.icon/icon.json @@ -0,0 +1,16 @@ +{ + "groups" : [ + { + "layers" : [ + { + "image-name" : "back.png", + "name" : "back" + }, + { + "image-name" : "front.png", + "name" : "front" + } + ] + } + ] +} diff --git a/tests/dotnet/UnitTests/AppIconTest.cs b/tests/dotnet/UnitTests/AppIconTest.cs index e8bea97b7c7e..0bdafa8ed9a5 100644 --- a/tests/dotnet/UnitTests/AppIconTest.cs +++ b/tests/dotnet/UnitTests/AppIconTest.cs @@ -672,5 +672,39 @@ void TestXCAssetsImpl (ApplePlatform platform, string runtimeIdentifiers, Dictio throw; } } + + [TestCase (ApplePlatform.iOS, "iossimulator-x64")] + [TestCase (ApplePlatform.TVOS, "tvossimulator-x64")] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-x64")] + [TestCase (ApplePlatform.MacOSX, "osx-x64")] + public void ComposerIcon (ApplePlatform platform, string runtimeIdentifiers) + { + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + Configuration.IgnoreIfIgnoredPlatform (platform); + + var project = "AppWithComposerIcon"; + var projectPath = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath); + Clean (projectPath); + + var properties = GetDefaultProperties (runtimeIdentifiers); + DotNet.Execute ("build", projectPath, properties); + + var resourcesDirectory = GetResourcesDirectory (platform, appPath); + + // Verify that the raw .icon files are not in the app bundle as BundleResources + var iconJsonInBundle = Path.Combine (resourcesDirectory, "AppIcon.icon", "icon.json"); + Assert.That (iconJsonInBundle, Does.Not.Exist, "icon.json should not be in the app bundle as a raw BundleResource"); + + var frontPngInBundle = Path.Combine (resourcesDirectory, "AppIcon.icon", "Assets", "front.png"); + Assert.That (frontPngInBundle, Does.Not.Exist, "front.png should not be in the app bundle as a raw BundleResource"); + + var backPngInBundle = Path.Combine (resourcesDirectory, "AppIcon.icon", "Assets", "back.png"); + Assert.That (backPngInBundle, Does.Not.Exist, "back.png should not be in the app bundle as a raw BundleResource"); + + // Verify that the compiled asset catalog exists in the app bundle + var assetsCar = Path.Combine (resourcesDirectory, "Assets.car"); + Assert.That (assetsCar, Does.Exist, "Assets.car"); + } } } + diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs index 75f52139b7af..1da179b58333 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs @@ -634,5 +634,216 @@ public void XSAppIconAssetsAndAppIcon (ApplePlatform platform) ExecuteTask (actool, 1); Assert.AreEqual ("Can't specify both 'XSAppIconAssets' in the Info.plist and 'AppIcon' in the project file. Please select one or the other.", Engine.Logger.ErrorEvents [0].Message, "Error message"); } + + [Test] + [TestCase (ApplePlatform.iOS)] + [TestCase (ApplePlatform.TVOS)] + [TestCase (ApplePlatform.MacCatalyst)] + [TestCase (ApplePlatform.MacOSX)] + public void IconFileSupport (ApplePlatform platform) + { + // Test that .icon folders (Icon Composer format) are recognized as app icons + var projectDir = Cache.CreateTemporaryDirectory (); + var iconFolderPath = Path.Combine (projectDir, "Resources", "AppIcon.icon"); + var assetsPath = Path.Combine (iconFolderPath, "Assets"); + Directory.CreateDirectory (assetsPath); + + // Create a placeholder icon.json file (simplified structure for testing) + var iconJsonPath = Path.Combine (iconFolderPath, "icon.json"); + File.WriteAllText (iconJsonPath, @"{""groups"":[{""layers"":[{""image-name"":""icon_512x512.png"",""name"":""icon""}]}]}"); + + // Create a placeholder image file in the Assets folder + var imagePath = Path.Combine (assetsPath, "icon_512x512.png"); + File.WriteAllText (imagePath, "placeholder image"); + + var actool = CreateACToolTask ( + platform, + projectDir, + out var _, + iconJsonPath + "|Resources/AppIcon.icon/icon.json", + imagePath + "|Resources/AppIcon.icon/Assets/icon_512x512.png" + ); + actool.AppIcon = "AppIcon"; + + // actool may fail on the placeholder .icon content, but the validation phase should pass + actool.Execute (); + + // Verify that no icon validation errors were logged + AssertNoIconValidationErrors (); + } + + [Test] + [TestCase (ApplePlatform.iOS)] + [TestCase (ApplePlatform.TVOS)] + [TestCase (ApplePlatform.MacCatalyst)] + [TestCase (ApplePlatform.MacOSX)] + public void IconFileSupportWithIncludeAllAppIcons (ApplePlatform platform) + { + // Test that .icon folders work with IncludeAllAppIcons + var projectDir = Cache.CreateTemporaryDirectory (); + var iconFolderPath = Path.Combine (projectDir, "Resources", "AppIcon.icon"); + var assetsPath = Path.Combine (iconFolderPath, "Assets"); + Directory.CreateDirectory (assetsPath); + + var iconJsonPath = Path.Combine (iconFolderPath, "icon.json"); + File.WriteAllText (iconJsonPath, @"{""groups"":[{""layers"":[{""image-name"":""icon_512x512.png"",""name"":""icon""}]}]}"); + + var imagePath = Path.Combine (assetsPath, "icon_512x512.png"); + File.WriteAllText (imagePath, "placeholder image"); + + var actool = CreateACToolTask ( + platform, + projectDir, + out var _, + iconJsonPath + "|Resources/AppIcon.icon/icon.json", + imagePath + "|Resources/AppIcon.icon/Assets/icon_512x512.png" + ); + actool.AppIcon = "AppIcon"; + actool.IncludeAllAppIcons = true; + + // actool may fail on the placeholder .icon content, but the validation phase should pass + actool.Execute (); + + // Verify that no icon validation errors were logged + AssertNoIconValidationErrors (); + } + + [Test] + [TestCase (ApplePlatform.iOS)] + [TestCase (ApplePlatform.TVOS)] + [TestCase (ApplePlatform.MacCatalyst)] + [TestCase (ApplePlatform.MacOSX)] + public void IconFileSupportAsAlternateIcon (ApplePlatform platform) + { + // Test that .icon folders work as alternate app icons + var projectDir = Cache.CreateTemporaryDirectory (); + var iconFolderPath = Path.Combine (projectDir, "Resources", "AlternateIcon.icon"); + var assetsPath = Path.Combine (iconFolderPath, "Assets"); + Directory.CreateDirectory (assetsPath); + + var iconJsonPath = Path.Combine (iconFolderPath, "icon.json"); + File.WriteAllText (iconJsonPath, @"{""groups"":[{""layers"":[{""image-name"":""icon_512x512.png"",""name"":""icon""}]}]}"); + + var imagePath = Path.Combine (assetsPath, "icon_512x512.png"); + File.WriteAllText (imagePath, "placeholder image"); + + // Also need a primary icon for the alternate icon test to make sense + var primaryIconPath = Path.Combine (projectDir, "Resources", "AppIcon.icon"); + var primaryAssetsPath = Path.Combine (primaryIconPath, "Assets"); + Directory.CreateDirectory (primaryAssetsPath); + + var primaryIconJsonPath = Path.Combine (primaryIconPath, "icon.json"); + File.WriteAllText (primaryIconJsonPath, @"{""groups"":[{""layers"":[{""image-name"":""icon_512x512.png"",""name"":""icon""}]}]}"); + + var primaryImagePath = Path.Combine (primaryAssetsPath, "icon_512x512.png"); + File.WriteAllText (primaryImagePath, "placeholder image"); + + var actool = CreateACToolTask ( + platform, + projectDir, + out var _, + iconJsonPath + "|Resources/AlternateIcon.icon/icon.json", + imagePath + "|Resources/AlternateIcon.icon/Assets/icon_512x512.png", + primaryIconJsonPath + "|Resources/AppIcon.icon/icon.json", + primaryImagePath + "|Resources/AppIcon.icon/Assets/icon_512x512.png" + ); + actool.AppIcon = "AppIcon"; + actool.AlternateAppIcons = new ITaskItem [] { new TaskItem ("AlternateIcon") }; + + // actool may fail on the placeholder .icon content, but the validation phase should pass + actool.Execute (); + + // Verify that no icon validation errors were logged + AssertNoIconValidationErrors (); + } + + [Test] + [TestCase (ApplePlatform.iOS)] + [TestCase (ApplePlatform.TVOS)] + [TestCase (ApplePlatform.MacCatalyst)] + [TestCase (ApplePlatform.MacOSX)] + public void InexistentIconFile (ApplePlatform platform) + { + // Test that an inexistent .icon-based app icon is correctly reported + var projectDir = Cache.CreateTemporaryDirectory (); + var iconFolderPath = Path.Combine (projectDir, "Resources", "AppIcon.icon"); + var assetsPath = Path.Combine (iconFolderPath, "Assets"); + Directory.CreateDirectory (assetsPath); + + var iconJsonPath = Path.Combine (iconFolderPath, "icon.json"); + File.WriteAllText (iconJsonPath, @"{""groups"":[{""layers"":[{""image-name"":""icon_512x512.png"",""name"":""icon""}]}]}"); + + var imagePath = Path.Combine (assetsPath, "icon_512x512.png"); + File.WriteAllText (imagePath, "placeholder image"); + + var actool = CreateACToolTask ( + platform, + projectDir, + out var _, + iconJsonPath + "|Resources/AppIcon.icon/icon.json", + imagePath + "|Resources/AppIcon.icon/Assets/icon_512x512.png" + ); + actool.AppIcon = "InexistentIcon"; + + ExecuteTask (actool, 1); + + var errorMessages = Engine.Logger.ErrorEvents.Select (e => e.Message).ToList (); + Assert.IsTrue (errorMessages.Any (m => m.Contains ("Can't find the AppIcon 'InexistentIcon'")), + "Should report that InexistentIcon is not found among image resources"); + } + + [Test] + [TestCase (ApplePlatform.iOS)] + [TestCase (ApplePlatform.MacCatalyst)] + [TestCase (ApplePlatform.MacOSX)] + public void MixedXCAssetsAndIconFile (ApplePlatform platform) + { + // Test that .icon folders and .xcassets can coexist in the validation phase + var projectDir = Path.Combine (Configuration.SourceRoot, "tests", "dotnet", "AppWithXCAssets", platform.AsString ()); + var files = Directory.GetFiles (Path.Combine (projectDir, "Resources", "Images.xcassets"), "*", SearchOption.AllDirectories); + var imageAssets = files.Select (v => v + "|" + v.Substring (projectDir.Length + 1)).ToList (); + + // Add a .icon folder alongside the existing .xcassets + var tmpDir = Cache.CreateTemporaryDirectory (); + var iconFolderPath = Path.Combine (tmpDir, "ComposerIcon.icon"); + var assetsPath = Path.Combine (iconFolderPath, "Assets"); + Directory.CreateDirectory (assetsPath); + + var iconJsonPath = Path.Combine (iconFolderPath, "icon.json"); + File.WriteAllText (iconJsonPath, @"{""groups"":[{""layers"":[{""image-name"":""icon_512x512.png"",""name"":""icon""}]}]}"); + + var imagePath = Path.Combine (assetsPath, "icon_512x512.png"); + File.WriteAllText (imagePath, "placeholder image"); + + imageAssets.Add (iconJsonPath + "|Resources/ComposerIcon.icon/icon.json"); + imageAssets.Add (imagePath + "|Resources/ComposerIcon.icon/Assets/icon_512x512.png"); + + var actool = CreateACToolTask ( + platform, + projectDir, + out var _, + imageAssets.ToArray () + ); + actool.AppIcon = "AppIcons"; + + // actool may fail on the placeholder .icon content, but the validation phase should pass + actool.Execute (); + + // Verify that no icon validation errors were logged + AssertNoIconValidationErrors (); + } + + void AssertNoIconValidationErrors () + { + var errorMessages = Engine.Logger.ErrorEvents.Select (e => e.Message).ToList (); + Assert.That (errorMessages, Has.None.Contain ("Can't find the AppIcon"), + "Should not report that AppIcon is not found among image resources"); + Assert.That (errorMessages, Has.None.Contain ("Can't find the AlternateAppIcon"), + "Should not report that AlternateAppIcon is not found among image resources"); + Assert.That (errorMessages, Has.None.Contain ("is specified as both 'AppIcon' and 'AlternateAppIcon'"), + "Should not report icon conflict between AppIcon and AlternateAppIcon"); + Assert.That (errorMessages, Has.None.Contain ("Can't specify both 'XSAppIconAssets'"), + "Should not report XSAppIconAssets conflict"); + } } }