From 7463e2e976a0c2c6ae2617f9f1c1a66198168250 Mon Sep 17 00:00:00 2001 From: redth Date: Tue, 16 Jun 2026 16:16:40 -0400 Subject: [PATCH 01/23] Expose MaciOS artifact outputs Add public MSBuild item groups for final MaciOS build and publish artifacts so custom targets and CI can consume app bundles, IPA files, PKG installers, and Xcode archives with metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/building-apps/build-items.md | 50 ++++++++++++ docs/building-apps/build-properties.md | 10 +++ docs/building-apps/build-targets.md | 23 ++++++ .../Xamarin.Shared.Sdk.Publish.targets | 6 ++ msbuild/Xamarin.Shared/Xamarin.Shared.targets | 81 +++++++++++++++++++ .../Xamarin.Shared/Xamarin.iOS.Common.targets | 26 ++++++ tests/dotnet/UnitTests/PostBuildTest.cs | 47 ++++++++++- 7 files changed, 239 insertions(+), 4 deletions(-) diff --git a/docs/building-apps/build-items.md b/docs/building-apps/build-items.md index 2fd81202cf18..9fb4cf1f8d4d 100644 --- a/docs/building-apps/build-items.md +++ b/docs/building-apps/build-items.md @@ -249,6 +249,56 @@ An item group that contains environment variables that will be set when the app > [!NOTE] > This only applies when launching the app from the command line (`dotnet run` or `dotnet build -t:Run`), not when launching from the IDE. +## MaciOSArtifactOutput + +An item group that contains final app artifacts produced by the build. This can include: + +* `.app` app bundles for iOS, tvOS, macOS, and Mac Catalyst apps. +* `.ipa` packages when [BuildIpa](build-properties.md#buildipa) is enabled. +* `.pkg` installer packages when [CreatePackage](build-properties.md#createpackage) is enabled. +* `.xcarchive` directories when [ArchiveOnBuild](build-properties.md#archiveonbuild) is enabled. + +The following metadata is set: + +* `ArtifactKind`: The artifact kind. Possible values are `AppBundle`, `Package`, and `Archive`. +* `PackageFormat`: The artifact format. Possible values are `app`, `ipa`, `pkg`, and `xcarchive`. +* `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`. +* `TargetPlatformIdentifier`: The target platform identifier, such as `ios`, `tvos`, `macos`, or `maccatalyst`. +* `TargetFramework`: The target framework. +* `RuntimeIdentifier`: The runtime identifier when building with `RuntimeIdentifier`. +* `RuntimeIdentifiers`: The runtime identifiers when building with `RuntimeIdentifiers`. +* `Configuration`: The build configuration. +* `RelativePath`: The artifact file or directory name. +* `AppBundlePath`: The app bundle path associated with the artifact. +* `BundleIdentifier`: The resolved app bundle identifier. +* `CodeSigned`: Whether the app bundle was code signed. +* `PackageSigned`: Whether a `.pkg` installer package was signed. This metadata is only set for `.pkg` outputs. +* `BuildIpa`: The effective [BuildIpa](build-properties.md#buildipa) value. +* `CreatePackage`: The effective [CreatePackage](build-properties.md#createpackage) value. +* `ArchiveOnBuild`: The effective [ArchiveOnBuild](build-properties.md#archiveonbuild) value. + +Example: + +```xml + + + +``` + +See also the [GetMaciOSArtifactOutputs](build-targets.md#getmaciosartifactoutputs) target. + +## MaciOSPublishedArtifactOutput + +An item group that contains the `@(MaciOSArtifactOutput)` items produced by the `Publish` target. + +This item group has the same metadata as [MaciOSArtifactOutput](#maciosartifactoutput), plus: + +* `OriginalPath`: The corresponding path from `@(MaciOSArtifactOutput)`. + ## NativeReference An item group that contains any native references that should be linked into diff --git a/docs/building-apps/build-properties.md b/docs/building-apps/build-properties.md index 634396282b90..d656f44e6ee5 100644 --- a/docs/building-apps/build-properties.md +++ b/docs/building-apps/build-properties.md @@ -109,6 +109,8 @@ Only applicable to iOS projects (since only iOS projects can be built remotely f If an Xcode archive should be created at the end of the build. +Created archives are exposed in the [MaciOSArtifactOutput](build-items.md#maciosartifactoutput) item group. + ## BGenEmitDebugInformation Whether the `bgen` tool (the binding generator) should emit debug information or not. @@ -139,6 +141,8 @@ Only applicable to iOS and tvOS projects. See [CreatePackage](#createpackage) for macOS and Mac Catalyst projects. +Created IPA packages are exposed in the [MaciOSArtifactOutput](build-items.md#maciosartifactoutput) item group. + ## BundleCreateDump CoreCLR has a command-line utility called [`createdump`][createdump] to create @@ -350,6 +354,8 @@ Only applicable to macOS and Mac Catalyst projects. See [BuildIpa](#buildipa) for iOS and tvOS projects. +Created PKG packages are exposed in the [MaciOSArtifactOutput](build-items.md#maciosartifactoutput) item group. + ## Device Specifies which mobile device or simulator to target when using `dotnet run --device ` or MSBuild targets that interact with devices (such as `Run`, `Install`, or `Uninstall`). @@ -669,6 +675,8 @@ Specifies the path to the resulting .ipa file when creating an IPA package (see Only applicable to iOS and tvOS projects. +The resulting IPA is exposed in the [MaciOSArtifactOutput](build-items.md#maciosartifactoutput) item group. + ## IsAppExtension If a project is an app extension. @@ -1139,6 +1147,8 @@ Specifies the path to the resulting .pkg file when creating a package (see [Crea Only applicable to macOS and Mac Catalyst apps. +The resulting PKG is exposed in the [MaciOSArtifactOutput](build-items.md#maciosartifactoutput) item group. + ## PlutilPath The full path to the `plutil` command-line tool. diff --git a/docs/building-apps/build-targets.md b/docs/building-apps/build-targets.md index fbf0abff811e..3c0d449b5a7d 100644 --- a/docs/building-apps/build-targets.md +++ b/docs/building-apps/build-targets.md @@ -44,6 +44,29 @@ $ dotnet run --device UDID Added in .NET 11. +## GetMaciOSArtifactOutputs + +Builds the project and returns the `@(MaciOSArtifactOutput)` item group. This +target can be used by custom build scripts to query final `.app`, `.ipa`, +`.pkg`, and `.xcarchive` artifacts without invoking the `Publish` target. + +```shell +$ dotnet build -t:GetMaciOSArtifactOutputs +``` + +See [MaciOSArtifactOutput](build-items.md#maciosartifactoutput) for supported metadata. + +## GetMaciOSPublishedArtifactOutputs + +Publishes the project and returns the `@(MaciOSPublishedArtifactOutput)` item +group. + +```shell +$ dotnet publish -t:GetMaciOSPublishedArtifactOutputs +``` + +See [MaciOSPublishedArtifactOutput](build-items.md#maciospublishedartifactoutput) for supported metadata. + ## Run Builds the source code within a project and all dependencies, and then deploys and runs it diff --git a/dotnet/targets/Xamarin.Shared.Sdk.Publish.targets b/dotnet/targets/Xamarin.Shared.Sdk.Publish.targets index c053793baf15..3d585f8546f3 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.Publish.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.Publish.targets @@ -21,7 +21,13 @@ /> + + + %(MaciOSArtifactOutput.Identity) + + + diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 8307a85ebf48..089afaad3a2e 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2369,6 +2369,32 @@ Copyright (C) 2018 Microsoft. All rights reserved. + + + + AppBundle + app + true + $(_PlatformName) + $(TargetPlatformIdentifier) + $(TargetFramework) + $(RuntimeIdentifier) + $(RuntimeIdentifiers) + $(Configuration) + %(Filename)%(Extension) + $(AppBundleDir) + $(_BundleIdentifier) + true + false + $(BuildIpa) + $(CreatePackage) + $(ArchiveOnBuild) + + + + + $(GetApplicationArtifactsDependsOn);AddMauiApplicationArtifactMetadata + + + + + + + + + + + <_MauiObservedAppArtifact Include="@(ApplicationArtifact)" Condition="'%(ApplicationArtifact.PackageFormat)' == 'app'" /> + <_MauiObservedPackageArtifact Include="@(ApplicationArtifact)" Condition="'%(ApplicationArtifact.PackageFormat)' == '$(ExpectedAugmentedPackageFormat)'" /> + + My MAUI App + %(ApplicationArtifact.PackageFormat) + + + + + + diff --git a/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.mk b/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.mk new file mode 100644 index 000000000000..0e8169558074 --- /dev/null +++ b/tests/dotnet/MySimpleAppWithArtifactMetadata/shared.mk @@ -0,0 +1,3 @@ +TOP=../../../.. +TESTNAME=MySimpleAppWithArtifactMetadata +include $(TOP)/tests/common/shared-dotnet.mk diff --git a/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/Makefile b/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/MySimpleAppWithArtifactMetadata.csproj b/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/MySimpleAppWithArtifactMetadata.csproj new file mode 100644 index 000000000000..bd487ddcd88d --- /dev/null +++ b/tests/dotnet/MySimpleAppWithArtifactMetadata/tvOS/MySimpleAppWithArtifactMetadata.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index c28a75d2458e..a5126b5f53be 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Logging.StructuredLogger; @@ -8,22 +7,6 @@ namespace Xamarin.Tests { [TestFixture] public class PostBuildTest : TestBaseClass { - [Test] - public void GetApplicationArtifactsTargetDependenciesTest () - { - var targetsPath = Path.Combine (Configuration.SourceRoot, "msbuild", "Xamarin.Shared", "Xamarin.Shared.targets"); - var document = XDocument.Load (targetsPath); - var root = document.Root; - Assert.That (root, Is.Not.Null, "Project root"); - if (root is null) - throw new InvalidOperationException ("Xamarin.Shared.targets is missing its Project root."); - var msbuildNamespace = root.Name.Namespace; - var targets = document.Descendants (msbuildNamespace + "Target").ToDictionary (v => v.Attribute ("Name")?.Value ?? ""); - - Assert.That (targets, Contains.Key ("GetApplicationArtifacts"), "GetApplicationArtifacts"); - Assert.That (targets ["GetApplicationArtifacts"].Attribute ("DependsOnTargets")?.Value, Is.EqualTo ("Build;$(GetApplicationArtifactsDependsOn)"), "GetApplicationArtifacts dependencies"); - } - [Test] [TestCase (ApplePlatform.iOS, "ios-arm64")] [TestCase (ApplePlatform.TVOS, "tvos-arm64")] @@ -124,50 +107,26 @@ public void GetApplicationArtifactsIpaTest (ApplePlatform platform, string runti [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", "pkg")] public void GetApplicationArtifactsDependsOnTest (ApplePlatform platform, string runtimeIdentifiers, string packageFormat) { - var project = "MySimpleApp"; + var project = "MySimpleAppWithArtifactMetadata"; var configuration = "Release"; Configuration.IgnoreIfIgnoredPlatform (platform); Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration); Clean (project_path); - var existingProjectContent = File.ReadAllText (project_path); - var newProjectContent = existingProjectContent.Replace ("", @" - - AddMauiApplicationArtifactMetadata - - - - <_MauiObservedAppArtifact Include=""@(ApplicationArtifact)"" Condition=""'%(ApplicationArtifact.PackageFormat)' == 'app'"" /> - <_MauiObservedPackageArtifact Include=""@(ApplicationArtifact)"" Condition=""'%(ApplicationArtifact.PackageFormat)' == '$(ExpectedAugmentedPackageFormat)'"" /> - - My MAUI App - %(ApplicationArtifact.PackageFormat) - - - - - -"); - File.WriteAllText (project_path, newProjectContent); - - try { - var properties = GetDefaultProperties (runtimeIdentifiers); - properties ["BuildIpa"] = packageFormat == "ipa" ? "true" : "false"; - properties ["CreatePackage"] = packageFormat == "pkg" ? "true" : "false"; - properties ["Configuration"] = configuration; - properties ["ExpectedAugmentedPackageFormat"] = packageFormat; - - var outputs = GetApplicationArtifacts (project_path, properties); - var appOutput = AssertApplicationArtifact (outputs, appPath, platform, "app", isDirectory: true); - var packageOutput = AssertApplicationArtifact (outputs, platform, packageFormat, isDirectory: false); - Assert.That (GetMetadata (appOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); - Assert.That (GetMetadata (appOutput, "MauiObservedPackageFormat"), Is.EqualTo ("app"), "MauiObservedPackageFormat"); - Assert.That (GetMetadata (packageOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); - Assert.That (GetMetadata (packageOutput, "MauiObservedPackageFormat"), Is.EqualTo (packageFormat), "MauiObservedPackageFormat"); - } finally { - File.WriteAllText (project_path, existingProjectContent); - } + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["BuildIpa"] = packageFormat == "ipa" ? "true" : "false"; + properties ["CreatePackage"] = packageFormat == "pkg" ? "true" : "false"; + properties ["Configuration"] = configuration; + properties ["ExpectedAugmentedPackageFormat"] = packageFormat; + + var outputs = GetApplicationArtifacts (project_path, properties); + var appOutput = AssertApplicationArtifact (outputs, appPath, platform, "app", isDirectory: true); + var packageOutput = AssertApplicationArtifact (outputs, platform, packageFormat, isDirectory: false); + Assert.That (GetMetadata (appOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); + Assert.That (GetMetadata (appOutput, "MauiObservedPackageFormat"), Is.EqualTo ("app"), "MauiObservedPackageFormat"); + Assert.That (GetMetadata (packageOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); + Assert.That (GetMetadata (packageOutput, "MauiObservedPackageFormat"), Is.EqualTo (packageFormat), "MauiObservedPackageFormat"); } [Test] @@ -332,52 +291,29 @@ public void PublishTest (ApplePlatform platform, string runtimeIdentifiers) [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", "pkg", "PkgPackagePath")] public void PublishApplicationArtifactsDependsOnTest (ApplePlatform platform, string runtimeIdentifiers, string packageFormat, string pathVariable) { - var project = "MySimpleApp"; + var project = "MySimpleAppWithArtifactMetadata"; var configuration = "Release"; Configuration.IgnoreIfIgnoredPlatform (platform); Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); var project_path = GetProjectPath (project, runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration); Clean (project_path); - var existingProjectContent = File.ReadAllText (project_path); - var newProjectContent = existingProjectContent.Replace ("", @" - - AddMauiPublishApplicationArtifactMetadata - - - - <_MauiObservedPublishAppArtifact Include=""@(ApplicationArtifact)"" Condition=""'%(ApplicationArtifact.PackageFormat)' == 'app'"" /> - <_MauiObservedPublishPackageArtifact Include=""@(ApplicationArtifact)"" Condition=""'%(ApplicationArtifact.PackageFormat)' == '$(ExpectedAugmentedPackageFormat)'"" /> - - My Published MAUI App - %(ApplicationArtifact.PackageFormat) - - - - - -"); - File.WriteAllText (project_path, newProjectContent); - - try { - var tmpdir = Cache.CreateTemporaryDirectory (); - var pkgPath = Path.Combine (tmpdir, $"MyPackage.{packageFormat}"); - - var properties = GetDefaultProperties (runtimeIdentifiers); - properties [pathVariable] = pkgPath; - properties ["Configuration"] = configuration; - properties ["ExpectedAugmentedPackageFormat"] = packageFormat; - - var outputs = GetApplicationArtifacts (project_path, properties, target: "Publish"); - var appOutput = AssertApplicationArtifact (outputs, appPath, platform, "app", isDirectory: true); - var packageOutput = AssertApplicationArtifact (outputs, pkgPath, platform, packageFormat, isDirectory: false); - Assert.That (GetMetadata (appOutput, "ApplicationTitle"), Is.EqualTo ("My Published MAUI App"), "ApplicationTitle"); - Assert.That (GetMetadata (appOutput, "MauiPublishObservedPackageFormat"), Is.EqualTo ("app"), "MauiPublishObservedPackageFormat"); - Assert.That (GetMetadata (packageOutput, "ApplicationTitle"), Is.EqualTo ("My Published MAUI App"), "ApplicationTitle"); - Assert.That (GetMetadata (packageOutput, "MauiPublishObservedPackageFormat"), Is.EqualTo (packageFormat), "MauiPublishObservedPackageFormat"); - } finally { - File.WriteAllText (project_path, existingProjectContent); - } + + var tmpdir = Cache.CreateTemporaryDirectory (); + var pkgPath = Path.Combine (tmpdir, $"MyPackage.{packageFormat}"); + + var properties = GetDefaultProperties (runtimeIdentifiers); + properties [pathVariable] = pkgPath; + properties ["Configuration"] = configuration; + properties ["ExpectedAugmentedPackageFormat"] = packageFormat; + + var outputs = GetApplicationArtifacts (project_path, properties, target: "Publish"); + var appOutput = AssertApplicationArtifact (outputs, appPath, platform, "app", isDirectory: true); + var packageOutput = AssertApplicationArtifact (outputs, pkgPath, platform, packageFormat, isDirectory: false); + Assert.That (GetMetadata (appOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); + Assert.That (GetMetadata (appOutput, "MauiObservedPackageFormat"), Is.EqualTo ("app"), "MauiObservedPackageFormat"); + Assert.That (GetMetadata (packageOutput, "ApplicationTitle"), Is.EqualTo ("My MAUI App"), "ApplicationTitle"); + Assert.That (GetMetadata (packageOutput, "MauiObservedPackageFormat"), Is.EqualTo (packageFormat), "MauiObservedPackageFormat"); } From eebb82912dbeefd0ab5adc4fdb683ee1c2fe2424 Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Thu, 18 Jun 2026 15:09:49 -0400 Subject: [PATCH 18/23] [docs] Clarify GetApplicationArtifacts contract and query examples - Document that the returned @(ApplicationArtifact) item group reflects the artifacts the current invocation is configured to build (gated by BuildIpa, CreatePackage, and ArchiveOnBuild), not a scan of artifacts left on disk by a previous build. - Use -getItem:ApplicationArtifact in the query examples instead of -getTargetResult. The artifact item identities can be relative paths (for example the .app bundle), and -getItem resolves the well-known FullPath metadata against the project directory, whereas -getTargetResult resolves it against the current working directory, which can produce an incorrect path. -getItem also matches the form used by the unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/building-apps/build-targets.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/building-apps/build-targets.md b/docs/building-apps/build-targets.md index 4e530c4d55cc..82ebe9677f5d 100644 --- a/docs/building-apps/build-targets.md +++ b/docs/building-apps/build-targets.md @@ -52,9 +52,20 @@ target always runs `Build` first so platform `.app`, `.ipa`, `.pkg`, and extension targets run. The `Publish` target returns the same item group for artifacts it creates. +The returned item group reflects the artifacts the current invocation is +configured to build; it does not scan for `.ipa`, `.pkg`, or `.xcarchive` files +left on disk by a previous build. The `.app` bundle is always collected for an +application project (excluding app extension and watch app projects), while the +`.ipa`, `.pkg`, and `.xcarchive` outputs are only collected when +[BuildIpa](build-properties.md#buildipa), +[CreatePackage](build-properties.md#createpackage), or +[ArchiveOnBuild](build-properties.md#archiveonbuild) (respectively) is enabled in +the same invocation. The `Publish` target enables `BuildIpa` (mobile) or +`CreatePackage` (desktop) by default. + ```shell -$ dotnet build MyApp.csproj -t:GetApplicationArtifacts -getTargetResult:GetApplicationArtifacts -$ dotnet build MyApp.csproj -t:Publish -getTargetResult:Publish +$ dotnet build MyApp.csproj -t:GetApplicationArtifacts -getItem:ApplicationArtifact +$ dotnet build MyApp.csproj -t:Publish -getItem:ApplicationArtifact ``` See [ApplicationArtifact](build-items.md#applicationartifact) for supported metadata. From 1c8dc7dbe6377fada2577f743e28af0f3ca488cd Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Thu, 18 Jun 2026 17:33:50 -0400 Subject: [PATCH 19/23] [msbuild] Make @(ApplicationArtifact) paths absolute and fix artifact tests The PostBuildTest application artifact tests (ArchiveTest, BuildIpaTest, BuildPackageTest, PublishTest) crashed on every platform with: System.ArgumentNullException : Value cannot be null. (Parameter 'path') at System.IO.Path.GetFullPath(String path) at PostBuildTest.b__0(ITaskItem v) Two underlying causes: - AssertApplicationArtifact reads the @(ApplicationArtifact) items from the build's binlog and matched on the well-known 'FullPath' metadata. Items read from a binlog (StructuredLogger TaskItemData) only carry the metadata that was explicitly set on the item, so 'FullPath' (and any other unset metadata) comes back as null rather than an empty string, and Path.GetFullPath(null) throws. - The @(ApplicationArtifact) item identity was a relative path for .app bundles and the default .ipa, but an absolute path for .pkg and .xcarchive. A relative identity isn't a usable artifact path for consumers, and its well-known FullPath depends on the current directory at query time. Make the four artifact collectors emit an absolute, project-rooted identity with [MSBuild]::NormalizePath, so @(ApplicationArtifact) is consistently absolute (a no-op for the already-absolute .pkg/.xcarchive paths). Update the binlog-based test assertion to match on ItemSpec and to tolerate null for the metadata that is intentionally never set, and document that the identity is an absolute path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/building-apps/build-items.md | 2 +- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 6 +++--- msbuild/Xamarin.Shared/Xamarin.iOS.Common.targets | 2 +- tests/dotnet/UnitTests/PostBuildTest.cs | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/building-apps/build-items.md b/docs/building-apps/build-items.md index 5b8348255681..84329b453a04 100644 --- a/docs/building-apps/build-items.md +++ b/docs/building-apps/build-items.md @@ -251,7 +251,7 @@ An item group that contains environment variables that will be set when the app ## ApplicationArtifact -An item group that contains final application artifacts produced by Apple platform builds and publishes. The item identity is the path to the artifact. This can include: +An item group that contains final application artifacts produced by Apple platform builds and publishes. The item identity is the absolute path to the artifact. This can include: * `.app` app bundles for iOS, tvOS, macOS, and Mac Catalyst apps. * `.ipa` packages when [BuildIpa](build-properties.md#buildipa) is enabled. diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index ae0fcd8e1752..8f823b3f05f0 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2374,7 +2374,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. Condition="'$(_CanOutputAppBundle)' == 'true' And '$(OutputType)' == 'Exe' And '$(IsAppExtension)' != 'true' And '$(IsWatchApp)' != 'true' And Exists('$(AppBundleDir)')" AfterTargets="Codesign"> - + app true $(_PlatformName) @@ -3274,7 +3274,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. Condition="'$(ArchiveOnBuild)' == 'true' And '$(ArchiveDir)' != '' And Exists('$(ArchiveDir)')" AfterTargets="_CoreArchive"> - + xcarchive true $(_PlatformName) @@ -3390,7 +3390,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. Condition="'$(CreatePackage)' == 'true' And '$(PkgPackagePath)' != '' And Exists('$(PkgPackagePath)')" AfterTargets="_CreateInstaller"> - + pkg false $(_PlatformName) diff --git a/msbuild/Xamarin.Shared/Xamarin.iOS.Common.targets b/msbuild/Xamarin.Shared/Xamarin.iOS.Common.targets index e7a27eb16f14..57cab74b2b01 100644 --- a/msbuild/Xamarin.Shared/Xamarin.iOS.Common.targets +++ b/msbuild/Xamarin.Shared/Xamarin.iOS.Common.targets @@ -492,7 +492,7 @@ Copyright (C) 2013-2016 Xamarin. All rights reserved. Condition="'$(BuildIpa)' == 'true' And '$(IpaPackagePath)' != '' And Exists('$(IpaPackagePath)')" AfterTargets="_ZipIpa"> - + ipa false $(_PlatformName) diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index a5126b5f53be..b00d2f5b5106 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -509,17 +509,17 @@ static ITaskItem AssertApplicationArtifact (string binLogPath, string path, Appl { var outputs = GetItems (binLogPath, "ApplicationArtifact"); var fullPath = Path.GetFullPath (path); - var output = outputs.SingleOrDefault (v => Path.GetFullPath (v.GetMetadata ("FullPath")) == fullPath); - Assert.That (output, Is.Not.Null, $"Could not find {packageFormat} output for {fullPath}. All outputs:\n\t{string.Join ("\n\t", outputs.Select (v => v.GetMetadata ("FullPath")))}"); + var output = outputs.SingleOrDefault (v => Path.GetFullPath (v.ItemSpec) == fullPath); + Assert.That (output, Is.Not.Null, $"Could not find {packageFormat} output for {fullPath}. All outputs:\n\t{string.Join ("\n\t", outputs.Select (v => v.ItemSpec))}"); Assert.That (output!.GetMetadata ("PackageFormat"), Is.EqualTo (packageFormat), "PackageFormat"); 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 ("ArtifactKind"), Is.Empty, "ArtifactKind"); - Assert.That (output.GetMetadata ("AppBundlePath"), Is.Empty, "AppBundlePath"); - Assert.That (output.GetMetadata ("CodeSigned"), Is.Empty, "CodeSigned"); - Assert.That (output.GetMetadata ("Signed"), Is.Empty, "Signed"); - Assert.That (output.GetMetadata ("PackageSigned"), Is.Empty, "PackageSigned"); + 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"); + Assert.That (output.GetMetadata ("Signed"), Is.Null.Or.Empty, "Signed"); + Assert.That (output.GetMetadata ("PackageSigned"), Is.Null.Or.Empty, "PackageSigned"); return output; } From e0d618e4b4baf1da39480bb763205a3980c28cbf Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Fri, 19 Jun 2026 15:48:19 -0400 Subject: [PATCH 20/23] [msbuild] Collect the Windows Pair-to-Mac .xcarchive after copy-back On a Windows (Pair to Mac) build the .xcarchive is produced on the Mac and only copied back to the Windows machine by CopyArchiveFromMac, which is appended as the last entry of ArchiveDependsOn and sets $(ArchivePath) to the local Windows path while $(ArchiveDir) keeps pointing at the Mac path. The @(ApplicationArtifact) archive collector ran AfterTargets _CoreArchive against $(ArchiveDir), i.e. before the copy-back and against the Mac path, so on Windows the .xcarchive was missing because Exists('$(ArchiveDir)') is false locally. Collect after the public Archive wrapper target instead - unchanged on macOS, where it just wraps _CoreArchive, but on Windows it completes after the copy-back - and prefer $(ArchiveDir) when it exists locally (the normal macOS build), falling back to the copied-back $(ArchivePath) otherwise. The .ipa collector is likewise moved from _ZipIpa to the public CreateIpa wrapper for consistency, but that move is a no-op cleanup rather than a fix: the Zip task already copies the .ipa back to the local $(IpaPackagePath) during _ZipIpa (via GetFileAsync), so the IPA was already surfaced in @(ApplicationArtifact) on Windows. The Windows .app (which stays on the Mac unless CopyAppBundle is set) and the IsAppDistribution IPA (copied to $(DistributionPath)) are still not surfaced on Windows; neither is a regression and both are left for a follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 17 +++++++++++++---- .../Xamarin.Shared/Xamarin.iOS.Common.targets | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 8f823b3f05f0..aab1096c88f3 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -3271,10 +3271,19 @@ Copyright (C) 2018 Microsoft. All rights reserved. - - + Condition="'$(ArchiveOnBuild)' == 'true'" + AfterTargets="Archive"> + + + <_ApplicationArtifactArchivePath>$(ArchiveDir) + <_ApplicationArtifactArchivePath Condition="'$(ArchivePath)' != '' And !Exists('$(_ApplicationArtifactArchivePath)')">$(ArchivePath) + + + xcarchive true $(_PlatformName) diff --git a/msbuild/Xamarin.Shared/Xamarin.iOS.Common.targets b/msbuild/Xamarin.Shared/Xamarin.iOS.Common.targets index 57cab74b2b01..d088149ca300 100644 --- a/msbuild/Xamarin.Shared/Xamarin.iOS.Common.targets +++ b/msbuild/Xamarin.Shared/Xamarin.iOS.Common.targets @@ -490,7 +490,7 @@ Copyright (C) 2013-2016 Xamarin. All rights reserved. + AfterTargets="CreateIpa"> ipa From c540b7e1c4e7a8d3a889a181e86501eadfc17b8c Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Fri, 19 Jun 2026 16:33:49 -0400 Subject: [PATCH 21/23] [tests] Add Windows Pair-to-Mac @(ApplicationArtifact) tests Add two RemoteWindows integration tests that validate @(ApplicationArtifact) on a Windows (Pair to Mac) build, where the artifacts are produced on the paired Mac and copied back to the Windows machine: - ArchiveOnRemoteWindowsTest (ArchiveOnBuild=true) is the regression test for the .xcarchive collector move: before that change the archive was collected against the Mac-only $(ArchiveDir) before CopyArchiveFromMac ran, so it was missing on Windows. The Windows archive path is date-based, so it matches on PackageFormat (and asserts the artifact exists) rather than on an exact path. - BuildIpaOnRemoteWindowsTest (BuildIpa=true) is positive coverage that the .ipa is surfaced on Windows. The .ipa is already copied back locally by the Zip task during _ZipIpa, so this passes regardless of the collector move; it locks in that behaviour. Both tests live in PostBuildTest so they can reuse the private artifact assertion helpers, are tagged [Category("RemoteWindows")] so the Windows test leg picks them up, and self-skip on macOS via IgnoreIfNotOnWindows. Add a binlog AssertApplicationArtifact overload that finds the single artifact of a given package format (for the date-based archive path). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/dotnet/UnitTests/PostBuildTest.cs | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index b00d2f5b5106..cf2a01541974 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -78,6 +78,59 @@ public void BuildIpaTest (ApplePlatform platform, string runtimeIdentifiers) AssertDSymDirectory (appPath); } + [Test] + [Category ("RemoteWindows")] + [TestCase (ApplePlatform.iOS, "ios-arm64")] + public void BuildIpaOnRemoteWindowsTest (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "MySimpleApp"; + var configuration = "Release"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + Configuration.IgnoreIfNotOnWindows (); + + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["BuildIpa"] = "true"; + properties ["Configuration"] = configuration; + + var result = DotNet.AssertBuild (project_path, properties, timeout: TimeSpan.FromMinutes (15)); + + // The .ipa is built on the paired Mac and copied back to this Windows machine, so it must be + // surfaced in @(ApplicationArtifact) with the local (Windows) path. The .app bundle stays on the + // Mac, so it's intentionally not surfaced as an artifact on Windows. + var ipaPath = Path.Combine (appPath, "..", $"{project}.ipa"); + Assert.That (ipaPath, Does.Exist, "ipa creation"); + AssertApplicationArtifact (result.BinLogPath, ipaPath, platform, "ipa", isDirectory: false); + } + + [Test] + [Category ("RemoteWindows")] + [TestCase (ApplePlatform.iOS, "ios-arm64")] + public void ArchiveOnRemoteWindowsTest (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "MySimpleApp"; + var configuration = "Release"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + Configuration.IgnoreIfNotOnWindows (); + + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["ArchiveOnBuild"] = "true"; + properties ["Configuration"] = configuration; + + var result = DotNet.AssertBuild (project_path, properties, timeout: TimeSpan.FromMinutes (20)); + + // The .xcarchive is produced on the paired Mac and copied back to this Windows machine by + // CopyArchiveFromMac, which sets $(ArchivePath) to the local (Windows) path while $(ArchiveDir) + // keeps pointing at the Mac path. The Windows archive path is date-based, so match on the + // package format rather than an exact path. + AssertApplicationArtifact (result.BinLogPath, platform, "xcarchive", isDirectory: true); + } + [Test] [TestCase (ApplePlatform.iOS, "ios-arm64")] [TestCase (ApplePlatform.TVOS, "tvos-arm64")] @@ -523,6 +576,17 @@ static ITaskItem AssertApplicationArtifact (string binLogPath, string path, Appl return output; } + static ITaskItem AssertApplicationArtifact (string binLogPath, ApplePlatform platform, string packageFormat, bool isDirectory) + { + var outputs = GetItems (binLogPath, "ApplicationArtifact"); + var matchingOutputs = outputs.Where (v => v.GetMetadata ("PackageFormat") == packageFormat).ToList (); + Assert.That (matchingOutputs, Has.Count.EqualTo (1), $"Expected one {packageFormat} output. All outputs:\n\t{string.Join ("\n\t", outputs.Select (v => v.ItemSpec))}"); + + var itemSpec = matchingOutputs [0].ItemSpec; + Assert.That (itemSpec, Does.Exist, packageFormat); + return AssertApplicationArtifact (binLogPath, itemSpec, platform, packageFormat, isDirectory); + } + static JsonElement [] GetApplicationArtifacts (string projectPath, Dictionary properties, string target = "GetApplicationArtifacts") { using var document = JsonDocument.Parse (DotNet.GetItems (projectPath, "ApplicationArtifact", target: target, properties: properties)); From 9aabadf2772b750351213725d77da7cb1342d2cb Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Fri, 19 Jun 2026 17:53:01 -0400 Subject: [PATCH 22/23] [msbuild] Don't collect the .app artifact on remote Windows builds On a Windows (Pair to Mac) build the .app bundle is produced on the Mac and is never copied back to the local $(AppBundleDir): the managed output lands in the parent $(OutputPath), the bundle and codesign tasks run remotely, and the only app-bundle copy-back (CopyAppBundleFromMac, and only when CopyAppBundle is set) extracts it to a separate AppBundle\ subfolder rather than to $(AppBundleDir). So _CollectAppleAppBundleOutput was already a no-op on Windows because Exists('$(AppBundleDir)') is false there, but that made the skip incidental: a stale local bundle, a custom CreateAppBundleDependsOn hook that writes locally, or a user-provided AppBundleDir could make it surface an incomplete bundle. Add an explicit '$(IsRemoteBuild)' != 'true' guard (empty on macOS so the collector still runs, 'true' on a remote Windows build) plus a comment, so we never surface an incomplete local bundle on Windows. Surfacing the .app on Windows (as we now do for the .ipa and .xcarchive, which are copied back) is left for a follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index aab1096c88f3..7e0e8a7c7370 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2370,8 +2370,13 @@ Copyright (C) 2018 Microsoft. All rights reserved. + From ee63ecc6c82cb94664b9bc50941c9da0d382c84a Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Sat, 20 Jun 2026 00:16:35 -0400 Subject: [PATCH 23/23] [msbuild] Fix app extensions not being embedded after adding @(ApplicationArtifact) Adding the GetApplicationArtifacts target (with Returns="@(ApplicationArtifact)") to Xamarin.Shared.targets regressed app-extension embedding: a plain 'dotnet build' of an app that references an app extension stopped copying the .appex into the container app's PlugIns folder, failing DotNetProjectTest.BuildProjectsWithExtensions on iOS, tvOS and macOS. The cause is an MSBuild behavior: once any target in a .targets file uses the 'Returns' attribute, MSBuild no longer treats the 'Outputs' attribute of that file's other targets as their return value - those targets return an empty set to callers. GetAppExtensionBundlePath and BuildAndGetAppExtensionBundlePath relied on 'Outputs="@(_AppExtensionBundlePath)"' being returned (they are invoked cross-project by _ResolveAppExtensionReferences / _ResolveWatchAppReferences, which capture TargetOutputs into _ResolvedAppExtensionReferences). After GetApplicationArtifacts introduced the first 'Returns' in the file, those two targets returned nothing, so _ResolvedAppExtensionReferences was empty and _CopyAppExtensionsToBundle copied nothing. Fix by giving both targets an explicit Returns equal to their Outputs, restoring their previous return value, and add a comment documenting the pitfall. The @(ApplicationArtifact) feature (GetApplicationArtifacts / Publish) is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 7e0e8a7c7370..5accac63eb06 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2708,9 +2708,14 @@ Copyright (C) 2018 Microsoft. All rights reserved. - - - + + + +