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) +