Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 38 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,44 @@ 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.

## ApplicationArtifact

`@(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. .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:

- `%(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
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)` 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:

```xml
<Target Name="WriteApplicationArtifacts" AfterTargets="Publish">
<WriteLinesToFile
File="$(PublishDir)application-artifacts.txt"
Lines="@(ApplicationArtifact->'%(FullPath)|%(Filename)%(Extension)|%(PackageFormat)|%(Signed)|%(PackageId)|%(Abi)')"
Overwrite="true" />
</Target>
```

## AndroidAdditionalJavaManifest

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

```shell
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 build MyApp.csproj -t:Publish -getTargetResult:Publish
```

## Install

[Creates, signs](#signandroidpackage), and installs the Android package onto
Expand Down Expand Up @@ -135,6 +162,27 @@ 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)`.

`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:

```shell
dotnet build MyApp.csproj -t:Publish -getTargetResult:Publish
```

## RunWithLogging

Runs the application with additional logging enabled. Helpful when reporting or investigating an issue with
Expand All @@ -153,6 +201,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
[`@(ApplicationArtifact)`](build-items.md#applicationartifact) 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;
_CollectApplicationArtifacts;
</BuildDependsOn>
<IncrementalCleanDependsOn>
_PrepareAssemblies;
Expand All @@ -62,6 +63,7 @@ properties that determine build ordering.
<_PackageForAndroidDependsOn>
Build;
_CopyPackage;
_CollectApplicationArtifacts;
</_PackageForAndroidDependsOn>
<_PrepareBuildApkDependsOnTargets>
_SetLatestTargetFrameworkVersion;
Expand Down Expand Up @@ -115,12 +117,14 @@ properties that determine build ordering.
_CopyPackage;
_Sign;
_CreateUniversalApkFromBundle;
_CollectApplicationArtifacts;
</_MinimalSignAndroidPackageDependsOn>
<SignAndroidPackageDependsOn Condition=" '$(BuildingInsideVisualStudio)' != 'true' ">
Build;
Package;
_Sign;
_CreateUniversalApkFromBundle;
_CollectApplicationArtifacts;
</SignAndroidPackageDependsOn>
<SignAndroidPackageDependsOn Condition=" '$(BuildingInsideVisualStudio)' == 'true' ">
$(_MinimalSignAndroidPackageDependsOn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,46 @@ This file contains the implementation for 'dotnet publish'.

<PropertyGroup>
<_PublishDependsOn>
Build;
GetApplicationArtifacts;
Comment thread
Redth marked this conversation as resolved.
PrepareForPublish;
_CalculateAndroidFilesToPublish;
CopyFilesToPublishDirectory;
_UpdateApplicationArtifactsForPublish;
</_PublishDependsOn>
</PropertyGroup>

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

<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="@(ApplicationArtifact)" RelativePath="%(Filename)%(Extension)" />
<ResolvedFileToPublish Include="@(_AndroidFilesToPublish)" RelativePath="%(FileName)%(Extension)" />
</ItemGroup>
</Target>

<Target Name="_UpdateApplicationArtifactsForPublish">
<ItemGroup>
<_ApplicationArtifactForPublish Remove="@(_ApplicationArtifactForPublish)" />
<_ApplicationArtifactPublishCopy Remove="@(_ApplicationArtifactPublishCopy)" />
<_ApplicationArtifactForPublish Include="@(ApplicationArtifact)" />
<_ApplicationArtifactPublishCopy
Include="@(_ApplicationArtifactForPublish->'$(PublishDir)%(Filename)%(Extension)')"
Condition="Exists('$(PublishDir)%(_ApplicationArtifactForPublish.Filename)%(_ApplicationArtifactForPublish.Extension)')">
<PackageFormat>%(_ApplicationArtifactForPublish.PackageFormat)</PackageFormat>
<Signed>%(_ApplicationArtifactForPublish.Signed)</Signed>
<PackageId>%(_ApplicationArtifactForPublish.PackageId)</PackageId>
<Abi Condition=" '%(_ApplicationArtifactForPublish.Abi)' != '' ">%(_ApplicationArtifactForPublish.Abi)</Abi>
</_ApplicationArtifactPublishCopy>
<ApplicationArtifact Remove="@(ApplicationArtifact)" />
<ApplicationArtifact Include="@(_ApplicationArtifactPublishCopy)" />
<_ApplicationArtifactForPublish Remove="@(_ApplicationArtifactForPublish)" />
<_ApplicationArtifactPublishCopy Remove="@(_ApplicationArtifactPublishCopy)" />
</ItemGroup>
</Target>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -210,6 +211,137 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease, bool aot, bo
}
}

[Test]
// 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 = () => """
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<GetApplicationArtifactsDependsOn>$(GetApplicationArtifactsDependsOn);_AddExtensionArtifactMetadata</GetApplicationArtifactsDependsOn>
</PropertyGroup>
<Target Name="_AddExtensionArtifactMetadata">
<Error Condition=" '@(ApplicationArtifact)' == '' " Text="Expected ApplicationArtifact items before extension metadata augmentation." />
<ItemGroup>
<ApplicationArtifact Update="@(ApplicationArtifact)" MauiArtifact="true" />
</ItemGroup>
</Target>
</Project>
"""
});
}

using var builder = CreateDllBuilder ();
builder.Save (proj);

var dotnet = new DotNetCLI (Path.Combine (Root, builder.ProjectDirectory, proj.ProjectFilePath)) {
Verbosity = "minimal",
};
var msbuildArgs = new List<string> { $"-getTargetResult:{target}" };
if (isRelease) {
msbuildArgs.Add ("-c:Release");
}
Assert.IsTrue (
dotnet.Build (target: target, msbuildArguments: msbuildArgs.ToArray ()),
$"`dotnet build -t:{target} -getTargetResult:{target}` should succeed");

var items = ReadApplicationArtifactTargetResultItems (dotnet.ProcessLogFile, target);
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<Dictionary<string, string>> 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<Dictionary<string, string>> ();
foreach (var item in targetResult.GetProperty ("Items").EnumerateArray ()) {
var metadata = new Dictionary<string, string> (StringComparer.Ordinal);
foreach (var property in item.EnumerateObject ()) {
metadata.Add (property.Name, property.Value.GetString () ?? "");
}
items.Add (metadata);
}
return items;
}

static void AssertApplicationArtifactTargetResultItem (List<Dictionary<string, string>> 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 &&
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<string, string> item, string name)
{
return item.TryGetValue (name, out var value) ? value : "";
}

static string FormatApplicationArtifactTargetResultItems (List<Dictionary<string, string>> 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, "Abi")}|{GetTargetResultMetadata (item, "MauiArtifact")}"));
}

static object [] MonoComponentMaskChecks () => new object [] {
new object[] {
true, // enableProfiler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ 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
Expand Down
Loading
Loading