Skip to content
Merged
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
27 changes: 25 additions & 2 deletions Documentation/docs-mobile/building-apps/build-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,35 @@ same item name for their final application artifacts.

Each item includes the following metadata:

- `%(ApplicationId)`: The package name from the final merged
**AndroidManifest.xml**.
- `%(ApplicationTitle)`: The `android:label` value from the final merged
manifest.
- `%(ApplicationName)`: The same final manifest `android:label` value as
`%(ApplicationTitle)`.
- `%(ApplicationDisplayVersion)`: The `android:versionName` value from the
final merged manifest.
- `%(ApplicationVersion)`: The `android:versionCode` value from the final
merged manifest.
- `%(PackageFormat)`: `apk` or `aab`.
- `%(Signed)`: `true` when the package is signed.
- `%(PackageId)`: The resolved Android package name.
- `%(PackageId)`: The resolved Android package name, also exposed as
`%(ApplicationId)`.
- `%(Abi)`: The Android ABI for a per-ABI APK output. This metadata is only
set for per-ABI APKs.

The final merged manifest is authoritative for the common application
metadata. Its values take precedence over project properties such as
`$(ApplicationId)`, `$(ApplicationTitle)`, `$(ApplicationDisplayVersion)`, and
`$(ApplicationVersion)`. This also applies to custom manifests and projects
that set `$(GenerateApplicationManifest)` to `false`.

Resource-backed application labels are returned unchanged. For example, an
`android:label` value of `@string/app_name` produces
`ApplicationTitle="@string/app_name"` and
`ApplicationName="@string/app_name"`; the build does not select or resolve a
locale-specific resource value.

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.
Expand All @@ -48,7 +71,7 @@ For example:
<Target Name="WriteApplicationArtifacts" AfterTargets="Publish">
<WriteLinesToFile
File="$(PublishDir)application-artifacts.txt"
Lines="@(ApplicationArtifact->'%(FullPath)|%(Filename)%(Extension)|%(PackageFormat)|%(Signed)|%(PackageId)|%(Abi)')"
Lines="@(ApplicationArtifact->'%(FullPath)|%(Filename)%(Extension)|%(PackageFormat)|%(Signed)|%(PackageId)|%(Abi)|%(ApplicationTitle)|%(ApplicationDisplayVersion)|%(ApplicationVersion)')"
Overwrite="true" />
</Target>
```
Expand Down
12 changes: 11 additions & 1 deletion Documentation/docs-mobile/building-apps/build-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ existing items with additional metadata before this target or the `Publish`
target returns them. Replacing `$(GetApplicationArtifactsDependsOn)` does not
remove the required `Build` dependency.

Common application metadata is read from the final merged
**AndroidManifest.xml** before the artifacts are collected. Final manifest
values take precedence over corresponding project properties, including when a
custom manifest overrides generated values or
`$(GenerateApplicationManifest)` is `false`. An `android:label` resource
reference such as `@string/app_name` remains a resource reference; no locale is
selected when the metadata is returned.

Call this target directly when a CI job or custom tool needs the build output
artifact paths:

Expand Down Expand Up @@ -168,7 +176,9 @@ 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)`.
metadata such as `%(PackageFormat)`, `%(Signed)`, `%(PackageId)`, `%(Abi)`,
`%(ApplicationId)`, `%(ApplicationTitle)`, `%(ApplicationName)`,
`%(ApplicationDisplayVersion)`, and `%(ApplicationVersion)`.

`Publish` first runs `GetApplicationArtifacts`, which builds the project and
populates `@(ApplicationArtifact)` with the platform-produced artifacts. Targets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ This file contains the implementation for 'dotnet publish'.
<_ApplicationArtifactForPublish Include="@(ApplicationArtifact)" />
<_ApplicationArtifactPublishCopy
Include="@(_ApplicationArtifactForPublish->'$(PublishDir)%(Filename)%(Extension)')">
<!-- Keep this metadata in sync with _CollectApplicationArtifacts. -->
<PackageFormat>%(_ApplicationArtifactForPublish.PackageFormat)</PackageFormat>
<Signed>%(_ApplicationArtifactForPublish.Signed)</Signed>
<PackageId>%(_ApplicationArtifactForPublish.PackageId)</PackageId>
<Abi Condition=" '%(_ApplicationArtifactForPublish.Abi)' != '' ">%(_ApplicationArtifactForPublish.Abi)</Abi>
<ApplicationId>%(_ApplicationArtifactForPublish.ApplicationId)</ApplicationId>
<ApplicationTitle>%(_ApplicationArtifactForPublish.ApplicationTitle)</ApplicationTitle>
<ApplicationName>%(_ApplicationArtifactForPublish.ApplicationName)</ApplicationName>
<ApplicationDisplayVersion>%(_ApplicationArtifactForPublish.ApplicationDisplayVersion)</ApplicationDisplayVersion>
<ApplicationVersion>%(_ApplicationArtifactForPublish.ApplicationVersion)</ApplicationVersion>
Comment thread
Redth marked this conversation as resolved.
</_ApplicationArtifactPublishCopy>
<ApplicationArtifact Remove="@(ApplicationArtifact)" />
<ApplicationArtifact Include="@(_ApplicationArtifactPublishCopy)" />
Expand Down
20 changes: 19 additions & 1 deletion src/Xamarin.Android.Build.Tasks/Tasks/ReadAndroidManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,29 @@ public class ReadAndroidManifest : AndroidTask
[Output]
public bool IsTestOnly { get; set; } = false;

[Output]
public string? PackageName { get; set; }

[Output]
public string? ApplicationLabel { get; set; }

[Output]
public string? VersionName { get; set; }

[Output]
public string? VersionCode { get; set; }

public override bool RunTask ()
{
var androidNs = AndroidAppManifest.AndroidXNamespace;
var manifest = AndroidAppManifest.Load (ManifestFile, MonoAndroidHelper.SupportedVersions);
var app = manifest.Document.Element ("manifest")?.Element ("application");
var root = manifest.Document.Element ("manifest");
var app = root?.Element ("application");

PackageName = root?.Attribute ("package")?.Value;
VersionName = root?.Attribute (androidNs + "versionName")?.Value;
VersionCode = root?.Attribute (androidNs + "versionCode")?.Value;
ApplicationLabel = app?.Attribute (androidNs + "label")?.Value;

if (app != null) {
string? text = app.Attribute (androidNs + "extractNativeLibs")?.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,22 +212,52 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease, bool aot, bo
}

[Test]
// target, isRelease, packageFormat, withExtensionHook
[TestCase ("GetApplicationArtifacts", false, "apk", false)]
[TestCase ("Publish", false, "apk", false)]
[TestCase ("GetApplicationArtifacts", true, "aab", false)]
[TestCase ("GetApplicationArtifacts", false, "apk", true)]
public void DotNetBuildReturnsApplicationArtifacts (string target, bool isRelease, string packageFormat, bool withExtensionHook)
// target, isRelease, packageFormat, withExtensionHook, perAbi
[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 ("Publish", false, "apk", true, false)]
[TestCase ("GetApplicationArtifacts", true, "apk", false, true)]
public void DotNetBuildReturnsApplicationArtifacts (string target, bool isRelease, string packageFormat, bool withExtensionHook, bool perAbi)
{
const string applicationTitle = "Application Artifact Test";
const string applicationDisplayVersion = "3.2.1";
const string applicationVersion = "321";
var proj = new XamarinAndroidApplicationProject {
IsRelease = isRelease,
EnableDefaultItems = true,
};
proj.SetProperty ("AndroidPackageFormat", packageFormat);
proj.SetProperty ("ApplicationId", proj.PackageName);
proj.SetProperty ("ApplicationTitle", applicationTitle);
proj.SetProperty ("ApplicationDisplayVersion", applicationDisplayVersion);
proj.SetProperty ("ApplicationVersion", applicationVersion);
proj.AndroidManifest = proj.AndroidManifest
.Replace ("package=\"${PACKAGENAME}\"", "")
.Replace ("android:label=\"${PROJECT_NAME}\"", "")
.Replace ("android:versionName=\"1.0\"", "")
.Replace ("android:versionCode=\"1\"", "");
if (packageFormat == "aab") {
// Disable fast deployment for AABs to avoid XA0119.
proj.EmbedAssembliesIntoApk = true;
}
if (perAbi) {
proj.SetProperty (proj.ReleaseProperties, KnownProperties.AndroidCreatePackagePerAbi, true);
proj.SetProperty (proj.ReleaseProperties, KnownProperties.RunAOTCompilation, false);
proj.SetRuntimeIdentifiers (AndroidTargetArch.Arm64, AndroidTargetArch.X86_64);
proj.Imports.Add (new Import (() => "ApplicationArtifactPerAbi.targets") {
TextContent = () => """
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="_CreateApplicationArtifactTestPerAbiFiles" BeforeTargets="_CollectApplicationArtifacts">
<Touch
Files="@(_BuildTargetAbis->'$(OutDir)$(_AndroidPackage)-%(Identity).apk');@(_BuildTargetAbis->'$(OutDir)$(_AndroidPackage)-%(Identity)-Signed.apk')"
AlwaysCreate="true" />
</Target>
</Project>
"""
});
}
if (withExtensionHook) {
// Validate that $(GetApplicationArtifactsDependsOn) runs *after* _CollectApplicationArtifacts,
// so MAUI-style extension targets can enrich the items the platform already produced.
Expand All @@ -241,7 +271,11 @@ public void DotNetBuildReturnsApplicationArtifacts (string target, bool isReleas
<Target Name="_AddExtensionArtifactMetadata">
<Error Condition=" '@(ApplicationArtifact)' == '' " Text="Expected ApplicationArtifact items before extension metadata augmentation." />
<ItemGroup>
<ApplicationArtifact Update="@(ApplicationArtifact)" MauiArtifact="true" />
<ApplicationArtifact
Update="@(ApplicationArtifact)"
ApplicationTitle="Extended Application Title"
ApplicationName="Extended Application Name"
MauiArtifact="true" />
</ItemGroup>
</Target>
</Project>
Expand All @@ -264,21 +298,68 @@ public void DotNetBuildReturnsApplicationArtifacts (string target, bool isReleas
$"`dotnet build -t:{target} -getTargetResult:{target}` should succeed");

var items = ReadApplicationArtifactTargetResultItems (dotnet.ProcessLogFile, target);
var expectedApplicationTitle = withExtensionHook ? "Extended Application Title" : applicationTitle;
var expectedApplicationName = withExtensionHook ? "Extended Application Name" : applicationTitle;
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);
AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}.aab", "aab", "false", proj.PackageName, "", expectedApplicationTitle, expectedApplicationName, applicationDisplayVersion, applicationVersion, expectedMauiArtifact);
AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-Signed.aab", "aab", "true", proj.PackageName, "", expectedApplicationTitle, expectedApplicationName, applicationDisplayVersion, applicationVersion, expectedMauiArtifact);
AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "", expectedApplicationTitle, expectedApplicationName, applicationDisplayVersion, applicationVersion, 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);
Assert.AreEqual (perAbi ? 6 : 2, items.Count, $"Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}");
AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}.apk", "apk", "false", proj.PackageName, "", expectedApplicationTitle, expectedApplicationName, applicationDisplayVersion, applicationVersion, expectedMauiArtifact);
AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-Signed.apk", "apk", "true", proj.PackageName, "", expectedApplicationTitle, expectedApplicationName, applicationDisplayVersion, applicationVersion, expectedMauiArtifact);
if (perAbi) {
foreach (var abi in new [] { "arm64-v8a", "x86_64" }) {
AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-{abi}.apk", "apk", "false", proj.PackageName, abi, expectedApplicationTitle, expectedApplicationName, applicationDisplayVersion, applicationVersion, expectedMauiArtifact);
AssertApplicationArtifactTargetResultItem (items, $"{proj.PackageName}-{abi}-Signed.apk", "apk", "true", proj.PackageName, abi, expectedApplicationTitle, expectedApplicationName, applicationDisplayVersion, applicationVersion, expectedMauiArtifact);
}
}
}
}

[Test]
[TestCase ("GetApplicationArtifacts", true, "Manifest Application")]
[TestCase ("GetApplicationArtifacts", true, "@string/app_name")]
[TestCase ("Publish", false, "@string/app_name")]
public void ApplicationArtifactsUseFinalManifestMetadata (string target, bool generateApplicationManifest, string applicationLabel)
{
const string packageName = "com.example.manifestmetadata";
const string versionName = "9.8.7";
const string versionCode = "987";
var proj = new XamarinAndroidApplicationProject {
EnableDefaultItems = true,
};
proj.SetProperty ("GenerateApplicationManifest", generateApplicationManifest.ToString ());
proj.SetProperty ("ApplicationId", "com.example.property");
proj.SetProperty ("ApplicationTitle", "Property Application");
proj.SetProperty ("ApplicationDisplayVersion", "1.2.3");
proj.SetProperty ("ApplicationVersion", "123");
proj.AndroidManifest = proj.AndroidManifest
.Replace ("package=\"${PACKAGENAME}\"", $"package=\"{packageName}\"")
.Replace ("android:label=\"${PROJECT_NAME}\"", $"android:label=\"{applicationLabel}\"")
.Replace ("android:versionName=\"1.0\"", $"android:versionName=\"{versionName}\"")
.Replace ("android:versionCode=\"1\"", $"android:versionCode=\"{versionCode}\"");

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: new [] { $"-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, $"{packageName}.apk", "apk", "false", packageName, "", applicationLabel, applicationLabel, versionName, versionCode, "");
AssertApplicationArtifactTargetResultItem (items, $"{packageName}-Signed.apk", "apk", "true", packageName, "", applicationLabel, applicationLabel, versionName, versionCode, "");
}

static List<Dictionary<string, string>> ReadApplicationArtifactTargetResultItems (string processLogFile, string target)
{
var output = File.ReadAllText (processLogFile);
Expand All @@ -304,16 +385,21 @@ static List<Dictionary<string, string>> ReadApplicationArtifactTargetResultItems
return items;
}

static void AssertApplicationArtifactTargetResultItem (List<Dictionary<string, string>> items, string fileName, string packageFormat, string signed, string packageId, string abi, string mauiArtifact)
static void AssertApplicationArtifactTargetResultItem (List<Dictionary<string, string>> items, string fileName, string packageFormat, string signed, string applicationId, string abi, string applicationTitle, string applicationName, string applicationDisplayVersion, string applicationVersion, 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, "PackageId") == applicationId &&
GetTargetResultMetadata (item, "Abi") == abi &&
GetTargetResultMetadata (item, "ApplicationId") == applicationId &&
GetTargetResultMetadata (item, "ApplicationTitle") == applicationTitle &&
GetTargetResultMetadata (item, "ApplicationName") == applicationName &&
GetTargetResultMetadata (item, "ApplicationDisplayVersion") == applicationDisplayVersion &&
GetTargetResultMetadata (item, "ApplicationVersion") == applicationVersion &&
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)}");
Assert.AreEqual (1, matches.Count, $"Expected application artifact item '{fileName}|{packageFormat}|{signed}|{applicationId}|{abi}|{applicationTitle}|{applicationName}|{applicationDisplayVersion}|{applicationVersion}|{mauiArtifact}'. Actual items:{Environment.NewLine}{FormatApplicationArtifactTargetResultItems (items)}");
}

static string GetTargetResultMetadata (Dictionary<string, string> item, string name)
Expand All @@ -324,7 +410,7 @@ static string GetTargetResultMetadata (Dictionary<string, string> item, string n
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")}"));
$"{GetTargetResultMetadata (item, "Identity")}|{GetTargetResultMetadata (item, "Filename")}{GetTargetResultMetadata (item, "Extension")}|{GetTargetResultMetadata (item, "PackageFormat")}|{GetTargetResultMetadata (item, "Signed")}|{GetTargetResultMetadata (item, "PackageId")}|{GetTargetResultMetadata (item, "Abi")}|{GetTargetResultMetadata (item, "ApplicationId")}|{GetTargetResultMetadata (item, "ApplicationTitle")}|{GetTargetResultMetadata (item, "ApplicationName")}|{GetTargetResultMetadata (item, "ApplicationDisplayVersion")}|{GetTargetResultMetadata (item, "ApplicationVersion")}|{GetTargetResultMetadata (item, "MauiArtifact")}"));
}


Expand Down
Loading
Loading