From aa80d158860ba6bbc5561e3ac044218f1e09a314 Mon Sep 17 00:00:00 2001 From: redth Date: Tue, 16 Jun 2026 16:17:15 -0400 Subject: [PATCH 01/19] Expose Android package output items Add metadata-rich AndroidPackageOutput and AndroidPublishedPackageOutput item groups so custom MSBuild targets and CI can discover final APK/AAB outputs without recalculating file names. Wire the publish target to consume the package output items, cover publish/signing scenarios in tests, and document the new item metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs-mobile/building-apps/build-items.md | 51 +++++++++++++ .../building-apps/build-targets.md | 3 + .../Microsoft.Android.Sdk.BuildOrder.targets | 4 + .../Microsoft.Android.Sdk.Publish.targets | 18 ++++- .../PackagingTest.cs | 17 +++++ .../Xamarin.Android.Build.Tests/XASdkTests.cs | 61 +++++++++++++++ .../Xamarin.Android.Common.targets | 75 +++++++++++++++++++ 7 files changed, 226 insertions(+), 3 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-items.md b/Documentation/docs-mobile/building-apps/build-items.md index 46bd157c07b..e9de17a213e 100644 --- a/Documentation/docs-mobile/building-apps/build-items.md +++ b/Documentation/docs-mobile/building-apps/build-items.md @@ -15,6 +15,57 @@ an [MSBuild ItemGroup](/visualstudio/msbuild/itemgroup-element-msbuild). > [!NOTE] > In .NET for Android there is technically no distinction between an application and a bindings project, so build items will work in both. In practice it is highly recommended to create separate application and bindings projects. Build items that are primarily used in bindings projects are documented in the [MSBuild bindings project items](../binding-libs/msbuild-reference/build-items.md) reference guide. +## AndroidPackageOutput + +`@(AndroidPackageOutput)` contains the final Android package files produced +in the build output directory by the package and signing targets. This item +group can be used by custom MSBuild targets to discover APK and Android App +Bundle outputs without recalculating the final file names. + +Each item includes the following metadata: + +- `%(PackageFormat)`: `apk` or `aab`. +- `%(Signed)`: `true` when the package is signed. +- `%(PackageId)`: The resolved Android package name. +- `%(RuntimeIdentifier)`: The current runtime identifier, if any. +- `%(TargetFramework)`: The current target framework. +- `%(Configuration)`: The current configuration. +- `%(AndroidPackageFormat)`: The effective primary package format. +- `%(AndroidPackageFormats)`: The requested package formats. +- `%(IsUniversal)`: `true` for the signed universal APK generated from an Android App Bundle. +- `%(SourcePackageFormat)`: The package format that produced the item. +- `%(RelativePath)`: The output file name. + +For example: + +```xml + + + +``` + +## AndroidPublishedPackageOutput + +`@(AndroidPublishedPackageOutput)` contains the Android package files copied +to the publish directory by `dotnet publish`. It preserves the metadata from +`@(AndroidPackageOutput)` and adds: + +- `%(OriginalPath)`: The corresponding build output artifact before publish copy. + +For example: + +```xml + + + +``` + ## AndroidAdditionalJavaManifest `` is used in conjunction with diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index bd43e2dbc7f..46781e42287 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -153,6 +153,9 @@ Creates and signs the Android package (`.apk`) file. Use with `/p:Configuration=Release` to generate self-contained "Release" packages. +Package files created by this target are available in the +[`@(AndroidPackageOutput)`](build-items.md#androidpackageoutput) item group. + ## StartAndroidActivity Starts the default activity on the device or the running emulator. diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets index fe61bad5a88..5dc5f164af6 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets @@ -51,6 +51,7 @@ properties that determine build ordering. _CopyPackage; _Sign; _CreateUniversalApkFromBundle; + _CollectAndroidPackageOutputs; _PrepareAssemblies; @@ -62,6 +63,7 @@ properties that determine build ordering. <_PackageForAndroidDependsOn> Build; _CopyPackage; + _CollectAndroidPackageOutputs; <_PrepareBuildApkDependsOnTargets> _SetLatestTargetFrameworkVersion; @@ -115,12 +117,14 @@ properties that determine build ordering. _CopyPackage; _Sign; _CreateUniversalApkFromBundle; + _CollectAndroidPackageOutputs; Build; Package; _Sign; _CreateUniversalApkFromBundle; + _CollectAndroidPackageOutputs; $(_MinimalSignAndroidPackageDependsOn); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets index 95dc9575032..11d789c5eb4 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets @@ -12,8 +12,10 @@ This file contains the implementation for 'dotnet publish'. <_PublishDependsOn> Build; PrepareForPublish; + _CollectAndroidPackageOutputs; _CalculateAndroidFilesToPublish; CopyFilesToPublishDirectory; + _CollectAndroidPublishedPackageOutputs; @@ -21,14 +23,24 @@ This file contains the implementation for 'dotnet publish'. - <_AllPackageFormats Include="$(AndroidPackageFormat);$(AndroidPackageFormats)" /> - <_AndroidPackageFormats Include="@(_AllPackageFormats->Distinct())" /> - <_AndroidFilesToPublish Include="$(OutputPath)*.%(_AndroidPackageFormats.Identity)" /> <_AndroidFilesToPublish Include="$(AndroidProguardMappingFile)" Condition="Exists ('$(AndroidProguardMappingFile)')" /> <_AndroidFilesToPublish Include="$(_GenerateResourceDesignerAssemblyOutput)" Condition="Exists('$(_GenerateResourceDesignerAssemblyOutput)')" /> + + + + + + %(AndroidPackageOutput.FullPath) + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index ea19d25ad0f..2673c577ac8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -545,6 +545,20 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ Assert.IsTrue (task.Execute (), "Task should have succeeded."); var proj = new XamarinAndroidApplicationProject () { IsRelease = isRelease, + Imports = { + new Import (() => "PackageOutputs.targets") { + TextContent = () => """ + + + + + +""" + }, + } }; proj.SetRuntime (runtime); proj.SetProperty (proj.ReleaseProperties, "AndroidUseApkSigner", useApkSigner); @@ -573,8 +587,11 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ } //Make sure the APKs are signed + var packageOutputItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "android-package-output-items.txt")); foreach (var apk in Directory.GetFiles (bin, "*-Signed.apk")) { AssertApkIsSigned (apk); + var expectedItem = $"{Path.GetFileName (apk)}|apk|true"; + Assert.IsTrue (packageOutputItems.Contains (expectedItem), $"Expected AndroidPackageOutput item '{expectedItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, packageOutputItems)}"); } // Make sure the APKs have unique version codes diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index c43bc312316..9233340861b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -307,6 +307,24 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot EnableDefaultItems = true, ExtraNuGetConfigSources = { Path.Combine (XABuildPaths.BuildOutputDirectory, "nuget-unsigned"), + }, + Imports = { + new Import (() => "PackageOutputs.targets") { + TextContent = () => """ + + + + + + +""" + }, } }; proj.SetRuntime (runtime); @@ -364,6 +382,49 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot FileAssert.Exists (aab); FileAssert.Exists (aabSigned); } + + var packageOutputItems = ReadAndroidPackageOutputItems (Path.Combine (Root, projBuilder.ProjectDirectory, "android-package-output-items.txt")); + if (!isRelease) { + AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}.apk", "apk", "false", "false", "apk", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}-Signed.apk", "apk", "true", "false", "apk", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}.apk", "apk", "false", "false", "apk", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}-Signed.apk", "apk", "true", "false", "apk", proj.PackageName); + } else { + AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}.aab", "aab", "false", "false", "aab", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}-Signed.aab", "aab", "true", "false", "aab", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}-Signed.apk", "apk", "true", "true", "aab", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}.aab", "aab", "false", "false", "aab", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}-Signed.aab", "aab", "true", "false", "aab", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}-Signed.apk", "apk", "true", "true", "aab", proj.PackageName); + } + } + + static List ReadAndroidPackageOutputItems (string path) + { + FileAssert.Exists (path); + return File.ReadAllLines (path) + .Where (line => line.Length > 0) + .Select (line => line.Split ('|')) + .ToList (); + } + + static void AssertPackageOutputItem (List items, string itemType, string relativePath, string packageFormat, string signed, string isUniversal, string sourcePackageFormat, string packageId) + { + var matches = items.Where (item => + item.Length == 7 && + item [0] == itemType && + item [1] == relativePath && + item [2] == packageFormat && + item [3] == signed && + item [4] == isUniversal && + item [5] == sourcePackageFormat && + item [6] == packageId).ToList (); + Assert.AreEqual (1, matches.Count, $"Expected package output item '{itemType}|{relativePath}|{packageFormat}|{signed}|{isUniversal}|{sourcePackageFormat}|{packageId}'. Actual items:{Environment.NewLine}{FormatPackageOutputItems (items)}"); + } + + static string FormatPackageOutputItems (List items) + { + return string.Join (Environment.NewLine, items.Select (item => string.Join ("|", item))); } [Test] diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index ea3cf49c141..9bebdd3a26e 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2678,6 +2678,81 @@ because xbuild doesn't support framework reference assemblies. + + + + <_AndroidUnsignedApkPackageOutput Remove="@(_AndroidUnsignedApkPackageOutput)" /> + <_AndroidSignedApkPackageOutput Remove="@(_AndroidSignedApkPackageOutput)" /> + <_AndroidUnsignedAabPackageOutput Remove="@(_AndroidUnsignedAabPackageOutput)" /> + <_AndroidSignedAabPackageOutput Remove="@(_AndroidSignedAabPackageOutput)" /> + <_AndroidUnsignedApkPackageOutput + Condition=" '$(AndroidPackageFormat)' != 'aab' And Exists('$(ApkFile)') " + Include="$(ApkFile)"> + apk + false + apk + false + + <_AndroidUnsignedApkPackageOutput + Condition=" '$(AndroidPackageFormat)' != 'aab' And '$(AndroidCreatePackagePerAbi)' == 'true' " + Include="$(OutDir)$(_AndroidPackage)*.apk" + Exclude="$(ApkFile);$(OutDir)$(_AndroidPackage)*-Signed.apk;$(OutDir)$(_AndroidPackage)*-Signed-Unaligned.apk"> + apk + false + apk + false + + <_AndroidSignedApkPackageOutput Condition="Exists('$(ApkFileSigned)')" Include="$(ApkFileSigned)"> + apk + true + aab + apk + true + false + + <_AndroidSignedApkPackageOutput + Condition=" '$(AndroidCreatePackagePerAbi)' == 'true' " + Include="$(OutDir)$(_AndroidPackage)*-Signed.apk" + Exclude="$(ApkFileSigned)"> + apk + true + aab + apk + true + false + + <_AndroidUnsignedAabPackageOutput Condition=" '$(AndroidPackageFormat)' == 'aab' And Exists('$(_AabFile)') " Include="$(_AabFile)"> + aab + false + aab + false + + <_AndroidSignedAabPackageOutput Condition=" '$(AndroidPackageFormat)' == 'aab' And Exists('$(_AabFileSigned)') " Include="$(_AabFileSigned)"> + aab + true + aab + false + + + + + + + + From a311128ab98c1f75e21fab506b510687c9e9a490 Mon Sep 17 00:00:00 2001 From: redth Date: Tue, 16 Jun 2026 16:56:24 -0400 Subject: [PATCH 02/19] Trim Android package output item metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs-mobile/building-apps/build-items.md | 41 ++++--------------- .../Microsoft.Android.Sdk.Publish.targets | 27 +++++++----- .../PackagingTest.cs | 4 +- .../Xamarin.Android.Build.Tests/XASdkTests.cs | 37 +++++++---------- .../Xamarin.Android.Common.targets | 24 +---------- 5 files changed, 43 insertions(+), 90 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-items.md b/Documentation/docs-mobile/building-apps/build-items.md index e9de17a213e..af1a4692bbb 100644 --- a/Documentation/docs-mobile/building-apps/build-items.md +++ b/Documentation/docs-mobile/building-apps/build-items.md @@ -18,50 +18,27 @@ an [MSBuild ItemGroup](/visualstudio/msbuild/itemgroup-element-msbuild). ## AndroidPackageOutput `@(AndroidPackageOutput)` contains the final Android package files produced -in the build output directory by the package and signing targets. This item -group can be used by custom MSBuild targets to discover APK and Android App -Bundle outputs without recalculating the final file names. +by package, signing, and publish targets. This item group can be used by +custom MSBuild targets to discover APK and Android App Bundle outputs without +recalculating the final file names. Each item includes the following metadata: - `%(PackageFormat)`: `apk` or `aab`. - `%(Signed)`: `true` when the package is signed. - `%(PackageId)`: The resolved Android package name. -- `%(RuntimeIdentifier)`: The current runtime identifier, if any. -- `%(TargetFramework)`: The current target framework. -- `%(Configuration)`: The current configuration. -- `%(AndroidPackageFormat)`: The effective primary package format. -- `%(AndroidPackageFormats)`: The requested package formats. -- `%(IsUniversal)`: `true` for the signed universal APK generated from an Android App Bundle. -- `%(SourcePackageFormat)`: The package format that produced the item. -- `%(RelativePath)`: The output file name. -For example: - -```xml - - - -``` - -## AndroidPublishedPackageOutput - -`@(AndroidPublishedPackageOutput)` contains the Android package files copied -to the publish directory by `dotnet publish`. It preserves the metadata from -`@(AndroidPackageOutput)` and adds: - -- `%(OriginalPath)`: The corresponding build output artifact before publish copy. +MSBuild also provides well-known metadata for each item. For example, +`%(Filename)%(Extension)` is the package file name and `%(FullPath)` is the +full package path. For example: ```xml - + ``` diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets index 11d789c5eb4..f618043af34 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets @@ -15,7 +15,7 @@ This file contains the implementation for 'dotnet publish'. _CollectAndroidPackageOutputs; _CalculateAndroidFilesToPublish; CopyFilesToPublishDirectory; - _CollectAndroidPublishedPackageOutputs; + _UpdateAndroidPackageOutputsForPublish; @@ -26,20 +26,27 @@ This file contains the implementation for 'dotnet publish'. <_AndroidFilesToPublish Include="$(AndroidProguardMappingFile)" Condition="Exists ('$(AndroidProguardMappingFile)')" /> <_AndroidFilesToPublish Include="$(_GenerateResourceDesignerAssemblyOutput)" Condition="Exists('$(_GenerateResourceDesignerAssemblyOutput)')" /> - + - + - - - %(AndroidPackageOutput.FullPath) - - + <_AndroidPackageOutputForPublish Remove="@(_AndroidPackageOutputForPublish)" /> + <_AndroidPackageOutputPublishCopy Remove="@(_AndroidPackageOutputPublishCopy)" /> + <_AndroidPackageOutputForPublish Include="@(AndroidPackageOutput)" /> + <_AndroidPackageOutputPublishCopy + Include="@(_AndroidPackageOutputForPublish->'$(PublishDir)%(Filename)%(Extension)')" + Condition="Exists('$(PublishDir)%(_AndroidPackageOutputForPublish.Filename)%(_AndroidPackageOutputForPublish.Extension)')"> + %(_AndroidPackageOutputForPublish.PackageFormat) + %(_AndroidPackageOutputForPublish.Signed) + %(_AndroidPackageOutputForPublish.PackageId) + + + + <_AndroidPackageOutputForPublish Remove="@(_AndroidPackageOutputForPublish)" /> + <_AndroidPackageOutputPublishCopy Remove="@(_AndroidPackageOutputPublishCopy)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 2673c577ac8..9d40921c76e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -552,7 +552,7 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ @@ -590,7 +590,7 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ var packageOutputItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "android-package-output-items.txt")); foreach (var apk in Directory.GetFiles (bin, "*-Signed.apk")) { AssertApkIsSigned (apk); - var expectedItem = $"{Path.GetFileName (apk)}|apk|true"; + var expectedItem = $"{Path.GetFileName (apk)}|apk|true|{proj.PackageName}"; Assert.IsTrue (packageOutputItems.Contains (expectedItem), $"Expected AndroidPackageOutput item '{expectedItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, packageOutputItems)}"); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 9233340861b..83ad8437f15 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -315,12 +315,8 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot - """ @@ -385,17 +381,14 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot var packageOutputItems = ReadAndroidPackageOutputItems (Path.Combine (Root, projBuilder.ProjectDirectory, "android-package-output-items.txt")); if (!isRelease) { - AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}.apk", "apk", "false", "false", "apk", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}-Signed.apk", "apk", "true", "false", "apk", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}.apk", "apk", "false", "false", "apk", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}-Signed.apk", "apk", "true", "false", "apk", proj.PackageName); + Assert.AreEqual (2, packageOutputItems.Count, $"Actual items:{Environment.NewLine}{FormatPackageOutputItems (packageOutputItems)}"); + AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); } else { - AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}.aab", "aab", "false", "false", "aab", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}-Signed.aab", "aab", "true", "false", "aab", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, "package", $"{proj.PackageName}-Signed.apk", "apk", "true", "true", "aab", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}.aab", "aab", "false", "false", "aab", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}-Signed.aab", "aab", "true", "false", "aab", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, "published", $"{proj.PackageName}-Signed.apk", "apk", "true", "true", "aab", proj.PackageName); + Assert.AreEqual (3, packageOutputItems.Count, $"Actual items:{Environment.NewLine}{FormatPackageOutputItems (packageOutputItems)}"); + AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName); + AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); } } @@ -408,18 +401,16 @@ static List ReadAndroidPackageOutputItems (string path) .ToList (); } - static void AssertPackageOutputItem (List items, string itemType, string relativePath, string packageFormat, string signed, string isUniversal, string sourcePackageFormat, string packageId) + static void AssertPackageOutputItem (List items, string fullPath, string fileName, string packageFormat, string signed, string packageId) { var matches = items.Where (item => - item.Length == 7 && - item [0] == itemType && - item [1] == relativePath && + item.Length == 5 && + item [0] == fullPath && + item [1] == fileName && item [2] == packageFormat && item [3] == signed && - item [4] == isUniversal && - item [5] == sourcePackageFormat && - item [6] == packageId).ToList (); - Assert.AreEqual (1, matches.Count, $"Expected package output item '{itemType}|{relativePath}|{packageFormat}|{signed}|{isUniversal}|{sourcePackageFormat}|{packageId}'. Actual items:{Environment.NewLine}{FormatPackageOutputItems (items)}"); + item [4] == packageId).ToList (); + Assert.AreEqual (1, matches.Count, $"Expected package output item '{fullPath}|{fileName}|{packageFormat}|{signed}|{packageId}'. Actual items:{Environment.NewLine}{FormatPackageOutputItems (items)}"); } static string FormatPackageOutputItems (List items) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 9bebdd3a26e..0def9e0b72d 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2692,8 +2692,6 @@ because xbuild doesn't support framework reference assemblies. Include="$(ApkFile)"> apk false - apk - false <_AndroidUnsignedApkPackageOutput Condition=" '$(AndroidPackageFormat)' != 'aab' And '$(AndroidCreatePackagePerAbi)' == 'true' " @@ -2701,16 +2699,10 @@ because xbuild doesn't support framework reference assemblies. Exclude="$(ApkFile);$(OutDir)$(_AndroidPackage)*-Signed.apk;$(OutDir)$(_AndroidPackage)*-Signed-Unaligned.apk"> apk false - apk - false <_AndroidSignedApkPackageOutput Condition="Exists('$(ApkFileSigned)')" Include="$(ApkFileSigned)"> apk true - aab - apk - true - false <_AndroidSignedApkPackageOutput Condition=" '$(AndroidCreatePackagePerAbi)' == 'true' " @@ -2718,33 +2710,19 @@ because xbuild doesn't support framework reference assemblies. Exclude="$(ApkFileSigned)"> apk true - aab - apk - true - false <_AndroidUnsignedAabPackageOutput Condition=" '$(AndroidPackageFormat)' == 'aab' And Exists('$(_AabFile)') " Include="$(_AabFile)"> aab false - aab - false <_AndroidSignedAabPackageOutput Condition=" '$(AndroidPackageFormat)' == 'aab' And Exists('$(_AabFileSigned)') " Include="$(_AabFileSigned)"> aab true - aab - false + PackageId="$(_AndroidPackage)" /> From 0ca42b060ba5f65d38dd802a3941bb03b1e7db3a Mon Sep 17 00:00:00 2001 From: redth Date: Tue, 16 Jun 2026 17:07:15 -0400 Subject: [PATCH 03/19] Simplify Android package output collection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Common.targets | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 0def9e0b72d..4191791bd84 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2683,43 +2683,38 @@ because xbuild doesn't support framework reference assemblies. DependsOnTargets="_ValidateAndroidPackageProperties"> - <_AndroidUnsignedApkPackageOutput Remove="@(_AndroidUnsignedApkPackageOutput)" /> - <_AndroidSignedApkPackageOutput Remove="@(_AndroidSignedApkPackageOutput)" /> - <_AndroidUnsignedAabPackageOutput Remove="@(_AndroidUnsignedAabPackageOutput)" /> - <_AndroidSignedAabPackageOutput Remove="@(_AndroidSignedAabPackageOutput)" /> - <_AndroidUnsignedApkPackageOutput + apk false - - <_AndroidUnsignedApkPackageOutput + + apk false - - <_AndroidSignedApkPackageOutput Condition="Exists('$(ApkFileSigned)')" Include="$(ApkFileSigned)"> + + apk true - - <_AndroidSignedApkPackageOutput + + apk true - - <_AndroidUnsignedAabPackageOutput Condition=" '$(AndroidPackageFormat)' == 'aab' And Exists('$(_AabFile)') " Include="$(_AabFile)"> + + aab false - - <_AndroidSignedAabPackageOutput Condition=" '$(AndroidPackageFormat)' == 'aab' And Exists('$(_AabFileSigned)') " Include="$(_AabFileSigned)"> + + aab true - - + From 9a91ce2e41560cc33c1029a3584c947d20022f2f Mon Sep 17 00:00:00 2001 From: redth Date: Tue, 16 Jun 2026 17:14:41 -0400 Subject: [PATCH 04/19] Guard per-ABI package output collection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 4191791bd84..0fc6f1a9e9a 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2701,7 +2701,7 @@ because xbuild doesn't support framework reference assemblies. true apk From 222e91f1cad8584b8a405d02872dd143c94d0eae Mon Sep 17 00:00:00 2001 From: redth Date: Tue, 16 Jun 2026 17:23:05 -0400 Subject: [PATCH 05/19] Add ABI metadata to package outputs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs-mobile/building-apps/build-items.md | 4 +++- .../targets/Microsoft.Android.Sdk.Publish.targets | 1 + .../Xamarin.Android.Build.Tests/PackagingTest.cs | 15 +++++++++++++-- .../Xamarin.Android.Common.targets | 12 ++++++------ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-items.md b/Documentation/docs-mobile/building-apps/build-items.md index af1a4692bbb..8d1dd89b940 100644 --- a/Documentation/docs-mobile/building-apps/build-items.md +++ b/Documentation/docs-mobile/building-apps/build-items.md @@ -27,6 +27,8 @@ Each item includes the following metadata: - `%(PackageFormat)`: `apk` or `aab`. - `%(Signed)`: `true` when the package is signed. - `%(PackageId)`: The resolved Android package name. +- `%(Abi)`: The Android ABI for a per-ABI APK output. This metadata is only + set for per-ABI APKs. MSBuild also provides well-known metadata for each item. For example, `%(Filename)%(Extension)` is the package file name and `%(FullPath)` is the @@ -38,7 +40,7 @@ For example: ``` diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets index f618043af34..68d6d6c320f 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets @@ -42,6 +42,7 @@ This file contains the implementation for 'dotnet publish'. %(_AndroidPackageOutputForPublish.PackageFormat) %(_AndroidPackageOutputForPublish.Signed) %(_AndroidPackageOutputForPublish.PackageId) + %(_AndroidPackageOutputForPublish.Abi) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 9d40921c76e..68f8b5cd268 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -552,7 +552,7 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ @@ -590,7 +590,9 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ var packageOutputItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "android-package-output-items.txt")); foreach (var apk in Directory.GetFiles (bin, "*-Signed.apk")) { AssertApkIsSigned (apk); - var expectedItem = $"{Path.GetFileName (apk)}|apk|true|{proj.PackageName}"; + var fileName = Path.GetFileName (apk); + var abi = GetPackageOutputAbi (proj.PackageName, fileName); + var expectedItem = $"{fileName}|apk|true|{proj.PackageName}|{abi}"; Assert.IsTrue (packageOutputItems.Contains (expectedItem), $"Expected AndroidPackageOutput item '{expectedItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, packageOutputItems)}"); } @@ -627,6 +629,15 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ } } + string GetPackageOutputAbi (string packageName, string fileName) + { + var prefix = $"{packageName}-"; + const string suffix = "-Signed.apk"; + if (fileName == $"{packageName}{suffix}" || !fileName.StartsWith (prefix, StringComparison.Ordinal) || !fileName.EndsWith (suffix, StringComparison.Ordinal)) + return ""; + return fileName.Substring (prefix.Length, fileName.Length - prefix.Length - suffix.Length); + } + int GetVersionCodeFromIntermediateManifest (string manifestFilePath) { var doc = XDocument.Load (manifestFilePath); diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 0fc6f1a9e9a..e2cfdf90828 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2690,22 +2690,22 @@ because xbuild doesn't support framework reference assemblies. false + Condition=" '$(AndroidPackageFormat)' != 'aab' And '$(AndroidCreatePackagePerAbi)' == 'true' And Exists('$(OutDir)$(_AndroidPackage)-%(_BuildTargetAbis.Identity).apk') " + Include="@(_BuildTargetAbis->'$(OutDir)$(_AndroidPackage)-%(Identity).apk')"> apk false + %(_BuildTargetAbis.Identity) apk true + Condition=" '$(AndroidPackageFormat)' != 'aab' And '$(AndroidCreatePackagePerAbi)' == 'true' And Exists('$(OutDir)$(_AndroidPackage)-%(_BuildTargetAbis.Identity)-Signed.apk') " + Include="@(_BuildTargetAbis->'$(OutDir)$(_AndroidPackage)-%(Identity)-Signed.apk')"> apk true + %(_BuildTargetAbis.Identity) aab From d7e18c305d0ab9c6325d947dbe702d0582e07d3c Mon Sep 17 00:00:00 2001 From: redth Date: Tue, 16 Jun 2026 17:26:34 -0400 Subject: [PATCH 06/19] Add publish package item coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/XASdkTests.cs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 83ad8437f15..b8e037ed3bb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -318,6 +318,17 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot Lines="%(AndroidPackageOutput.FullPath)|%(AndroidPackageOutput.Filename)%(AndroidPackageOutput.Extension)|%(AndroidPackageOutput.PackageFormat)|%(AndroidPackageOutput.Signed)|%(AndroidPackageOutput.PackageId)" Overwrite="true" /> + + + <_ResolvedPackagePublishItem + Include="@(ResolvedFileToPublish)" + Condition=" '%(ResolvedFileToPublish.Extension)' == '.apk' Or '%(ResolvedFileToPublish.Extension)' == '.aab' " /> + + + """ }, @@ -362,7 +373,8 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot Assert.IsTrue (dotnet.LastBuildOutput.ContainsText (expectedMonoAndroidRuntimePath), $"Build should be using {expectedMonoAndroidRuntimePath}"); } - var publishDirectory = Path.Combine (Root, projBuilder.ProjectDirectory, proj.OutputPath, runtimeIdentifier, "publish"); + var packageDirectory = Path.Combine (Root, projBuilder.ProjectDirectory, proj.OutputPath, runtimeIdentifier); + var publishDirectory = Path.Combine (packageDirectory, "publish"); var apk = Path.Combine (publishDirectory, $"{proj.PackageName}.apk"); var apkSigned = Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"); // NOTE: the unsigned .apk doesn't exist when $(AndroidPackageFormats) is `aab;apk` @@ -379,6 +391,18 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot FileAssert.Exists (aabSigned); } + var resolvedPackagePublishItems = ReadAndroidPackageOutputItems (Path.Combine (Root, projBuilder.ProjectDirectory, "resolved-package-publish-items.txt")); + if (!isRelease) { + Assert.AreEqual (2, resolvedPackagePublishItems.Count, $"Actual items:{Environment.NewLine}{FormatPackageOutputItems (resolvedPackagePublishItems)}"); + AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk"); + AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk"); + } else { + Assert.AreEqual (3, resolvedPackagePublishItems.Count, $"Actual items:{Environment.NewLine}{FormatPackageOutputItems (resolvedPackagePublishItems)}"); + AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab"); + AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab"); + AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk"); + } + var packageOutputItems = ReadAndroidPackageOutputItems (Path.Combine (Root, projBuilder.ProjectDirectory, "android-package-output-items.txt")); if (!isRelease) { Assert.AreEqual (2, packageOutputItems.Count, $"Actual items:{Environment.NewLine}{FormatPackageOutputItems (packageOutputItems)}"); @@ -413,6 +437,15 @@ static void AssertPackageOutputItem (List items, string fullPath, str Assert.AreEqual (1, matches.Count, $"Expected package output item '{fullPath}|{fileName}|{packageFormat}|{signed}|{packageId}'. Actual items:{Environment.NewLine}{FormatPackageOutputItems (items)}"); } + static void AssertResolvedPackagePublishItem (List items, string fullPath, string relativePath) + { + var matches = items.Where (item => + item.Length == 2 && + item [0] == fullPath && + item [1] == relativePath).ToList (); + Assert.AreEqual (1, matches.Count, $"Expected resolved package publish item '{fullPath}|{relativePath}'. Actual items:{Environment.NewLine}{FormatPackageOutputItems (items)}"); + } + static string FormatPackageOutputItems (List items) { return string.Join (Environment.NewLine, items.Select (item => string.Join ("|", item))); From 9756adcdd90b9281e6aad6743ecb02c912f46362 Mon Sep 17 00:00:00 2001 From: redth Date: Tue, 16 Jun 2026 18:35:20 -0400 Subject: [PATCH 07/19] Rename application artifact item Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs-mobile/building-apps/build-items.md | 14 ++++--- .../building-apps/build-targets.md | 2 +- .../Microsoft.Android.Sdk.BuildOrder.targets | 8 ++-- .../Microsoft.Android.Sdk.Publish.targets | 38 +++++++++--------- .../PackagingTest.cs | 16 ++++---- .../Xamarin.Android.Build.Tests/XASdkTests.cs | 40 +++++++++---------- .../Xamarin.Android.Common.targets | 38 +++++++++--------- 7 files changed, 79 insertions(+), 77 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-items.md b/Documentation/docs-mobile/building-apps/build-items.md index 8d1dd89b940..bde9eec6853 100644 --- a/Documentation/docs-mobile/building-apps/build-items.md +++ b/Documentation/docs-mobile/building-apps/build-items.md @@ -15,12 +15,14 @@ an [MSBuild ItemGroup](/visualstudio/msbuild/itemgroup-element-msbuild). > [!NOTE] > In .NET for Android there is technically no distinction between an application and a bindings project, so build items will work in both. In practice it is highly recommended to create separate application and bindings projects. Build items that are primarily used in bindings projects are documented in the [MSBuild bindings project items](../binding-libs/msbuild-reference/build-items.md) reference guide. -## AndroidPackageOutput +## ApplicationArtifact -`@(AndroidPackageOutput)` contains the final Android package files produced +`@(ApplicationArtifact)` contains the final application artifact files produced by package, signing, and publish targets. This item group can be used by custom MSBuild targets to discover APK and Android App Bundle outputs without -recalculating the final file names. +recalculating the final file names. .NET for Android populates this item group +with Android-specific artifacts, and other .NET mobile platforms can use the +same item name for their final application artifacts. Each item includes the following metadata: @@ -37,10 +39,10 @@ full package path. For example: ```xml - + ``` diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index 46781e42287..4554a008240 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -154,7 +154,7 @@ Creates and signs the Android package (`.apk`) file. Use with `/p:Configuration=Release` to generate self-contained "Release" packages. Package files created by this target are available in the -[`@(AndroidPackageOutput)`](build-items.md#androidpackageoutput) item group. +[`@(ApplicationArtifact)`](build-items.md#applicationartifact) item group. ## StartAndroidActivity diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets index 5dc5f164af6..bb9999a24f1 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets @@ -51,7 +51,7 @@ properties that determine build ordering. _CopyPackage; _Sign; _CreateUniversalApkFromBundle; - _CollectAndroidPackageOutputs; + _CollectApplicationArtifacts; _PrepareAssemblies; @@ -63,7 +63,7 @@ properties that determine build ordering. <_PackageForAndroidDependsOn> Build; _CopyPackage; - _CollectAndroidPackageOutputs; + _CollectApplicationArtifacts; <_PrepareBuildApkDependsOnTargets> _SetLatestTargetFrameworkVersion; @@ -117,14 +117,14 @@ properties that determine build ordering. _CopyPackage; _Sign; _CreateUniversalApkFromBundle; - _CollectAndroidPackageOutputs; + _CollectApplicationArtifacts; Build; Package; _Sign; _CreateUniversalApkFromBundle; - _CollectAndroidPackageOutputs; + _CollectApplicationArtifacts; $(_MinimalSignAndroidPackageDependsOn); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets index 68d6d6c320f..4066401672e 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets @@ -12,10 +12,10 @@ This file contains the implementation for 'dotnet publish'. <_PublishDependsOn> Build; PrepareForPublish; - _CollectAndroidPackageOutputs; + _CollectApplicationArtifacts; _CalculateAndroidFilesToPublish; CopyFilesToPublishDirectory; - _UpdateAndroidPackageOutputsForPublish; + _UpdateApplicationArtifactsForPublish; @@ -26,28 +26,28 @@ This file contains the implementation for 'dotnet publish'. <_AndroidFilesToPublish Include="$(AndroidProguardMappingFile)" Condition="Exists ('$(AndroidProguardMappingFile)')" /> <_AndroidFilesToPublish Include="$(_GenerateResourceDesignerAssemblyOutput)" Condition="Exists('$(_GenerateResourceDesignerAssemblyOutput)')" /> - + - + - <_AndroidPackageOutputForPublish Remove="@(_AndroidPackageOutputForPublish)" /> - <_AndroidPackageOutputPublishCopy Remove="@(_AndroidPackageOutputPublishCopy)" /> - <_AndroidPackageOutputForPublish Include="@(AndroidPackageOutput)" /> - <_AndroidPackageOutputPublishCopy - Include="@(_AndroidPackageOutputForPublish->'$(PublishDir)%(Filename)%(Extension)')" - Condition="Exists('$(PublishDir)%(_AndroidPackageOutputForPublish.Filename)%(_AndroidPackageOutputForPublish.Extension)')"> - %(_AndroidPackageOutputForPublish.PackageFormat) - %(_AndroidPackageOutputForPublish.Signed) - %(_AndroidPackageOutputForPublish.PackageId) - %(_AndroidPackageOutputForPublish.Abi) - - - - <_AndroidPackageOutputForPublish Remove="@(_AndroidPackageOutputForPublish)" /> - <_AndroidPackageOutputPublishCopy Remove="@(_AndroidPackageOutputPublishCopy)" /> + <_ApplicationArtifactForPublish Remove="@(_ApplicationArtifactForPublish)" /> + <_ApplicationArtifactPublishCopy Remove="@(_ApplicationArtifactPublishCopy)" /> + <_ApplicationArtifactForPublish Include="@(ApplicationArtifact)" /> + <_ApplicationArtifactPublishCopy + Include="@(_ApplicationArtifactForPublish->'$(PublishDir)%(Filename)%(Extension)')" + Condition="Exists('$(PublishDir)%(_ApplicationArtifactForPublish.Filename)%(_ApplicationArtifactForPublish.Extension)')"> + %(_ApplicationArtifactForPublish.PackageFormat) + %(_ApplicationArtifactForPublish.Signed) + %(_ApplicationArtifactForPublish.PackageId) + %(_ApplicationArtifactForPublish.Abi) + + + + <_ApplicationArtifactForPublish Remove="@(_ApplicationArtifactForPublish)" /> + <_ApplicationArtifactPublishCopy Remove="@(_ApplicationArtifactPublishCopy)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 68f8b5cd268..057dd2c1c36 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -546,13 +546,13 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ var proj = new XamarinAndroidApplicationProject () { IsRelease = isRelease, Imports = { - new Import (() => "PackageOutputs.targets") { + new Import (() => "ApplicationArtifacts.targets") { TextContent = () => """ - + @@ -587,13 +587,13 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ } //Make sure the APKs are signed - var packageOutputItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "android-package-output-items.txt")); + var applicationArtifactItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "application-artifact-items.txt")); foreach (var apk in Directory.GetFiles (bin, "*-Signed.apk")) { AssertApkIsSigned (apk); var fileName = Path.GetFileName (apk); - var abi = GetPackageOutputAbi (proj.PackageName, fileName); + var abi = GetApplicationArtifactAbi (proj.PackageName, fileName); var expectedItem = $"{fileName}|apk|true|{proj.PackageName}|{abi}"; - Assert.IsTrue (packageOutputItems.Contains (expectedItem), $"Expected AndroidPackageOutput item '{expectedItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, packageOutputItems)}"); + Assert.IsTrue (applicationArtifactItems.Contains (expectedItem), $"Expected ApplicationArtifact item '{expectedItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, applicationArtifactItems)}"); } // Make sure the APKs have unique version codes @@ -629,7 +629,7 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ } } - string GetPackageOutputAbi (string packageName, string fileName) + string GetApplicationArtifactAbi (string packageName, string fileName) { var prefix = $"{packageName}-"; const string suffix = "-Signed.apk"; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index b8e037ed3bb..ef8d8f9c383 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -309,13 +309,13 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot Path.Combine (XABuildPaths.BuildOutputDirectory, "nuget-unsigned"), }, Imports = { - new Import (() => "PackageOutputs.targets") { + new Import (() => "ApplicationArtifacts.targets") { TextContent = () => """ - + @@ -391,32 +391,32 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot FileAssert.Exists (aabSigned); } - var resolvedPackagePublishItems = ReadAndroidPackageOutputItems (Path.Combine (Root, projBuilder.ProjectDirectory, "resolved-package-publish-items.txt")); + var resolvedPackagePublishItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "resolved-package-publish-items.txt")); if (!isRelease) { - Assert.AreEqual (2, resolvedPackagePublishItems.Count, $"Actual items:{Environment.NewLine}{FormatPackageOutputItems (resolvedPackagePublishItems)}"); + Assert.AreEqual (2, resolvedPackagePublishItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (resolvedPackagePublishItems)}"); AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk"); AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk"); } else { - Assert.AreEqual (3, resolvedPackagePublishItems.Count, $"Actual items:{Environment.NewLine}{FormatPackageOutputItems (resolvedPackagePublishItems)}"); + Assert.AreEqual (3, resolvedPackagePublishItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (resolvedPackagePublishItems)}"); AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab"); AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab"); AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk"); } - var packageOutputItems = ReadAndroidPackageOutputItems (Path.Combine (Root, projBuilder.ProjectDirectory, "android-package-output-items.txt")); + var applicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "application-artifact-items.txt")); if (!isRelease) { - Assert.AreEqual (2, packageOutputItems.Count, $"Actual items:{Environment.NewLine}{FormatPackageOutputItems (packageOutputItems)}"); - AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + Assert.AreEqual (2, applicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (applicationArtifactItems)}"); + AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); + AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); } else { - Assert.AreEqual (3, packageOutputItems.Count, $"Actual items:{Environment.NewLine}{FormatPackageOutputItems (packageOutputItems)}"); - AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName); - AssertPackageOutputItem (packageOutputItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + Assert.AreEqual (3, applicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (applicationArtifactItems)}"); + AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName); + AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName); + AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); } } - static List ReadAndroidPackageOutputItems (string path) + static List ReadApplicationArtifactItems (string path) { FileAssert.Exists (path); return File.ReadAllLines (path) @@ -425,7 +425,7 @@ static List ReadAndroidPackageOutputItems (string path) .ToList (); } - static void AssertPackageOutputItem (List items, string fullPath, string fileName, string packageFormat, string signed, string packageId) + static void AssertApplicationArtifactItem (List items, string fullPath, string fileName, string packageFormat, string signed, string packageId) { var matches = items.Where (item => item.Length == 5 && @@ -434,7 +434,7 @@ static void AssertPackageOutputItem (List items, string fullPath, str item [2] == packageFormat && item [3] == signed && item [4] == packageId).ToList (); - Assert.AreEqual (1, matches.Count, $"Expected package output item '{fullPath}|{fileName}|{packageFormat}|{signed}|{packageId}'. Actual items:{Environment.NewLine}{FormatPackageOutputItems (items)}"); + Assert.AreEqual (1, matches.Count, $"Expected application artifact item '{fullPath}|{fileName}|{packageFormat}|{signed}|{packageId}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (items)}"); } static void AssertResolvedPackagePublishItem (List items, string fullPath, string relativePath) @@ -443,10 +443,10 @@ static void AssertResolvedPackagePublishItem (List items, string full item.Length == 2 && item [0] == fullPath && item [1] == relativePath).ToList (); - Assert.AreEqual (1, matches.Count, $"Expected resolved package publish item '{fullPath}|{relativePath}'. Actual items:{Environment.NewLine}{FormatPackageOutputItems (items)}"); + Assert.AreEqual (1, matches.Count, $"Expected resolved package publish item '{fullPath}|{relativePath}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (items)}"); } - static string FormatPackageOutputItems (List items) + static string FormatApplicationArtifactItems (List items) { return string.Join (Environment.NewLine, items.Select (item => string.Join ("|", item))); } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index e2cfdf90828..52f1b8d3d71 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2678,53 +2678,53 @@ because xbuild doesn't support framework reference assemblies. - - - + apk false - - + apk false %(_BuildTargetAbis.Identity) - - + + apk true - - + apk true %(_BuildTargetAbis.Identity) - - + + aab false - - + + aab true - - + - + DependsOnTargets="SignAndroidPackage;_CollectApplicationArtifacts" + Returns="@(ApplicationArtifact)" /> From 595668b92d984b3b60863e003ccf3e596729078a Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 09:15:41 -0400 Subject: [PATCH 08/19] Align application artifact query target Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs-mobile/building-apps/build-items.md | 3 + .../building-apps/build-targets.md | 11 +++ .../Microsoft.Android.Sdk.Publish.targets | 4 +- .../Xamarin.Android.Build.Tests/XASdkTests.cs | 68 +++++++++++++++++++ .../Xamarin.Android.Common.targets | 9 ++- 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-items.md b/Documentation/docs-mobile/building-apps/build-items.md index bde9eec6853..7a42770a01a 100644 --- a/Documentation/docs-mobile/building-apps/build-items.md +++ b/Documentation/docs-mobile/building-apps/build-items.md @@ -36,6 +36,9 @@ MSBuild also provides well-known metadata for each item. For example, `%(Filename)%(Extension)` is the package file name and `%(FullPath)` is the full package path. +Use the [`GetApplicationArtifacts`](build-targets.md#getapplicationartifacts) +target when another target needs to query the application artifacts directly. + For example: ```xml diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index 4554a008240..27d2b1bfa4c 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -96,6 +96,17 @@ Creates the `@(AndroidDependency)` item group, which is used by the [`InstallAndroidDependencies`](#installandroiddependencies) target to determine which Android SDK packages to install. +## GetApplicationArtifacts + +Creates and returns the +[`@(ApplicationArtifact)`](build-items.md#applicationartifact) item group, +which contains the APK and Android App Bundle files produced by the build. + +This target depends on `$(GetApplicationArtifactsDependsOn)`, which defaults to +`Build;$(GetApplicationArtifactsDependsOn)`. Later imports can append targets to +`$(GetApplicationArtifactsDependsOn)` to update or enrich `@(ApplicationArtifact)` +metadata before the target returns the items. + ## Install [Creates, signs](#signandroidpackage), and installs the Android package onto diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets index 4066401672e..736cecb9e11 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets @@ -19,7 +19,9 @@ This file contains the implementation for 'dotnet publish'. - + diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index ef8d8f9c383..e155e44bdd9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -312,6 +312,17 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot new Import (() => "ApplicationArtifacts.targets") { TextContent = () => """ + + + $(GetApplicationArtifactsDependsOn); + AddMauiApplicationArtifactMetadata + + + + + + + + + + + + + + + + """ }, @@ -414,6 +443,32 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName); AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); } + + Assert.IsTrue (dotnet.Build (target: "WritePublishReturnedApplicationArtifactItems", parameters: configParam), "`dotnet build -t:WritePublishReturnedApplicationArtifactItems` should succeed"); + var publishReturnedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "publish-returned-application-artifact-items.txt")); + if (!isRelease) { + Assert.AreEqual (2, publishReturnedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (publishReturnedApplicationArtifactItems)}"); + AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); + AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + } else { + Assert.AreEqual (3, publishReturnedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (publishReturnedApplicationArtifactItems)}"); + AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName); + AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName); + AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + } + + Assert.IsTrue (dotnet.Build (target: "GetApplicationArtifacts", parameters: configParam), "`dotnet build -t:GetApplicationArtifacts` should succeed"); + var queriedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "queried-application-artifact-items.txt")); + if (!isRelease) { + Assert.AreEqual (2, queriedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (queriedApplicationArtifactItems)}"); + AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName, "true"); + AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); + } else { + Assert.AreEqual (3, queriedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (queriedApplicationArtifactItems)}"); + AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName, "true"); + AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName, "true"); + AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); + } } static List ReadApplicationArtifactItems (string path) @@ -437,6 +492,19 @@ static void AssertApplicationArtifactItem (List items, string fullPat Assert.AreEqual (1, matches.Count, $"Expected application artifact item '{fullPath}|{fileName}|{packageFormat}|{signed}|{packageId}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (items)}"); } + static void AssertQueriedApplicationArtifactItem (List items, string fullPath, string fileName, string packageFormat, string signed, string packageId, string mauiArtifact) + { + var matches = items.Where (item => + item.Length == 6 && + item [0] == fullPath && + item [1] == fileName && + item [2] == packageFormat && + item [3] == signed && + item [4] == packageId && + item [5] == mauiArtifact).ToList (); + Assert.AreEqual (1, matches.Count, $"Expected queried application artifact item '{fullPath}|{fileName}|{packageFormat}|{signed}|{packageId}|{mauiArtifact}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (items)}"); + } + static void AssertResolvedPackagePublishItem (List items, string fullPath, string relativePath) { var matches = items.Where (item => diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 52f1b8d3d71..856e0cd929a 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2721,9 +2721,16 @@ because xbuild doesn't support framework reference assemblies. + + + Build; + $(GetApplicationArtifactsDependsOn); + + + From 220e089d4f385d1c0a2b156c2511cdaf55a304fb Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 09:46:53 -0400 Subject: [PATCH 09/19] Strengthen application artifact extension tests Prove that GetApplicationArtifactsDependsOn targets observe Android-produced ApplicationArtifact items before adding MAUI-style metadata, including publish outputs and per-ABI APK artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PackagingTest.cs | 46 +++++++++++++++++++ .../Xamarin.Android.Build.Tests/XASdkTests.cs | 41 +++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 057dd2c1c36..fe9ddd01559 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -549,12 +549,48 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ new Import (() => "ApplicationArtifacts.targets") { TextContent = () => """ + + + $(GetApplicationArtifactsDependsOn); + AddMauiApplicationArtifactMetadata + + + + + <_ObservedApplicationArtifact Include="@(ApplicationArtifact)" /> + <_ObservedSignedApplicationArtifact + Include="@(_ObservedApplicationArtifact)" + Condition=" '%(_ObservedApplicationArtifact.PackageFormat)' == 'apk' And '%(_ObservedApplicationArtifact.Signed)' == 'true' " /> + <_ObservedAbiApplicationArtifact + Include="@(_ObservedApplicationArtifact)" + Condition=" '%(_ObservedApplicationArtifact.Abi)' != '' " /> + + + + + + + + + + + + """ }, @@ -595,6 +631,16 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ var expectedItem = $"{fileName}|apk|true|{proj.PackageName}|{abi}"; Assert.IsTrue (applicationArtifactItems.Contains (expectedItem), $"Expected ApplicationArtifact item '{expectedItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, applicationArtifactItems)}"); } + if (perAbiApk) { + Assert.IsTrue (b.RunTarget (proj, "GetApplicationArtifacts", doNotCleanupOnUpdate: true), "`GetApplicationArtifacts` should have succeeded."); + var observedApplicationArtifactItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "observed-application-artifact-items.txt")); + var queriedApplicationArtifactItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "queried-application-artifact-items.txt")); + foreach (var item in applicationArtifactItems) { + Assert.IsTrue (observedApplicationArtifactItems.Contains (item), $"Expected observed ApplicationArtifact item '{item}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, observedApplicationArtifactItems)}"); + var queriedItem = $"{item}|true"; + Assert.IsTrue (queriedApplicationArtifactItems.Contains (queriedItem), $"Expected queried ApplicationArtifact item '{queriedItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, queriedApplicationArtifactItems)}"); + } + } // Make sure the APKs have unique version codes if (perAbiApk) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index e155e44bdd9..fe1d48b6370 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -319,6 +319,35 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot + + <_ObservedApplicationArtifact Include="@(ApplicationArtifact)" /> + <_ObservedUnsignedApkApplicationArtifact + Include="@(_ObservedApplicationArtifact)" + Condition=" '%(_ObservedApplicationArtifact.Filename)%(_ObservedApplicationArtifact.Extension)' == '$(_AndroidPackage).apk' And '%(_ObservedApplicationArtifact.PackageFormat)' == 'apk' And '%(_ObservedApplicationArtifact.Signed)' == 'false' " /> + <_ObservedSignedApkApplicationArtifact + Include="@(_ObservedApplicationArtifact)" + Condition=" '%(_ObservedApplicationArtifact.Filename)%(_ObservedApplicationArtifact.Extension)' == '$(_AndroidPackage)-Signed.apk' And '%(_ObservedApplicationArtifact.PackageFormat)' == 'apk' And '%(_ObservedApplicationArtifact.Signed)' == 'true' " /> + <_ObservedUnsignedAabApplicationArtifact + Include="@(_ObservedApplicationArtifact)" + Condition=" '%(_ObservedApplicationArtifact.Filename)%(_ObservedApplicationArtifact.Extension)' == '$(_AndroidPackage).aab' And '%(_ObservedApplicationArtifact.PackageFormat)' == 'aab' And '%(_ObservedApplicationArtifact.Signed)' == 'false' " /> + <_ObservedSignedAabApplicationArtifact + Include="@(_ObservedApplicationArtifact)" + Condition=" '%(_ObservedApplicationArtifact.Filename)%(_ObservedApplicationArtifact.Extension)' == '$(_AndroidPackage)-Signed.aab' And '%(_ObservedApplicationArtifact.PackageFormat)' == 'aab' And '%(_ObservedApplicationArtifact.Signed)' == 'true' " /> + + + + + + + @@ -458,6 +487,18 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot } Assert.IsTrue (dotnet.Build (target: "GetApplicationArtifacts", parameters: configParam), "`dotnet build -t:GetApplicationArtifacts` should succeed"); + var observedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "observed-application-artifact-items.txt")); + if (!isRelease) { + Assert.AreEqual (2, observedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (observedApplicationArtifactItems)}"); + AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); + AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + } else { + Assert.AreEqual (3, observedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (observedApplicationArtifactItems)}"); + AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName); + AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName); + AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + } + var queriedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "queried-application-artifact-items.txt")); if (!isRelease) { Assert.AreEqual (2, queriedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (queriedApplicationArtifactItems)}"); From 8dd85f4a3c982b6df739ff5984c05e2c9afc3b94 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 10:12:33 -0400 Subject: [PATCH 10/19] Run application artifact extensions during publish Route Android Publish through GetApplicationArtifacts so targets appended through GetApplicationArtifactsDependsOn can observe and augment ApplicationArtifact items before Publish calculates and returns them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs-mobile/building-apps/build-items.md | 2 + .../building-apps/build-targets.md | 2 +- .../Microsoft.Android.Sdk.Publish.targets | 3 +- .../Xamarin.Android.Build.Tests/XASdkTests.cs | 42 +++++++++---------- .../Xamarin.Android.Common.targets | 1 - 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-items.md b/Documentation/docs-mobile/building-apps/build-items.md index 7a42770a01a..c27a359b67b 100644 --- a/Documentation/docs-mobile/building-apps/build-items.md +++ b/Documentation/docs-mobile/building-apps/build-items.md @@ -38,6 +38,8 @@ full package path. Use the [`GetApplicationArtifacts`](build-targets.md#getapplicationartifacts) target when another target needs to query the application artifacts directly. +Targets appended to `$(GetApplicationArtifactsDependsOn)` can enrich the item +metadata before `GetApplicationArtifacts` or `Publish` returns the items. For example: diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index 27d2b1bfa4c..4af825d167b 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -105,7 +105,7 @@ which contains the APK and Android App Bundle files produced by the build. This target depends on `$(GetApplicationArtifactsDependsOn)`, which defaults to `Build;$(GetApplicationArtifactsDependsOn)`. Later imports can append targets to `$(GetApplicationArtifactsDependsOn)` to update or enrich `@(ApplicationArtifact)` -metadata before the target returns the items. +metadata before this target or the `Publish` target returns the items. ## Install diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets index 736cecb9e11..60edce2ca09 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Publish.targets @@ -10,9 +10,8 @@ This file contains the implementation for 'dotnet publish'. <_PublishDependsOn> - Build; + GetApplicationArtifacts; PrepareForPublish; - _CollectApplicationArtifacts; _CalculateAndroidFilesToPublish; CopyFilesToPublishDirectory; _UpdateApplicationArtifactsForPublish; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index fe1d48b6370..9f4624556ef 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -355,7 +355,7 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot @@ -384,7 +384,7 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot @@ -464,29 +464,15 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot var applicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "application-artifact-items.txt")); if (!isRelease) { Assert.AreEqual (2, applicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (applicationArtifactItems)}"); - AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); - AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName, "true"); + AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); } else { Assert.AreEqual (3, applicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (applicationArtifactItems)}"); - AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName); - AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName); - AssertApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); - } - - Assert.IsTrue (dotnet.Build (target: "WritePublishReturnedApplicationArtifactItems", parameters: configParam), "`dotnet build -t:WritePublishReturnedApplicationArtifactItems` should succeed"); - var publishReturnedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "publish-returned-application-artifact-items.txt")); - if (!isRelease) { - Assert.AreEqual (2, publishReturnedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (publishReturnedApplicationArtifactItems)}"); - AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); - AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); - } else { - Assert.AreEqual (3, publishReturnedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (publishReturnedApplicationArtifactItems)}"); - AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName); - AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName); - AssertApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName, "true"); + AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName, "true"); + AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); } - Assert.IsTrue (dotnet.Build (target: "GetApplicationArtifacts", parameters: configParam), "`dotnet build -t:GetApplicationArtifacts` should succeed"); var observedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "observed-application-artifact-items.txt")); if (!isRelease) { Assert.AreEqual (2, observedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (observedApplicationArtifactItems)}"); @@ -499,6 +485,20 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); } + Assert.IsTrue (dotnet.Build (target: "WritePublishReturnedApplicationArtifactItems", parameters: configParam), "`dotnet build -t:WritePublishReturnedApplicationArtifactItems` should succeed"); + var publishReturnedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "publish-returned-application-artifact-items.txt")); + if (!isRelease) { + Assert.AreEqual (2, publishReturnedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (publishReturnedApplicationArtifactItems)}"); + AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName, "true"); + AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); + } else { + Assert.AreEqual (3, publishReturnedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (publishReturnedApplicationArtifactItems)}"); + AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName, "true"); + AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName, "true"); + AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); + } + + Assert.IsTrue (dotnet.Build (target: "GetApplicationArtifacts", parameters: configParam), "`dotnet build -t:GetApplicationArtifacts` should succeed"); var queriedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "queried-application-artifact-items.txt")); if (!isRelease) { Assert.AreEqual (2, queriedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (queriedApplicationArtifactItems)}"); diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 856e0cd929a..4d6c0b66024 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2729,7 +2729,6 @@ because xbuild doesn't support framework reference assemblies. From 4afcab593148879078ac7a02616d30dc6ed1124b Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 10:59:49 -0400 Subject: [PATCH 11/19] Document application artifact query targets Add command-line target-result examples for GetApplicationArtifacts and Publish so callers can discover returned ApplicationArtifact items from the documented target surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../building-apps/build-targets.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index 4af825d167b..9992eb2829b 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -107,6 +107,20 @@ This target depends on `$(GetApplicationArtifactsDependsOn)`, which defaults to `$(GetApplicationArtifactsDependsOn)` to update or enrich `@(ApplicationArtifact)` metadata before this target or the `Publish` target returns the items. +Call this target directly when a CI job or custom tool needs the build output +artifact paths: + +```shell +dotnet msbuild MyApp.csproj -t:GetApplicationArtifacts -getTargetResult:GetApplicationArtifacts +``` + +Use the `Publish` target result when the caller needs the copied publish +outputs in `$(PublishDir)`: + +```shell +dotnet msbuild MyApp.csproj -t:Publish -getTargetResult:Publish +``` + ## Install [Creates, signs](#signandroidpackage), and installs the Android package onto @@ -146,6 +160,24 @@ MSBuild property controls which [Visual Studio SDK Manager repository](/xamarin/android/get-started/installation/android-sdk?tabs=windows#repository-selection) is used for package name and package version detection, and URLs to download. +## Publish + +Builds the application, copies final APK and Android App Bundle files to +`$(PublishDir)`, and returns the +[`@(ApplicationArtifact)`](build-items.md#applicationartifact) item group. +Returned items use the copied publish-directory paths and preserve artifact +metadata such as `%(PackageFormat)`, `%(Signed)`, `%(PackageId)`, and `%(Abi)`. + +Targets appended to `$(GetApplicationArtifactsDependsOn)` run before `Publish` +calculates publish files and before `Publish` returns `@(ApplicationArtifact)`, +so later imports can enrich artifact metadata for publish callers. + +For example, to query the published artifacts: + +```shell +dotnet msbuild MyApp.csproj -t:Publish -getTargetResult:Publish +``` + ## RunWithLogging Runs the application with additional logging enabled. Helpful when reporting or investigating an issue with From 0be30443cb39e732c4bf56b9dfc16c4c957eb277 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 11:49:23 -0400 Subject: [PATCH 12/19] Clarify application artifact enrichment docs State that GetApplicationArtifactsDependsOn extension targets run after platform ApplicationArtifact items are populated so later imports can update existing items with additional metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs-mobile/building-apps/build-items.md | 5 +++-- .../docs-mobile/building-apps/build-targets.md | 17 +++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-items.md b/Documentation/docs-mobile/building-apps/build-items.md index c27a359b67b..d1454f60396 100644 --- a/Documentation/docs-mobile/building-apps/build-items.md +++ b/Documentation/docs-mobile/building-apps/build-items.md @@ -38,8 +38,9 @@ full package path. Use the [`GetApplicationArtifacts`](build-targets.md#getapplicationartifacts) target when another target needs to query the application artifacts directly. -Targets appended to `$(GetApplicationArtifactsDependsOn)` can enrich the item -metadata before `GetApplicationArtifacts` or `Publish` returns the items. +Targets appended to `$(GetApplicationArtifactsDependsOn)` run after .NET for +Android populates this item group, so they can update the existing items with +additional metadata before `GetApplicationArtifacts` or `Publish` returns them. For example: diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index 9992eb2829b..d5fae0dfc29 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -103,9 +103,11 @@ Creates and returns the which contains the APK and Android App Bundle files produced by the build. This target depends on `$(GetApplicationArtifactsDependsOn)`, which defaults to -`Build;$(GetApplicationArtifactsDependsOn)`. Later imports can append targets to -`$(GetApplicationArtifactsDependsOn)` to update or enrich `@(ApplicationArtifact)` -metadata before this target or the `Publish` target returns the items. +`Build;$(GetApplicationArtifactsDependsOn)`. The `Build` target populates +`@(ApplicationArtifact)` with the platform-produced artifacts, then later +imports can append targets to `$(GetApplicationArtifactsDependsOn)` to update +those existing items with additional metadata before this target or the +`Publish` target returns them. Call this target directly when a CI job or custom tool needs the build output artifact paths: @@ -168,9 +170,12 @@ Builds the application, copies final APK and Android App Bundle files to Returned items use the copied publish-directory paths and preserve artifact metadata such as `%(PackageFormat)`, `%(Signed)`, `%(PackageId)`, and `%(Abi)`. -Targets appended to `$(GetApplicationArtifactsDependsOn)` run before `Publish` -calculates publish files and before `Publish` returns `@(ApplicationArtifact)`, -so later imports can enrich artifact metadata for publish callers. +`Publish` first runs `GetApplicationArtifacts`, which builds the project and +populates `@(ApplicationArtifact)` with the platform-produced artifacts. Targets +appended to `$(GetApplicationArtifactsDependsOn)` then run against those +existing items before `Publish` calculates publish files and before `Publish` +returns `@(ApplicationArtifact)`, so later imports can add metadata for publish +callers. For example, to query the published artifacts: From 8accb875188224ce15b42e31e4b15fd8adcf3308 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 12:10:33 -0400 Subject: [PATCH 13/19] Make application artifact build dependency mandatory Keep Build outside the user-extensible GetApplicationArtifactsDependsOn property so overriding the property cannot skip platform ApplicationArtifact population before metadata extension targets run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs-mobile/building-apps/build-targets.md | 12 ++++++------ .../Xamarin.Android.Build.Tests/PackagingTest.cs | 1 - .../Tests/Xamarin.Android.Build.Tests/XASdkTests.cs | 1 - .../Xamarin.Android.Common.targets | 11 +++-------- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index d5fae0dfc29..c687aefe792 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -102,12 +102,12 @@ Creates and returns the [`@(ApplicationArtifact)`](build-items.md#applicationartifact) item group, which contains the APK and Android App Bundle files produced by the build. -This target depends on `$(GetApplicationArtifactsDependsOn)`, which defaults to -`Build;$(GetApplicationArtifactsDependsOn)`. The `Build` target populates -`@(ApplicationArtifact)` with the platform-produced artifacts, then later -imports can append targets to `$(GetApplicationArtifactsDependsOn)` to update -those existing items with additional metadata before this target or the -`Publish` target returns them. +This target always depends on `Build`, which populates +`@(ApplicationArtifact)` with the platform-produced artifacts. Later imports can +set or append targets to `$(GetApplicationArtifactsDependsOn)` to update those +existing items with additional metadata before this target or the `Publish` +target returns them. Replacing `$(GetApplicationArtifactsDependsOn)` does not +remove the required `Build` dependency. Call this target directly when a CI job or custom tool needs the build output artifact paths: diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index fe9ddd01559..b21522b713a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -551,7 +551,6 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ - $(GetApplicationArtifactsDependsOn); AddMauiApplicationArtifactMetadata diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 9f4624556ef..1716f3be863 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -314,7 +314,6 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot - $(GetApplicationArtifactsDependsOn); AddMauiApplicationArtifactMetadata diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 4d6c0b66024..0845413266d 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2721,15 +2721,10 @@ because xbuild doesn't support framework reference assemblies. - - - Build; - $(GetApplicationArtifactsDependsOn); - - - From a39b4467a3b1aa7ec00e1e1b819a4d11e834e1ba Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 13:14:45 -0400 Subject: [PATCH 14/19] Use dotnet build in artifact query docs Prefer dotnet build for ApplicationArtifact target-result examples instead of dotnet msbuild. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/building-apps/build-targets.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index c687aefe792..866c1e9892c 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -113,14 +113,14 @@ Call this target directly when a CI job or custom tool needs the build output artifact paths: ```shell -dotnet msbuild MyApp.csproj -t:GetApplicationArtifacts -getTargetResult:GetApplicationArtifacts +dotnet build MyApp.csproj -t:GetApplicationArtifacts -getTargetResult:GetApplicationArtifacts ``` Use the `Publish` target result when the caller needs the copied publish outputs in `$(PublishDir)`: ```shell -dotnet msbuild MyApp.csproj -t:Publish -getTargetResult:Publish +dotnet build MyApp.csproj -t:Publish -getTargetResult:Publish ``` ## Install @@ -180,7 +180,7 @@ callers. For example, to query the published artifacts: ```shell -dotnet msbuild MyApp.csproj -t:Publish -getTargetResult:Publish +dotnet build MyApp.csproj -t:Publish -getTargetResult:Publish ``` ## RunWithLogging From 62fded75632c5c25932308fcb98847a6aee88307 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 14:28:51 -0400 Subject: [PATCH 15/19] Use transforms for artifact test output Write ApplicationArtifact test output with item transforms so WriteLinesToFile receives the full item list in one invocation instead of batching per item. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tests/Xamarin.Android.Build.Tests/XASdkTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 1716f3be863..7179a3822d9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -354,7 +354,7 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot @@ -371,7 +371,7 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot From f0b786125b523376aae6b30271549ef27e94d343 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 14:31:25 -0400 Subject: [PATCH 16/19] Use transforms for packaging artifact test output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tests/Xamarin.Android.Build.Tests/PackagingTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index b21522b713a..7909f9c105c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -581,13 +581,13 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ From 56bff99a2d822bf2bf98574ae48eb9dcef28ec6e Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 15:59:11 -0400 Subject: [PATCH 17/19] Harden Android application artifacts target Keep artifact creation behind a mandatory internal target that runs Build and then explicitly packages, signs, and collects application artifacts before public extension targets can enrich the items. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../building-apps/build-targets.md | 22 +++--- .../Xamarin.Android.Build.Tests/BuildTest.cs | 71 +++++++++++++++++++ .../PackagingTest.cs | 10 +-- .../Xamarin.ProjectTools/Common/DotNetCLI.cs | 21 +++--- .../Xamarin.Android.Common.targets | 20 +++++- 5 files changed, 118 insertions(+), 26 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index 866c1e9892c..438df8ef5d1 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -102,12 +102,12 @@ Creates and returns the [`@(ApplicationArtifact)`](build-items.md#applicationartifact) item group, which contains the APK and Android App Bundle files produced by the build. -This target always depends on `Build`, which populates -`@(ApplicationArtifact)` with the platform-produced artifacts. Later imports can -set or append targets to `$(GetApplicationArtifactsDependsOn)` to update those -existing items with additional metadata before this target or the `Publish` -target returns them. Replacing `$(GetApplicationArtifactsDependsOn)` does not -remove the required `Build` dependency. +This target always runs the required `Build` step, then packages, signs, and +collects platform-produced artifacts into `@(ApplicationArtifact)`. Later +imports can set or append targets to `$(GetApplicationArtifactsDependsOn)` to +update those existing items with additional metadata before this target or the +`Publish` target returns them. Replacing `$(GetApplicationArtifactsDependsOn)` +does not remove the required build or artifact-production steps. Call this target directly when a CI job or custom tool needs the build output artifact paths: @@ -171,11 +171,11 @@ Returned items use the copied publish-directory paths and preserve artifact metadata such as `%(PackageFormat)`, `%(Signed)`, `%(PackageId)`, and `%(Abi)`. `Publish` first runs `GetApplicationArtifacts`, which builds the project and -populates `@(ApplicationArtifact)` with the platform-produced artifacts. Targets -appended to `$(GetApplicationArtifactsDependsOn)` then run against those -existing items before `Publish` calculates publish files and before `Publish` -returns `@(ApplicationArtifact)`, so later imports can add metadata for publish -callers. +populates `@(ApplicationArtifact)` with the platform-produced artifacts after +package and signing outputs are available. Targets appended to +`$(GetApplicationArtifactsDependsOn)` then run against those existing items +before `Publish` calculates publish files and before `Publish` returns +`@(ApplicationArtifact)`, so later imports can add metadata for publish callers. For example, to query the published artifacts: diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index e78b7a7eaa2..c331685a5c8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; @@ -210,6 +211,76 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease, bool aot, bo } } + [Test] + [TestCase ("GetApplicationArtifacts")] + [TestCase ("Publish")] + public void DotNetBuildReturnsApplicationArtifacts (string target) + { + var proj = new XamarinAndroidApplicationProject { + EnableDefaultItems = true, + }; + using var builder = CreateDllBuilder (); + builder.Save (proj); + + var dotnet = new DotNetCLI (Path.Combine (Root, builder.ProjectDirectory, proj.ProjectFilePath)) { + Verbosity = "minimal", + }; + Assert.IsTrue ( + dotnet.Build (target: target, msbuildArguments: [$"-getTargetResult:{target}"]), + $"`dotnet build -t:{target} -getTargetResult:{target}` should succeed"); + + var items = ReadApplicationArtifactTargetResultItems (dotnet.ProcessLogFile, target); + Assert.AreEqual (2, items.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}"); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + } + + static List> ReadApplicationArtifactTargetResultItems (string processLogFile, string target) + { + var output = File.ReadAllText (processLogFile); + var jsonStart = output.IndexOf ('{'); + var jsonEnd = output.LastIndexOf ('}'); + Assert.GreaterOrEqual (jsonStart, 0, $"Could not find JSON target result in {processLogFile}.{Environment.NewLine}{output}"); + Assert.Greater (jsonEnd, jsonStart, $"Could not find complete JSON target result in {processLogFile}.{Environment.NewLine}{output}"); + + using var document = JsonDocument.Parse (output.Substring (jsonStart, jsonEnd - jsonStart + 1)); + var targetResult = document.RootElement + .GetProperty ("TargetResults") + .GetProperty (target); + Assert.AreEqual ("Success", targetResult.GetProperty ("Result").GetString (), $"Target {target} should succeed."); + + var items = new List> (); + foreach (var item in targetResult.GetProperty ("Items").EnumerateArray ()) { + var metadata = new Dictionary (StringComparer.Ordinal); + foreach (var property in item.EnumerateObject ()) { + metadata.Add (property.Name, property.Value.GetString () ?? ""); + } + items.Add (metadata); + } + return items; + } + + static void AssertApplicationArtifactTargetResultItem (List> items, string fileName, string packageFormat, string signed, string packageId) + { + var matches = items.Where (item => + GetTargetResultMetadata (item, "Filename") + GetTargetResultMetadata (item, "Extension") == fileName && + GetTargetResultMetadata (item, "PackageFormat") == packageFormat && + GetTargetResultMetadata (item, "Signed") == signed && + GetTargetResultMetadata (item, "PackageId") == packageId).ToList (); + Assert.AreEqual (1, matches.Count, $"Expected application artifact item '{fileName}|{packageFormat}|{signed}|{packageId}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}"); + } + + static string GetTargetResultMetadata (Dictionary item, string name) + { + return item.TryGetValue (name, out var value) ? value : ""; + } + + static string FormatApplicationArtifactTargetResultItems (List> items) + { + return string.Join (Environment.NewLine, items.Select (item => + $"{GetTargetResultMetadata (item, "Identity")}|{GetTargetResultMetadata (item, "Filename")}{GetTargetResultMetadata (item, "Extension")}|{GetTargetResultMetadata (item, "PackageFormat")}|{GetTargetResultMetadata (item, "Signed")}|{GetTargetResultMetadata (item, "PackageId")}")); + } + static object [] MonoComponentMaskChecks () => new object [] { new object[] { true, // enableProfiler diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 7909f9c105c..8e2d8c91aa2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -568,7 +568,7 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ Text="Expected ApplicationArtifact items before MAUI metadata augmentation." /> - - + LastBuildOutput { public bool IsTargetSkipped (string target, bool defaultIfNotUsed = false) => BuildOutput.IsTargetSkipped (LastBuildOutput, target, defaultIfNotUsed); - List GetDefaultCommandLineArgs (string verb, string target = null, string runtimeIdentifier = null, string [] parameters = null) + List GetDefaultCommandLineArgs (string verb, string target = null, string runtimeIdentifier = null, string [] parameters = null, string [] msbuildArguments = null) { string testDir = string.IsNullOrEmpty (ProjectDirectory) ? Path.GetDirectoryName (projectOrSolution) : ProjectDirectory; if (string.IsNullOrEmpty (BuildLogFile)) @@ -269,6 +269,9 @@ List GetDefaultCommandLineArgs (string verb, string target = null, strin arguments.Add ($"/p:{parameter}"); } } + if (msbuildArguments != null) { + arguments.AddRange (msbuildArguments); + } if (!string.IsNullOrEmpty (runtimeIdentifier)) { // NOTE: that this one has to be -r, /r does not appear to work arguments.Add ("-r"); diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 0845413266d..a43e70f739e 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2721,9 +2721,27 @@ because xbuild doesn't support framework reference assemblies. + + <_CreateApplicationArtifactsDependsOn Condition=" '$(AndroidApplication)' == 'true' "> + Build; + _CopyPackage; + _Sign; + _CreateUniversalApkFromBundle; + _CollectApplicationArtifacts; + + <_CreateApplicationArtifactsDependsOn Condition=" '$(_CreateApplicationArtifactsDependsOn)' == '' "> + Build + + + + + + From 7af99daf447c9d2c77673d5f8974239daf67fa41 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 17 Jun 2026 16:18:54 -0400 Subject: [PATCH 18/19] Simplify application artifact target graph Keep GetApplicationArtifacts directly dependent on Build before extension targets run, avoiding an extra private artifact-production wrapper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../building-apps/build-targets.md | 22 +++++++++---------- .../Xamarin.Android.Common.targets | 20 +---------------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index 438df8ef5d1..4d342fab9d5 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -102,12 +102,12 @@ Creates and returns the [`@(ApplicationArtifact)`](build-items.md#applicationartifact) item group, which contains the APK and Android App Bundle files produced by the build. -This target always runs the required `Build` step, then packages, signs, and -collects platform-produced artifacts into `@(ApplicationArtifact)`. Later -imports can set or append targets to `$(GetApplicationArtifactsDependsOn)` to -update those existing items with additional metadata before this target or the -`Publish` target returns them. Replacing `$(GetApplicationArtifactsDependsOn)` -does not remove the required build or artifact-production steps. +This target always depends on the required `Build` target, which produces and +collects platform artifacts into `@(ApplicationArtifact)`. Later imports can set +or append targets to `$(GetApplicationArtifactsDependsOn)` to update those +existing items with additional metadata before this target or the `Publish` +target returns them. Replacing `$(GetApplicationArtifactsDependsOn)` does not +remove the required `Build` dependency. Call this target directly when a CI job or custom tool needs the build output artifact paths: @@ -171,11 +171,11 @@ Returned items use the copied publish-directory paths and preserve artifact metadata such as `%(PackageFormat)`, `%(Signed)`, `%(PackageId)`, and `%(Abi)`. `Publish` first runs `GetApplicationArtifacts`, which builds the project and -populates `@(ApplicationArtifact)` with the platform-produced artifacts after -package and signing outputs are available. Targets appended to -`$(GetApplicationArtifactsDependsOn)` then run against those existing items -before `Publish` calculates publish files and before `Publish` returns -`@(ApplicationArtifact)`, so later imports can add metadata for publish callers. +populates `@(ApplicationArtifact)` with the platform-produced artifacts. Targets +appended to `$(GetApplicationArtifactsDependsOn)` then run against those +existing items before `Publish` calculates publish files and before `Publish` +returns `@(ApplicationArtifact)`, so later imports can add metadata for publish +callers. For example, to query the published artifacts: diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index a43e70f739e..0845413266d 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2721,27 +2721,9 @@ because xbuild doesn't support framework reference assemblies. - - <_CreateApplicationArtifactsDependsOn Condition=" '$(AndroidApplication)' == 'true' "> - Build; - _CopyPackage; - _Sign; - _CreateUniversalApkFromBundle; - _CollectApplicationArtifacts; - - <_CreateApplicationArtifactsDependsOn Condition=" '$(_CreateApplicationArtifactsDependsOn)' == '' "> - Build - - - - - - From eef5cc8e16e57854c9bbda8bbe86eca79fb47ea4 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 17 Jun 2026 15:20:12 -0500 Subject: [PATCH 19/19] Consolidate ApplicationArtifact tests into one parameterized test The PackagingTest.CheckSignApk and XASdkTests.DotNetPublish additions injected ~80 lines of inline MSBuild XML plus file-based bookkeeping into existing matrix tests to validate the new `@(ApplicationArtifact)` behavior. That coupled the new feature to unrelated test surfaces and inflated test runtime across the `[Values]` matrix. Instead, parameterize `BuildTest.DotNetBuildReturnsApplicationArtifacts` to cover the same contract from one focused test that uses the JSON emitted by `-getTargetResult:` rather than `WriteLinesToFile`/read-back. Added cases: * `aab` package format - asserts 3 items (unsigned aab, signed aab, signed universal apk from the bundle). * Per-ABI apk with explicit RuntimeIdentifiers - asserts the per-ABI items carry `Abi` metadata. * Extension hook - validates the `_CreateApplicationArtifacts` -> `$(GetApplicationArtifactsDependsOn)` ordering contract by appending a target that runs `Update` to add `MauiArtifact=true`. If the order regresses, `Update` would have nothing to update and the metadata wouldn't appear on the returned items. Reverted all changes to PackagingTest.cs and XASdkTests.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest.cs | 83 +++++++- .../PackagingTest.cs | 75 +------ .../Xamarin.Android.Build.Tests/XASdkTests.cs | 195 +----------------- 3 files changed, 74 insertions(+), 279 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index c331685a5c8..0bdb220971e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -212,27 +212,86 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease, bool aot, bo } [Test] - [TestCase ("GetApplicationArtifacts")] - [TestCase ("Publish")] - public void DotNetBuildReturnsApplicationArtifacts (string target) + // target, isRelease, packageFormat, perAbi, withExtensionHook + [TestCase ("GetApplicationArtifacts", false, "apk", false, false)] + [TestCase ("Publish", false, "apk", false, false)] + [TestCase ("GetApplicationArtifacts", true, "aab", false, false)] + [TestCase ("GetApplicationArtifacts", false, "apk", true, false)] + [TestCase ("GetApplicationArtifacts", false, "apk", false, true)] + public void DotNetBuildReturnsApplicationArtifacts (string target, bool isRelease, string packageFormat, bool perAbi, bool withExtensionHook) { var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, EnableDefaultItems = true, }; + proj.SetProperty ("AndroidPackageFormat", packageFormat); + if (packageFormat == "aab") { + // Disable fast deployment for AABs to avoid XA0119. + proj.EmbedAssembliesIntoApk = true; + } + if (perAbi) { + proj.SetRuntimeIdentifiers (new [] { "arm64-v8a", "x86_64" }); + proj.SetProperty ("AndroidCreatePackagePerAbi", "true"); + } + if (withExtensionHook) { + // Validate that $(GetApplicationArtifactsDependsOn) runs *after* _CreateApplicationArtifacts, + // so MAUI-style extension targets can enrich the items the platform already produced. + // If the order regresses, `Update` will have nothing to update and the metadata won't appear. + proj.Imports.Add (new Import (() => "ApplicationArtifacts.targets") { + TextContent = () => """ + + + $(GetApplicationArtifactsDependsOn);_AddExtensionArtifactMetadata + + + + + + + + +""" + }); + } + using var builder = CreateDllBuilder (); builder.Save (proj); var dotnet = new DotNetCLI (Path.Combine (Root, builder.ProjectDirectory, proj.ProjectFilePath)) { Verbosity = "minimal", }; + var msbuildArgs = new List { $"-getTargetResult:{target}" }; + if (isRelease) { + msbuildArgs.Add ("-c:Release"); + } Assert.IsTrue ( - dotnet.Build (target: target, msbuildArguments: [$"-getTargetResult:{target}"]), + dotnet.Build (target: target, msbuildArguments: msbuildArgs.ToArray ()), $"`dotnet build -t:{target} -getTargetResult:{target}` should succeed"); var items = ReadApplicationArtifactTargetResultItems (dotnet.ProcessLogFile, target); - Assert.AreEqual (2, items.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}"); - AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); - AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); + var expectedMauiArtifact = withExtensionHook ? "true" : ""; + + if (packageFormat == "aab") { + // AAB produces: unsigned aab + signed aab + signed universal APK from the bundle. + Assert.AreEqual (3, items.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}"); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}.aab", "aab", "false", proj.PackageName, "", expectedMauiArtifact); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName, "", expectedMauiArtifact); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "", expectedMauiArtifact); + } else if (perAbi) { + // Per-ABI: main unsigned + main signed + per-ABI unsigned/signed for each requested ABI. + var abis = new [] { "arm64-v8a", "x86_64" }; + Assert.AreEqual (2 + abis.Length * 2, items.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}"); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}.apk", "apk", "false", proj.PackageName, "", expectedMauiArtifact); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "", expectedMauiArtifact); + foreach (var abi in abis) { + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-{abi}.apk", "apk", "false", proj.PackageName, abi, expectedMauiArtifact); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-{abi}-Signed.apk", "apk", "true", proj.PackageName, abi, expectedMauiArtifact); + } + } else { + Assert.AreEqual (2, items.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}"); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}.apk", "apk", "false", proj.PackageName, "", expectedMauiArtifact); + AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "", expectedMauiArtifact); + } } static List> ReadApplicationArtifactTargetResultItems (string processLogFile, string target) @@ -260,14 +319,16 @@ static List> ReadApplicationArtifactTargetResultItems return items; } - static void AssertApplicationArtifactTargetResultItem (List> items, string fileName, string packageFormat, string signed, string packageId) + static void AssertApplicationArtifactTargetResultItem (List> items, string fileName, string packageFormat, string signed, string packageId, string abi, string mauiArtifact) { var matches = items.Where (item => GetTargetResultMetadata (item, "Filename") + GetTargetResultMetadata (item, "Extension") == fileName && GetTargetResultMetadata (item, "PackageFormat") == packageFormat && GetTargetResultMetadata (item, "Signed") == signed && - GetTargetResultMetadata (item, "PackageId") == packageId).ToList (); - Assert.AreEqual (1, matches.Count, $"Expected application artifact item '{fileName}|{packageFormat}|{signed}|{packageId}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}"); + GetTargetResultMetadata (item, "PackageId") == packageId && + GetTargetResultMetadata (item, "Abi") == abi && + GetTargetResultMetadata (item, "MauiArtifact") == mauiArtifact).ToList (); + Assert.AreEqual (1, matches.Count, $"Expected application artifact item '{fileName}|{packageFormat}|{signed}|{packageId}|{abi}|{mauiArtifact}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}"); } static string GetTargetResultMetadata (Dictionary item, string name) @@ -278,7 +339,7 @@ static string GetTargetResultMetadata (Dictionary item, string n static string FormatApplicationArtifactTargetResultItems (List> items) { return string.Join (Environment.NewLine, items.Select (item => - $"{GetTargetResultMetadata (item, "Identity")}|{GetTargetResultMetadata (item, "Filename")}{GetTargetResultMetadata (item, "Extension")}|{GetTargetResultMetadata (item, "PackageFormat")}|{GetTargetResultMetadata (item, "Signed")}|{GetTargetResultMetadata (item, "PackageId")}")); + $"{GetTargetResultMetadata (item, "Identity")}|{GetTargetResultMetadata (item, "Filename")}{GetTargetResultMetadata (item, "Extension")}|{GetTargetResultMetadata (item, "PackageFormat")}|{GetTargetResultMetadata (item, "Signed")}|{GetTargetResultMetadata (item, "PackageId")}|{GetTargetResultMetadata (item, "Abi")}|{GetTargetResultMetadata (item, "MauiArtifact")}")); } static object [] MonoComponentMaskChecks () => new object [] { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 8e2d8c91aa2..f5676ab3d04 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -545,55 +545,6 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ Assert.IsTrue (task.Execute (), "Task should have succeeded."); var proj = new XamarinAndroidApplicationProject () { IsRelease = isRelease, - Imports = { - new Import (() => "ApplicationArtifacts.targets") { - TextContent = () => """ - - - - AddMauiApplicationArtifactMetadata - - - - - <_ObservedApplicationArtifact Include="@(ApplicationArtifact)" /> - <_ObservedSignedApplicationArtifact - Include="@(_ObservedApplicationArtifact)" - Condition=" '%(_ObservedApplicationArtifact.PackageFormat)' == 'apk' And '%(_ObservedApplicationArtifact.Signed)' == 'true' " /> - <_ObservedAbiApplicationArtifact - Include="@(_ObservedApplicationArtifact)" - Condition=" '%(_ObservedApplicationArtifact.Abi)' != '' " /> - - - - - - - - - - - - - - - - -""" - }, - } }; proj.SetRuntime (runtime); proj.SetProperty (proj.ReleaseProperties, "AndroidUseApkSigner", useApkSigner); @@ -618,27 +569,12 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ if (runtime != AndroidRuntime.NativeAOT) { b.AssertHasNoWarnings (); } else { - StringAssertEx.Contains ("2 Warning(s)", b.LastBuildOutput, "NativeAOT should produce two IL3053 warnings"); + b.AssertHasAtMostWarnings (2); } //Make sure the APKs are signed - var applicationArtifactItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "application-artifact-items.txt")); foreach (var apk in Directory.GetFiles (bin, "*-Signed.apk")) { AssertApkIsSigned (apk); - var fileName = Path.GetFileName (apk); - var abi = GetApplicationArtifactAbi (proj.PackageName, fileName); - var expectedItem = $"{fileName}|apk|true|{proj.PackageName}|{abi}"; - Assert.IsTrue (applicationArtifactItems.Contains (expectedItem), $"Expected ApplicationArtifact item '{expectedItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, applicationArtifactItems)}"); - } - if (perAbiApk) { - Assert.IsTrue (b.RunTarget (proj, "GetApplicationArtifacts", doNotCleanupOnUpdate: true), "`GetApplicationArtifacts` should have succeeded."); - var observedApplicationArtifactItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "observed-application-artifact-items.txt")); - var queriedApplicationArtifactItems = File.ReadAllLines (Path.Combine (Root, b.ProjectDirectory, "queried-application-artifact-items.txt")); - foreach (var artifactItem in applicationArtifactItems) { - Assert.IsTrue (observedApplicationArtifactItems.Contains (artifactItem), $"Expected observed ApplicationArtifact item '{artifactItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, observedApplicationArtifactItems)}"); - var queriedItem = $"{artifactItem}|true"; - Assert.IsTrue (queriedApplicationArtifactItems.Contains (queriedItem), $"Expected queried ApplicationArtifact item '{queriedItem}'. Actual items:{Environment.NewLine}{string.Join (Environment.NewLine, queriedApplicationArtifactItems)}"); - } } // Make sure the APKs have unique version codes @@ -674,15 +610,6 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ } } - string GetApplicationArtifactAbi (string packageName, string fileName) - { - var prefix = $"{packageName}-"; - const string suffix = "-Signed.apk"; - if (fileName == $"{packageName}{suffix}" || !fileName.StartsWith (prefix, StringComparison.Ordinal) || !fileName.EndsWith (suffix, StringComparison.Ordinal)) - return ""; - return fileName.Substring (prefix.Length, fileName.Length - prefix.Length - suffix.Length); - } - int GetVersionCodeFromIntermediateManifest (string manifestFilePath) { var doc = XDocument.Load (manifestFilePath); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 7179a3822d9..c43bc312316 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -307,88 +307,6 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot EnableDefaultItems = true, ExtraNuGetConfigSources = { Path.Combine (XABuildPaths.BuildOutputDirectory, "nuget-unsigned"), - }, - Imports = { - new Import (() => "ApplicationArtifacts.targets") { - TextContent = () => """ - - - - AddMauiApplicationArtifactMetadata - - - - - <_ObservedApplicationArtifact Include="@(ApplicationArtifact)" /> - <_ObservedUnsignedApkApplicationArtifact - Include="@(_ObservedApplicationArtifact)" - Condition=" '%(_ObservedApplicationArtifact.Filename)%(_ObservedApplicationArtifact.Extension)' == '$(_AndroidPackage).apk' And '%(_ObservedApplicationArtifact.PackageFormat)' == 'apk' And '%(_ObservedApplicationArtifact.Signed)' == 'false' " /> - <_ObservedSignedApkApplicationArtifact - Include="@(_ObservedApplicationArtifact)" - Condition=" '%(_ObservedApplicationArtifact.Filename)%(_ObservedApplicationArtifact.Extension)' == '$(_AndroidPackage)-Signed.apk' And '%(_ObservedApplicationArtifact.PackageFormat)' == 'apk' And '%(_ObservedApplicationArtifact.Signed)' == 'true' " /> - <_ObservedUnsignedAabApplicationArtifact - Include="@(_ObservedApplicationArtifact)" - Condition=" '%(_ObservedApplicationArtifact.Filename)%(_ObservedApplicationArtifact.Extension)' == '$(_AndroidPackage).aab' And '%(_ObservedApplicationArtifact.PackageFormat)' == 'aab' And '%(_ObservedApplicationArtifact.Signed)' == 'false' " /> - <_ObservedSignedAabApplicationArtifact - Include="@(_ObservedApplicationArtifact)" - Condition=" '%(_ObservedApplicationArtifact.Filename)%(_ObservedApplicationArtifact.Extension)' == '$(_AndroidPackage)-Signed.aab' And '%(_ObservedApplicationArtifact.PackageFormat)' == 'aab' And '%(_ObservedApplicationArtifact.Signed)' == 'true' " /> - - - - - - - - - - - - - - - - - <_ResolvedPackagePublishItem - Include="@(ResolvedFileToPublish)" - Condition=" '%(ResolvedFileToPublish.Extension)' == '.apk' Or '%(ResolvedFileToPublish.Extension)' == '.aab' " /> - - - - - - - - - - - - - -""" - }, } }; proj.SetRuntime (runtime); @@ -430,8 +348,7 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot Assert.IsTrue (dotnet.LastBuildOutput.ContainsText (expectedMonoAndroidRuntimePath), $"Build should be using {expectedMonoAndroidRuntimePath}"); } - var packageDirectory = Path.Combine (Root, projBuilder.ProjectDirectory, proj.OutputPath, runtimeIdentifier); - var publishDirectory = Path.Combine (packageDirectory, "publish"); + var publishDirectory = Path.Combine (Root, projBuilder.ProjectDirectory, proj.OutputPath, runtimeIdentifier, "publish"); var apk = Path.Combine (publishDirectory, $"{proj.PackageName}.apk"); var apkSigned = Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"); // NOTE: the unsigned .apk doesn't exist when $(AndroidPackageFormats) is `aab;apk` @@ -447,116 +364,6 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot FileAssert.Exists (aab); FileAssert.Exists (aabSigned); } - - var resolvedPackagePublishItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "resolved-package-publish-items.txt")); - if (!isRelease) { - Assert.AreEqual (2, resolvedPackagePublishItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (resolvedPackagePublishItems)}"); - AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk"); - AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk"); - } else { - Assert.AreEqual (3, resolvedPackagePublishItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (resolvedPackagePublishItems)}"); - AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab"); - AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab"); - AssertResolvedPackagePublishItem (resolvedPackagePublishItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk"); - } - - var applicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "application-artifact-items.txt")); - if (!isRelease) { - Assert.AreEqual (2, applicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (applicationArtifactItems)}"); - AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName, "true"); - AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); - } else { - Assert.AreEqual (3, applicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (applicationArtifactItems)}"); - AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName, "true"); - AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName, "true"); - AssertQueriedApplicationArtifactItem (applicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); - } - - var observedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "observed-application-artifact-items.txt")); - if (!isRelease) { - Assert.AreEqual (2, observedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (observedApplicationArtifactItems)}"); - AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName); - AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); - } else { - Assert.AreEqual (3, observedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (observedApplicationArtifactItems)}"); - AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName); - AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName); - AssertApplicationArtifactItem (observedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName); - } - - Assert.IsTrue (dotnet.Build (target: "WritePublishReturnedApplicationArtifactItems", parameters: configParam), "`dotnet build -t:WritePublishReturnedApplicationArtifactItems` should succeed"); - var publishReturnedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "publish-returned-application-artifact-items.txt")); - if (!isRelease) { - Assert.AreEqual (2, publishReturnedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (publishReturnedApplicationArtifactItems)}"); - AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName, "true"); - AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); - } else { - Assert.AreEqual (3, publishReturnedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (publishReturnedApplicationArtifactItems)}"); - AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName, "true"); - AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName, "true"); - AssertQueriedApplicationArtifactItem (publishReturnedApplicationArtifactItems, Path.Combine (publishDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); - } - - Assert.IsTrue (dotnet.Build (target: "GetApplicationArtifacts", parameters: configParam), "`dotnet build -t:GetApplicationArtifacts` should succeed"); - var queriedApplicationArtifactItems = ReadApplicationArtifactItems (Path.Combine (Root, projBuilder.ProjectDirectory, "queried-application-artifact-items.txt")); - if (!isRelease) { - Assert.AreEqual (2, queriedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (queriedApplicationArtifactItems)}"); - AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}.apk"), $"{proj.PackageName}.apk", "apk", "false", proj.PackageName, "true"); - AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); - } else { - Assert.AreEqual (3, queriedApplicationArtifactItems.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (queriedApplicationArtifactItems)}"); - AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}.aab"), $"{proj.PackageName}.aab", "aab", "false", proj.PackageName, "true"); - AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.aab"), $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName, "true"); - AssertQueriedApplicationArtifactItem (queriedApplicationArtifactItems, Path.Combine (packageDirectory, $"{proj.PackageName}-Signed.apk"), $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "true"); - } - } - - static List ReadApplicationArtifactItems (string path) - { - FileAssert.Exists (path); - return File.ReadAllLines (path) - .Where (line => line.Length > 0) - .Select (line => line.Split ('|')) - .ToList (); - } - - static void AssertApplicationArtifactItem (List items, string fullPath, string fileName, string packageFormat, string signed, string packageId) - { - var matches = items.Where (item => - item.Length == 5 && - item [0] == fullPath && - item [1] == fileName && - item [2] == packageFormat && - item [3] == signed && - item [4] == packageId).ToList (); - Assert.AreEqual (1, matches.Count, $"Expected application artifact item '{fullPath}|{fileName}|{packageFormat}|{signed}|{packageId}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (items)}"); - } - - static void AssertQueriedApplicationArtifactItem (List items, string fullPath, string fileName, string packageFormat, string signed, string packageId, string mauiArtifact) - { - var matches = items.Where (item => - item.Length == 6 && - item [0] == fullPath && - item [1] == fileName && - item [2] == packageFormat && - item [3] == signed && - item [4] == packageId && - item [5] == mauiArtifact).ToList (); - Assert.AreEqual (1, matches.Count, $"Expected queried application artifact item '{fullPath}|{fileName}|{packageFormat}|{signed}|{packageId}|{mauiArtifact}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (items)}"); - } - - static void AssertResolvedPackagePublishItem (List items, string fullPath, string relativePath) - { - var matches = items.Where (item => - item.Length == 2 && - item [0] == fullPath && - item [1] == relativePath).ToList (); - Assert.AreEqual (1, matches.Count, $"Expected resolved package publish item '{fullPath}|{relativePath}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactItems (items)}"); - } - - static string FormatApplicationArtifactItems (List items) - { - return string.Join (Environment.NewLine, items.Select (item => string.Join ("|", item))); } [Test]