From 41eb539aed9af741431934a2023cacd1c7a2f133 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 15 Jul 2026 12:49:51 -0400 Subject: [PATCH 1/2] Add common Apple artifact metadata Read authoritative application metadata from the final app manifest and stamp every produced Apple ApplicationArtifact before consumer extension targets run. Cover generated and custom manifests across Apple platforms and document the shared metadata contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c04e140a-a614-44c9-a88a-d4f186b13b9f --- docs/building-apps/build-items.md | 11 +++ docs/building-apps/build-properties.md | 8 +-- dotnet/SingleProject.md | 6 ++ .../Tasks/ReadAppManifest.cs | 8 +++ msbuild/Xamarin.Shared/Xamarin.Shared.targets | 33 ++++++++- .../Info.plist | 16 +++++ .../InfoWithoutDisplayName.plist | 14 ++++ .../shared.csproj | 3 +- tests/dotnet/UnitTests/PostBuildTest.cs | 70 +++++++++++++++++++ .../TaskTests/ReadAppManifestTaskTests.cs | 20 ++++++ 10 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 tests/dotnet/MySimpleAppWithArtifactMetadata/Info.plist create mode 100644 tests/dotnet/MySimpleAppWithArtifactMetadata/InfoWithoutDisplayName.plist diff --git a/docs/building-apps/build-items.md b/docs/building-apps/build-items.md index 84329b453a04..ad9f1ad1f87d 100644 --- a/docs/building-apps/build-items.md +++ b/docs/building-apps/build-items.md @@ -264,6 +264,17 @@ The following metadata is set: * `IsDirectory`: `true` for `.app` and `.xcarchive` outputs; `false` for `.ipa` and `.pkg` outputs. * `PlatformName`: The Apple platform name, such as `iOS`, `tvOS`, `macOS`, or `MacCatalyst`. * `BundleIdentifier`: The resolved app bundle identifier. +* `ApplicationId`: The resolved app bundle identifier. +* `ApplicationTitle`: The final `CFBundleDisplayName` value. +* `ApplicationName`: The final `CFBundleDisplayName` value, falling back to `CFBundleName` when `CFBundleDisplayName` isn't set. +* `ApplicationDisplayVersion`: The final `CFBundleShortVersionString` value. +* `ApplicationVersion`: The final `CFBundleVersion` value. + +The shared application metadata is read from the compiled app bundle +`Info.plist`, so values supplied by a custom manifest take precedence over +single-project properties. Values that are resolved or localized by Apple at a +later stage are returned as written in the compiled manifest; the build does +not choose a locale. Example: diff --git a/docs/building-apps/build-properties.md b/docs/building-apps/build-properties.md index 941410bc8f0a..3d6a33464f12 100644 --- a/docs/building-apps/build-properties.md +++ b/docs/building-apps/build-properties.md @@ -571,10 +571,10 @@ A semi-colon delimited property that can be used to extend the the platform build has collected `@(ApplicationArtifact)` items and before `GetApplicationArtifacts` or `Publish` returns them. -This can be used by SDKs such as .NET MAUI to add shared application metadata -to platform-produced artifacts. Extension targets should update existing -`@(ApplicationArtifact)` items to add metadata; they should only add new items -when introducing additional artifacts. +Apple platform builds populate the common application metadata documented for +[ApplicationArtifact](build-items.md#applicationartifact) before targets in +this property execute. Extension targets can update or override that metadata, +and should only add new items when introducing additional artifacts. Example: diff --git a/dotnet/SingleProject.md b/dotnet/SingleProject.md index 9a20de442520..77bc5c5d8ce1 100644 --- a/dotnet/SingleProject.md +++ b/dotnet/SingleProject.md @@ -20,6 +20,12 @@ Info.plist in the project doesn't already contain entries for these keys): This is only enabled if the `GenerateApplicationManifest` is set to `true` (which is the default for all supported .NET versions) +Final application outputs expose these values as common metadata on +`@(ApplicationArtifact)`. The metadata is read back from the compiled +`Info.plist`, so explicit values in a custom manifest win even when the +corresponding single-project properties are set. `ApplicationName` uses +`CFBundleDisplayName` when present and otherwise falls back to `CFBundleName`. + Additionally, `$(ApplicationDisplayVersion)` will overwrite the value for `$(Version)`, so the following properties will be set with the same value: diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ReadAppManifest.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ReadAppManifest.cs index 6e70959e2d15..86d6c0f0fb27 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/ReadAppManifest.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ReadAppManifest.cs @@ -23,9 +23,15 @@ public class ReadAppManifest : XamarinTask, ITaskCallback { [Output] public string? CFBundleDisplayName { get; set; } + [Output] + public string? CFBundleName { get; set; } + [Output] public string? CFBundleIdentifier { get; set; } + [Output] + public string? CFBundleShortVersionString { get; set; } + [Output] public string? CFBundleVersion { get; set; } @@ -65,7 +71,9 @@ public override bool Execute () CFBundleExecutable = plist.GetCFBundleExecutable (); CFBundleDisplayName = plist?.GetCFBundleDisplayName (); + CFBundleName = plist?.Get (ManifestKeys.CFBundleName)?.Value; CFBundleIdentifier = plist?.GetCFBundleIdentifier (); + CFBundleShortVersionString = plist?.Get (ManifestKeys.CFBundleShortVersionString)?.Value; CFBundleVersion = plist?.GetCFBundleVersion (); CLKComplicationGroup = plist?.Get (ManifestKeys.CLKComplicationGroup)?.Value; diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 311e05580cc6..5db16176c677 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -729,7 +729,9 @@ Copyright (C) 2018 Microsoft. All rights reserved. > + + @@ -3540,7 +3542,36 @@ Copyright (C) 2018 Microsoft. All rights reserved. - + + + + + + + + + + + %(ApplicationArtifact.BundleIdentifier) + $(_ApplicationArtifactCFBundleDisplayName) + $(_ApplicationArtifactCFBundleDisplayName) + $(_ApplicationArtifactCFBundleName) + $(_ApplicationArtifactCFBundleShortVersionString) + $(_ApplicationArtifactCFBundleVersion) + + + + + false diff --git a/tests/dotnet/MySimpleAppWithArtifactMetadata/Info.plist b/tests/dotnet/MySimpleAppWithArtifactMetadata/Info.plist new file mode 100644 index 000000000000..de8dfea8de53 --- /dev/null +++ b/tests/dotnet/MySimpleAppWithArtifactMetadata/Info.plist @@ -0,0 +1,16 @@ + + + + + CFBundleDisplayName + $(PRODUCT_NAME) + CFBundleIdentifier + com.xamarin.customartifactmetadata + CFBundleName + Custom Bundle Name + CFBundleShortVersionString + 9.8.7 + CFBundleVersion + 123 + + diff --git a/tests/dotnet/MySimpleAppWithArtifactMetadata/InfoWithoutDisplayName.plist b/tests/dotnet/MySimpleAppWithArtifactMetadata/InfoWithoutDisplayName.plist new file mode 100644 index 000000000000..48f99fb5e4af --- /dev/null +++ b/tests/dotnet/MySimpleAppWithArtifactMetadata/InfoWithoutDisplayName.plist @@ -0,0 +1,14 @@ + + + + + CFBundleIdentifier + com.xamarin.customartifactmetadata + CFBundleName + Fallback Bundle Name + CFBundleShortVersionString + 9.8.7 + CFBundleVersion + 123 + + diff --git a/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.csproj b/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.csproj index 27aa27792cf2..b7def2f80f40 100644 --- a/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.csproj +++ b/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.csproj @@ -15,9 +15,10 @@ + - + <_MauiObservedAppArtifact Include="@(ApplicationArtifact)" Condition="'%(ApplicationArtifact.PackageFormat)' == 'app'" /> <_MauiObservedPackageArtifact Include="@(ApplicationArtifact)" Condition="'%(ApplicationArtifact.PackageFormat)' == '$(ExpectedAugmentedPackageFormat)'" /> diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index 91c459d8e75e..0fe1aad56175 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -138,6 +138,57 @@ public void GetApplicationArtifactsIpaTest (ApplePlatform platform, string runti AssertApplicationArtifact (outputs, pkgPath, platform, "ipa", isDirectory: false); } + [Test] + [TestCase (ApplePlatform.iOS, "iossimulator-arm64", null, true)] + [TestCase (ApplePlatform.TVOS, "tvossimulator-arm64", null, true)] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", null, true)] + [TestCase (ApplePlatform.MacOSX, "osx-arm64", null, true)] + [TestCase (ApplePlatform.iOS, "iossimulator-arm64", "Info.plist", true)] + [TestCase (ApplePlatform.MacOSX, "osx-arm64", "Info.plist", false)] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", "InfoWithoutDisplayName.plist", false)] + public void ApplicationArtifactMetadataTest (ApplePlatform platform, string runtimeIdentifiers, string? customApplicationManifest, bool generateApplicationManifest) + { + var project = "MySimpleAppWithArtifactMetadata"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["GenerateApplicationManifest"] = generateApplicationManifest ? "true" : "false"; + if (customApplicationManifest is not null) + properties ["CustomApplicationManifest"] = customApplicationManifest; + + var outputs = GetApplicationArtifacts (project_path, properties); + var appOutput = AssertApplicationArtifact (outputs, appPath, platform, "app", isDirectory: true); + + if (customApplicationManifest is null) { + AssertApplicationMetadata ( + appOutput, + "com.xamarin.mysimpleappwithartifactmetadata", + "MySimpleAppWithArtifactMetadata", + "MySimpleAppWithArtifactMetadata", + "3.14", + "3.14"); + } else if (customApplicationManifest == "InfoWithoutDisplayName.plist") { + AssertApplicationMetadata ( + appOutput, + "com.xamarin.customartifactmetadata", + "", + "Fallback Bundle Name", + "9.8.7", + "123"); + } else { + AssertApplicationMetadata ( + appOutput, + "com.xamarin.customartifactmetadata", + "$(PRODUCT_NAME)", + "$(PRODUCT_NAME)", + "9.8.7", + "123"); + } + } + [Test] [TestCase (ApplePlatform.iOS, "ios-arm64", "ipa")] [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", "pkg")] @@ -551,6 +602,10 @@ static ITaskItem AssertApplicationArtifact (string binLogPath, string path, Appl Assert.That (output.GetMetadata ("IsDirectory"), Is.EqualTo (isDirectory ? "true" : "false"), "IsDirectory"); Assert.That (output.GetMetadata ("PlatformName"), Is.EqualTo (platform.AsString ()), "PlatformName"); Assert.That (output.GetMetadata ("BundleIdentifier"), Is.Not.Empty, "BundleIdentifier"); + Assert.That (output.GetMetadata ("ApplicationId"), Is.EqualTo (output.GetMetadata ("BundleIdentifier")), "ApplicationId"); + Assert.That (output.GetMetadata ("ApplicationName"), Is.Not.Empty, "ApplicationName"); + Assert.That (output.GetMetadata ("ApplicationDisplayVersion"), Is.Not.Empty, "ApplicationDisplayVersion"); + Assert.That (output.GetMetadata ("ApplicationVersion"), Is.Not.Empty, "ApplicationVersion"); Assert.That (output.GetMetadata ("ArtifactKind"), Is.Null.Or.Empty, "ArtifactKind"); Assert.That (output.GetMetadata ("AppBundlePath"), Is.Null.Or.Empty, "AppBundlePath"); Assert.That (output.GetMetadata ("CodeSigned"), Is.Null.Or.Empty, "CodeSigned"); @@ -587,6 +642,10 @@ static JsonElement AssertApplicationArtifact (JsonElement [] outputs, string pat Assert.That (GetMetadata (output, "IsDirectory"), Is.EqualTo (isDirectory ? "true" : "false"), "IsDirectory"); Assert.That (GetMetadata (output, "PlatformName"), Is.EqualTo (platform.AsString ()), "PlatformName"); Assert.That (GetMetadata (output, "BundleIdentifier"), Is.Not.Empty, "BundleIdentifier"); + Assert.That (GetMetadata (output, "ApplicationId"), Is.EqualTo (GetMetadata (output, "BundleIdentifier")), "ApplicationId"); + Assert.That (GetMetadata (output, "ApplicationName"), Is.Not.Empty, "ApplicationName"); + Assert.That (GetMetadata (output, "ApplicationDisplayVersion"), Is.Not.Empty, "ApplicationDisplayVersion"); + Assert.That (GetMetadata (output, "ApplicationVersion"), Is.Not.Empty, "ApplicationVersion"); Assert.That (GetMetadata (output, "ArtifactKind"), Is.Empty, "ArtifactKind"); Assert.That (GetMetadata (output, "AppBundlePath"), Is.Empty, "AppBundlePath"); Assert.That (GetMetadata (output, "CodeSigned"), Is.Empty, "CodeSigned"); @@ -605,6 +664,17 @@ static JsonElement AssertApplicationArtifact (JsonElement [] outputs, ApplePlatf return AssertApplicationArtifact (outputs, fullPath, platform, packageFormat, isDirectory); } + static void AssertApplicationMetadata (JsonElement output, string applicationId, string applicationTitle, string applicationName, string applicationDisplayVersion, string applicationVersion) + { + Assert.Multiple (() => { + Assert.That (GetMetadata (output, "ApplicationId"), Is.EqualTo (applicationId), "ApplicationId"); + Assert.That (GetMetadata (output, "ApplicationTitle"), Is.EqualTo (applicationTitle), "ApplicationTitle"); + Assert.That (GetMetadata (output, "ApplicationName"), Is.EqualTo (applicationName), "ApplicationName"); + Assert.That (GetMetadata (output, "ApplicationDisplayVersion"), Is.EqualTo (applicationDisplayVersion), "ApplicationDisplayVersion"); + Assert.That (GetMetadata (output, "ApplicationVersion"), Is.EqualTo (applicationVersion), "ApplicationVersion"); + }); + } + static string GetMetadata (JsonElement item, string name) { return item.TryGetProperty (name, out var value) ? value.GetString () ?? "" : ""; diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ReadAppManifestTaskTests.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ReadAppManifestTaskTests.cs index 813da25c467e..792962e13bf0 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ReadAppManifestTaskTests.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ReadAppManifestTaskTests.cs @@ -28,6 +28,26 @@ ReadAppManifest CreateTask (ApplePlatform platform = ApplePlatform.iOS, Action

{ + plist ["CFBundleDisplayName"] = "$(PRODUCT_NAME)"; + plist ["CFBundleName"] = "Bundle Name"; + plist ["CFBundleIdentifier"] = "com.xamarin.custom"; + plist ["CFBundleShortVersionString"] = "2.3.4"; + plist ["CFBundleVersion"] = "42"; + }); + ExecuteTask (task); + Assert.Multiple (() => { + Assert.That (task.CFBundleDisplayName, Is.EqualTo ("$(PRODUCT_NAME)"), "CFBundleDisplayName"); + Assert.That (task.CFBundleName, Is.EqualTo ("Bundle Name"), "CFBundleName"); + Assert.That (task.CFBundleIdentifier, Is.EqualTo ("com.xamarin.custom"), "CFBundleIdentifier"); + Assert.That (task.CFBundleShortVersionString, Is.EqualTo ("2.3.4"), "CFBundleShortVersionString"); + Assert.That (task.CFBundleVersion, Is.EqualTo ("42"), "CFBundleVersion"); + }); + } + [Test] public void MacCatalystVersionConversion () { From 052fd1787b9d48c7ed719a164b925595db25802b Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 15 Jul 2026 14:23:12 -0400 Subject: [PATCH 2/2] Address artifact metadata review feedback Use the existing manifest helpers and standard DependsOn property pattern, avoid expanding the artifact item list for the empty check, and keep common metadata assertions on the supported GetApplicationArtifacts and Publish result surfaces. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c04e140a-a614-44c9-a88a-d4f186b13b9f --- .../Xamarin.MacDev.Tasks/Tasks/ReadAppManifest.cs | 4 ++-- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 14 ++++++++++---- tests/dotnet/UnitTests/PostBuildTest.cs | 4 ---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ReadAppManifest.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ReadAppManifest.cs index 86d6c0f0fb27..fb43ce623f9f 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/ReadAppManifest.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ReadAppManifest.cs @@ -71,9 +71,9 @@ public override bool Execute () CFBundleExecutable = plist.GetCFBundleExecutable (); CFBundleDisplayName = plist?.GetCFBundleDisplayName (); - CFBundleName = plist?.Get (ManifestKeys.CFBundleName)?.Value; + CFBundleName = plist?.GetCFBundleName (); CFBundleIdentifier = plist?.GetCFBundleIdentifier (); - CFBundleShortVersionString = plist?.Get (ManifestKeys.CFBundleShortVersionString)?.Value; + CFBundleShortVersionString = plist?.GetCFBundleShortVersionString (); CFBundleVersion = plist?.GetCFBundleVersion (); CLKComplicationGroup = plist?.Get (ManifestKeys.CLKComplicationGroup)?.Value; diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 5db16176c677..45a4e16980de 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -3542,10 +3542,16 @@ Copyright (C) 2018 Microsoft. All rights reserved. + + + Build; + _AddAppleApplicationArtifactMetadata; + $(GetApplicationArtifactsDependsOn); + + + + Condition="'@(ApplicationArtifact->Count())' != '0' And '$(_AppBundleManifestPath)' != '' And (Exists('$(_AppBundleManifestPath)') Or '$(IsRemoteBuild)' == 'true')"> - + false diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index 0fe1aad56175..ec0a278c2435 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -602,10 +602,6 @@ static ITaskItem AssertApplicationArtifact (string binLogPath, string path, Appl Assert.That (output.GetMetadata ("IsDirectory"), Is.EqualTo (isDirectory ? "true" : "false"), "IsDirectory"); Assert.That (output.GetMetadata ("PlatformName"), Is.EqualTo (platform.AsString ()), "PlatformName"); Assert.That (output.GetMetadata ("BundleIdentifier"), Is.Not.Empty, "BundleIdentifier"); - Assert.That (output.GetMetadata ("ApplicationId"), Is.EqualTo (output.GetMetadata ("BundleIdentifier")), "ApplicationId"); - Assert.That (output.GetMetadata ("ApplicationName"), Is.Not.Empty, "ApplicationName"); - Assert.That (output.GetMetadata ("ApplicationDisplayVersion"), Is.Not.Empty, "ApplicationDisplayVersion"); - Assert.That (output.GetMetadata ("ApplicationVersion"), Is.Not.Empty, "ApplicationVersion"); Assert.That (output.GetMetadata ("ArtifactKind"), Is.Null.Or.Empty, "ArtifactKind"); Assert.That (output.GetMetadata ("AppBundlePath"), Is.Null.Or.Empty, "AppBundlePath"); Assert.That (output.GetMetadata ("CodeSigned"), Is.Null.Or.Empty, "CodeSigned");