Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
aa80d15
Expose Android package output items
Redth Jun 16, 2026
a311128
Trim Android package output item metadata
Redth Jun 16, 2026
0ca42b0
Simplify Android package output collection
Redth Jun 16, 2026
9a91ce2
Guard per-ABI package output collection
Redth Jun 16, 2026
222e91f
Add ABI metadata to package outputs
Redth Jun 16, 2026
d7e18c3
Add publish package item coverage
Redth Jun 16, 2026
9756adc
Rename application artifact item
Redth Jun 16, 2026
595668b
Align application artifact query target
Redth Jun 17, 2026
220e089
Strengthen application artifact extension tests
Redth Jun 17, 2026
8dd85f4
Run application artifact extensions during publish
Redth Jun 17, 2026
4afcab5
Document application artifact query targets
Redth Jun 17, 2026
0be3044
Clarify application artifact enrichment docs
Redth Jun 17, 2026
8accb87
Make application artifact build dependency mandatory
Redth Jun 17, 2026
a39b446
Use dotnet build in artifact query docs
Redth Jun 17, 2026
62fded7
Use transforms for artifact test output
Redth Jun 17, 2026
f0b7861
Use transforms for packaging artifact test output
Redth Jun 17, 2026
56bff99
Harden Android application artifacts target
Redth Jun 17, 2026
7af99da
Simplify application artifact target graph
Redth Jun 17, 2026
eef5cc8
Consolidate ApplicationArtifact tests into one parameterized test
jonathanpeppers Jun 17, 2026
b4450c4
Fix application artifact target result test
Redth Jun 26, 2026
89bef7c
Add warning count assertion helper
Redth Jul 2, 2026
e1c9393
Merge remote-tracking branch 'origin/main' into redth/android-publish…
Copilot Jul 2, 2026
73b90f5
Address PR review feedback
Copilot Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Documentation/docs-mobile/building-apps/build-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Target Name="WriteAndroidPackageOutputs" AfterTargets="SignAndroidPackage">
<WriteLinesToFile
File="$(OutputPath)android-package-outputs.txt"
Lines="%(AndroidPackageOutput.FullPath)|%(AndroidPackageOutput.PackageFormat)|%(AndroidPackageOutput.Signed)"
Overwrite="true" />
</Target>
```

## 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
<Target Name="WriteAndroidPublishedPackageOutputs" AfterTargets="Publish">
<WriteLinesToFile
File="$(PublishDir)android-published-package-outputs.txt"
Lines="%(AndroidPublishedPackageOutput.FullPath)|%(AndroidPublishedPackageOutput.PackageFormat)|%(AndroidPublishedPackageOutput.Signed)"
Overwrite="true" />
</Target>
```
Comment thread
Redth marked this conversation as resolved.
Outdated

## AndroidAdditionalJavaManifest

`<AndroidAdditionalJavaManifest>` is used in conjunction with
Expand Down
3 changes: 3 additions & 0 deletions Documentation/docs-mobile/building-apps/build-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Comment thread
Redth marked this conversation as resolved.
## StartAndroidActivity

Starts the default activity on the device or the running emulator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ properties that determine build ordering.
_CopyPackage;
_Sign;
_CreateUniversalApkFromBundle;
_CollectAndroidPackageOutputs;
</BuildDependsOn>
<IncrementalCleanDependsOn>
_PrepareAssemblies;
Expand All @@ -62,6 +63,7 @@ properties that determine build ordering.
<_PackageForAndroidDependsOn>
Build;
_CopyPackage;
_CollectAndroidPackageOutputs;
</_PackageForAndroidDependsOn>
<_PrepareBuildApkDependsOnTargets>
_SetLatestTargetFrameworkVersion;
Expand Down Expand Up @@ -115,12 +117,14 @@ properties that determine build ordering.
_CopyPackage;
_Sign;
_CreateUniversalApkFromBundle;
_CollectAndroidPackageOutputs;
</_MinimalSignAndroidPackageDependsOn>
<SignAndroidPackageDependsOn Condition=" '$(BuildingInsideVisualStudio)' != 'true' ">
Build;
Package;
_Sign;
_CreateUniversalApkFromBundle;
_CollectAndroidPackageOutputs;
</SignAndroidPackageDependsOn>
<SignAndroidPackageDependsOn Condition=" '$(BuildingInsideVisualStudio)' == 'true' ">
$(_MinimalSignAndroidPackageDependsOn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,35 @@ This file contains the implementation for 'dotnet publish'.
<_PublishDependsOn>
Build;
PrepareForPublish;
_CollectAndroidPackageOutputs;
_CalculateAndroidFilesToPublish;
CopyFilesToPublishDirectory;
_CollectAndroidPublishedPackageOutputs;
</_PublishDependsOn>
</PropertyGroup>

<Target Name="Publish" DependsOnTargets="$(_PublishDependsOn)" />

<Target Name="_CalculateAndroidFilesToPublish">
<ItemGroup>
<_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)')" />
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" />
<ResolvedFileToPublish Include="@(AndroidPackageOutput)" RelativePath="%(AndroidPackageOutput.RelativePath)" />
<ResolvedFileToPublish Include="@(_AndroidFilesToPublish)" RelativePath="%(FileName)%(Extension)" />
</ItemGroup>
</Target>

<Target Name="_CollectAndroidPublishedPackageOutputs">
<ItemGroup>
<AndroidPublishedPackageOutput Remove="@(AndroidPublishedPackageOutput)" />
<AndroidPublishedPackageOutput
Include="@(AndroidPackageOutput->'$(PublishDir)%(RelativePath)')"
Condition="Exists('$(PublishDir)%(AndroidPackageOutput.RelativePath)')">
<OriginalPath>%(AndroidPackageOutput.FullPath)</OriginalPath>
</AndroidPublishedPackageOutput>
<AndroidPublishedPackageOutput Update="@(AndroidPublishedPackageOutput)" RelativePath="%(Filename)%(Extension)" />
</ItemGroup>
</Target>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => """
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="WriteAndroidPackageOutputItems" AfterTargets="Build">
<WriteLinesToFile
File="$(MSBuildProjectDirectory)/android-package-output-items.txt"
Lines="%(AndroidPackageOutput.RelativePath)|%(AndroidPackageOutput.PackageFormat)|%(AndroidPackageOutput.Signed)"
Overwrite="true" />
</Target>
</Project>
"""
},
}
};
proj.SetRuntime (runtime);
proj.SetProperty (proj.ReleaseProperties, "AndroidUseApkSigner", useApkSigner);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => """
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Comment thread
Redth marked this conversation as resolved.
Outdated
<Target Name="WriteAndroidPackageOutputItems" AfterTargets="Publish">
<WriteLinesToFile
File="$(MSBuildProjectDirectory)/android-package-output-items.txt"
Lines="package|%(AndroidPackageOutput.RelativePath)|%(AndroidPackageOutput.PackageFormat)|%(AndroidPackageOutput.Signed)|%(AndroidPackageOutput.IsUniversal)|%(AndroidPackageOutput.SourcePackageFormat)|%(AndroidPackageOutput.PackageId)"
Overwrite="true" />
<WriteLinesToFile
File="$(MSBuildProjectDirectory)/android-package-output-items.txt"
Lines="published|%(AndroidPublishedPackageOutput.RelativePath)|%(AndroidPublishedPackageOutput.PackageFormat)|%(AndroidPublishedPackageOutput.Signed)|%(AndroidPublishedPackageOutput.IsUniversal)|%(AndroidPublishedPackageOutput.SourcePackageFormat)|%(AndroidPublishedPackageOutput.PackageId)"
Overwrite="false" />
</Target>
</Project>
"""
},
}
};
proj.SetRuntime (runtime);
Expand Down Expand Up @@ -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<string []> ReadAndroidPackageOutputItems (string path)
{
FileAssert.Exists (path);
return File.ReadAllLines (path)
.Where (line => line.Length > 0)
.Select (line => line.Split ('|'))
.ToList ();
}

static void AssertPackageOutputItem (List<string []> 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<string []> items)
{
return string.Join (Environment.NewLine, items.Select (item => string.Join ("|", item)));
}

[Test]
Expand Down
75 changes: 75 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
Original file line number Diff line number Diff line change
Expand Up @@ -2678,6 +2678,81 @@ because xbuild doesn't support framework reference assemblies.
<UnzipToFolder Sources="$(_UniversalApkSetIntermediate)" DestinationDirectories="$(OutDir)" Files="@(_FilestoExtract)" />
</Target>

<Target Name="_CollectAndroidPackageOutputs"
Condition=" '$(AndroidApplication)' == 'true' "
DependsOnTargets="_ValidateAndroidPackageProperties">
<ItemGroup>
<AndroidPackageOutput Remove="@(AndroidPackageOutput)" />
Comment thread
Redth marked this conversation as resolved.
Outdated
<_AndroidUnsignedApkPackageOutput Remove="@(_AndroidUnsignedApkPackageOutput)" />
<_AndroidSignedApkPackageOutput Remove="@(_AndroidSignedApkPackageOutput)" />
<_AndroidUnsignedAabPackageOutput Remove="@(_AndroidUnsignedAabPackageOutput)" />
<_AndroidSignedAabPackageOutput Remove="@(_AndroidSignedAabPackageOutput)" />
<_AndroidUnsignedApkPackageOutput
Condition=" '$(AndroidPackageFormat)' != 'aab' And Exists('$(ApkFile)') "
Include="$(ApkFile)">
<PackageFormat>apk</PackageFormat>
<Signed>false</Signed>
<SourcePackageFormat>apk</SourcePackageFormat>
<IsUniversal>false</IsUniversal>
</_AndroidUnsignedApkPackageOutput>
<_AndroidUnsignedApkPackageOutput
Condition=" '$(AndroidPackageFormat)' != 'aab' And '$(AndroidCreatePackagePerAbi)' == 'true' "
Include="$(OutDir)$(_AndroidPackage)*.apk"
Exclude="$(ApkFile);$(OutDir)$(_AndroidPackage)*-Signed.apk;$(OutDir)$(_AndroidPackage)*-Signed-Unaligned.apk">
<PackageFormat>apk</PackageFormat>
<Signed>false</Signed>
<SourcePackageFormat>apk</SourcePackageFormat>
<IsUniversal>false</IsUniversal>
</_AndroidUnsignedApkPackageOutput>
<_AndroidSignedApkPackageOutput Condition="Exists('$(ApkFileSigned)')" Include="$(ApkFileSigned)">
<PackageFormat>apk</PackageFormat>
<Signed>true</Signed>
<SourcePackageFormat Condition=" '$(AndroidPackageFormat)' == 'aab' ">aab</SourcePackageFormat>
<SourcePackageFormat Condition=" '$(AndroidPackageFormat)' != 'aab' ">apk</SourcePackageFormat>
<IsUniversal Condition=" '$(AndroidPackageFormat)' == 'aab' ">true</IsUniversal>
<IsUniversal Condition=" '$(AndroidPackageFormat)' != 'aab' ">false</IsUniversal>
</_AndroidSignedApkPackageOutput>
<_AndroidSignedApkPackageOutput
Condition=" '$(AndroidCreatePackagePerAbi)' == 'true' "
Include="$(OutDir)$(_AndroidPackage)*-Signed.apk"
Exclude="$(ApkFileSigned)">
<PackageFormat>apk</PackageFormat>
<Signed>true</Signed>
<SourcePackageFormat Condition=" '$(AndroidPackageFormat)' == 'aab' ">aab</SourcePackageFormat>
<SourcePackageFormat Condition=" '$(AndroidPackageFormat)' != 'aab' ">apk</SourcePackageFormat>
<IsUniversal Condition=" '$(AndroidPackageFormat)' == 'aab' ">true</IsUniversal>
<IsUniversal Condition=" '$(AndroidPackageFormat)' != 'aab' ">false</IsUniversal>
</_AndroidSignedApkPackageOutput>
<_AndroidUnsignedAabPackageOutput Condition=" '$(AndroidPackageFormat)' == 'aab' And Exists('$(_AabFile)') " Include="$(_AabFile)">
<PackageFormat>aab</PackageFormat>
<Signed>false</Signed>
<SourcePackageFormat>aab</SourcePackageFormat>
<IsUniversal>false</IsUniversal>
</_AndroidUnsignedAabPackageOutput>
<_AndroidSignedAabPackageOutput Condition=" '$(AndroidPackageFormat)' == 'aab' And Exists('$(_AabFileSigned)') " Include="$(_AabFileSigned)">
<PackageFormat>aab</PackageFormat>
<Signed>true</Signed>
<SourcePackageFormat>aab</SourcePackageFormat>
<IsUniversal>false</IsUniversal>
</_AndroidSignedAabPackageOutput>
<AndroidPackageOutput Include="@(_AndroidUnsignedApkPackageOutput);@(_AndroidSignedApkPackageOutput);@(_AndroidUnsignedAabPackageOutput);@(_AndroidSignedAabPackageOutput)" />
<AndroidPackageOutput
Update="@(AndroidPackageOutput)"
PackageId="$(_AndroidPackage)"
RuntimeIdentifier="$(RuntimeIdentifier)"
TargetFramework="$(TargetFramework)"
Configuration="$(Configuration)"
AndroidPackageFormat="$(AndroidPackageFormat)"
AndroidPackageFormats="$(AndroidPackageFormats)"
RelativePath="%(Filename)%(Extension)" />
</ItemGroup>
</Target>
Comment thread
Redth marked this conversation as resolved.
Outdated

<Target Name="GetAndroidPackageOutputs"
Condition=" '$(AndroidApplication)' == 'true' "
DependsOnTargets="SignAndroidPackage;_CollectAndroidPackageOutputs"
Returns="@(AndroidPackageOutput)" />

<Target Name="SignAndroidPackage" DependsOnTargets="$(SignAndroidPackageDependsOn)">
</Target>

Expand Down
Loading