diff --git a/Documentation/docs-mobile/building-apps/build-items.md b/Documentation/docs-mobile/building-apps/build-items.md
index d1454f60396..3d3087cc9b9 100644
--- a/Documentation/docs-mobile/building-apps/build-items.md
+++ b/Documentation/docs-mobile/building-apps/build-items.md
@@ -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.
@@ -48,7 +71,7 @@ For example:
```
diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md
index 4d342fab9d5..63ee27f9848 100644
--- a/Documentation/docs-mobile/building-apps/build-targets.md
+++ b/Documentation/docs-mobile/building-apps/build-targets.md
@@ -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:
@@ -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
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 bad26e57c19..dc9f51b6b0c 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
@@ -39,10 +39,16 @@ This file contains the implementation for 'dotnet publish'.
<_ApplicationArtifactForPublish Include="@(ApplicationArtifact)" />
<_ApplicationArtifactPublishCopy
Include="@(_ApplicationArtifactForPublish->'$(PublishDir)%(Filename)%(Extension)')">
+
%(_ApplicationArtifactForPublish.PackageFormat)
%(_ApplicationArtifactForPublish.Signed)
%(_ApplicationArtifactForPublish.PackageId)
%(_ApplicationArtifactForPublish.Abi)
+ %(_ApplicationArtifactForPublish.ApplicationId)
+ %(_ApplicationArtifactForPublish.ApplicationTitle)
+ %(_ApplicationArtifactForPublish.ApplicationName)
+ %(_ApplicationArtifactForPublish.ApplicationDisplayVersion)
+ %(_ApplicationArtifactForPublish.ApplicationVersion)
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ReadAndroidManifest.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ReadAndroidManifest.cs
index 4c77c4a89be..7de9d6c8398 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/ReadAndroidManifest.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/ReadAndroidManifest.cs
@@ -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;
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 cb343b4e592..ac944a79bdb 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,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 = () => """
+
+
+
+
+
+"""
+ });
+ }
if (withExtensionHook) {
// Validate that $(GetApplicationArtifactsDependsOn) runs *after* _CollectApplicationArtifacts,
// so MAUI-style extension targets can enrich the items the platform already produced.
@@ -241,7 +271,11 @@ public void DotNetBuildReturnsApplicationArtifacts (string target, bool isReleas
-
+
@@ -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> ReadApplicationArtifactTargetResultItems (string processLogFile, string target)
{
var output = File.ReadAllText (processLogFile);
@@ -304,16 +385,21 @@ static List> ReadApplicationArtifactTargetResultItems
return items;
}
- static void AssertApplicationArtifactTargetResultItem (List> items, string fileName, string packageFormat, string signed, string packageId, string abi, string mauiArtifact)
+ static void AssertApplicationArtifactTargetResultItem (List> 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 item, string name)
@@ -324,7 +410,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, "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")}"));
}
diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
index 8e0b5004150..3a00023605b 100644
--- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
+++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
@@ -1605,6 +1605,10 @@ because xbuild doesn't support framework reference assemblies.
+
+
+
+
.so;$(AndroidStoreUncompressedFileExtensions)
@@ -2701,7 +2705,7 @@ because xbuild doesn't support framework reference assemblies.
+ DependsOnTargets="_ValidateAndroidPackageProperties;_ReadAndroidManifest">
true
+ Update="@(ApplicationArtifact)">
+ $([MSBuild]::ValueOrDefault('$(_ApplicationArtifactApplicationId)', '$(_AndroidPackage)'))
+ $(_ApplicationArtifactApplicationId)
+ $(_ApplicationArtifactApplicationTitle)
+ $(_ApplicationArtifactApplicationTitle)
+ $(_ApplicationArtifactApplicationDisplayVersion)
+ $(_ApplicationArtifactApplicationVersion)
+