Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
22 changes: 22 additions & 0 deletions docs/design/ApplicationArtifact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# ApplicationArtifact metadata in .NET MAUI

`@(ApplicationArtifact)` is the shared public item group for final application artifacts. Platform SDKs own creating these items and their artifact identity, path, format, and platform-specific metadata:

- .NET for Android creates APK and AAB items.
- .NET for iOS, Mac Catalyst, tvOS, and macOS creates `.app`, `.ipa`, `.pkg`, and `.xcarchive` items.
- Other platforms should populate the same item group from their own build or publish pipeline.

MAUI does not rediscover platform package files and does not create a parallel MAUI-specific artifact item group. Instead, it defines default MAUI metadata for the shared `ApplicationArtifact` item type. Platform SDKs create the items, and the defaults supplement their platform-specific metadata.

MAUI supplies these metadata values when the matching project properties are set:

- `ApplicationId`
- `ApplicationIdGuid`
- `ApplicationName`, mapped from `ApplicationTitle`
- `ApplicationTitle`
- `ApplicationDisplayVersion`
- `ApplicationVersion`

The defaults are declared with an MSBuild `ItemDefinitionGroup`, so explicit metadata from a platform SDK takes precedence. The defaults also apply when a platform recreates an item, as Android does when changing artifact identities to published paths.

`GetApplicationArtifacts` and `Publish` remain platform-owned result paths. MAUI only supplies shared metadata and does not change which artifacts those targets produce or return.
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,130 @@ public void BuildsWithSpecialCharacters(string id, string projectName, string ex
$"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors.");
}

[Fact]
public void ApplicationArtifactsAreEnrichedWithMauiMetadata()
{
SetTestIdentifier(nameof(ApplicationArtifactsAreEnrichedWithMauiMetadata));
var projectDir = TestDirectory;
var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj");
var seededArtifactsFile = Path.Combine(projectDir, "seeded-application-artifacts.txt");
var publishedArtifactsFile = Path.Combine(projectDir, "published-application-artifacts.txt");
var mauiSdkAfterTargets = Path.Combine(
TestEnvironment.GetMauiDirectory(),
"src",
"Workload",
"Microsoft.Maui.Sdk",
"Sdk",
"Microsoft.Maui.Sdk.After.targets");

Assert.True(DotnetInternal.New("maui", projectDir, DotNetCurrent, output: _output),
$"Unable to create template maui. Check test output for errors.");

var buildProps = BuildProps;
buildProps.Add($"MauiSdkAfterTargets=\"{mauiSdkAfterTargets}\"");

FileUtilities.ReplaceInFile(projectFile,
"</Project>",
"""
<PropertyGroup>
<ApplicationTitle>My Artifact App</ApplicationTitle>
<ApplicationId>com.example.artifacts</ApplicationId>
<ApplicationIdGuid>11111111-2222-3333-4444-555555555555</ApplicationIdGuid>
<ApplicationDisplayVersion>2.3</ApplicationDisplayVersion>
<ApplicationVersion>42</ApplicationVersion>
</PropertyGroup>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The relative path $(MSBuildThisFileDirectory)..\..\..\ assumes the test directory is located within the MAUI repository bin/ folder. However, on Azure Pipelines CI, TestDirectory is located under AGENT_TEMPDIRECTORY (e.g., .../test-dir/ApplicationArtifactsAreEnrichedWithMauiMetadata/). As a result, traversing up 3 directories points to AGENT_TEMPDIRECTORY/src/Workload/... instead of the repository, causing MSB4019 missing import errors and failing the integration tests.

<Import Project="$(MauiSdkAfterTargets)" />
<Target Name="SeedApplicationArtifacts">
<ItemGroup>
<ApplicationArtifact Include="$(MSBuildProjectDirectory)/artifacts/platform/android/MyArtifactApp-Signed.apk">
<PackageFormat>apk</PackageFormat>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicit <Import Project="$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\\..\\..\\src\\Workload\\...'))" /> assumes the project directory is 3 levels deep inside the repo root. Locally this resolves from <repo>/bin/test-dir/<testname>/ to <repo>/. In CI, TestDirectory is under $AGENT_TEMPDIRECTORY/test-dir/<testname>/, so 3 parent traversals land in the agent temp directory—not the repo. The targets file won't exist there, causing a build error. This is the likely cause of the CI 'Build integration tests' failures on both macOS and Windows.

<Signed>true</Signed>
<PackageId>com.example.platform</PackageId>
<Abi>arm64-v8a</Abi>
</ApplicationArtifact>
<ApplicationArtifact Include="$(MSBuildProjectDirectory)/artifacts/platform/android/MyArtifactApp-Signed.aab">
<PackageFormat>aab</PackageFormat>
<Signed>true</Signed>
<PackageId>com.example.platform</PackageId>
</ApplicationArtifact>
</ItemGroup>
</Target>
<Target Name="WriteSeededApplicationArtifactsMetadata" DependsOnTargets="SeedApplicationArtifacts">
<WriteLinesToFile
File="$(MSBuildProjectDirectory)/seeded-application-artifacts.txt"
Lines="@(ApplicationArtifact->'%(Filename)%(Extension)|%(PackageFormat)|%(ApplicationTitle)|%(ApplicationName)|%(ApplicationId)|%(ApplicationIdGuid)|%(ApplicationDisplayVersion)|%(ApplicationVersion)|%(Signed)|%(PackageId)|%(Abi)')"
Overwrite="true" />
</Target>
<Target Name="RecreatePublishedApplicationArtifacts" DependsOnTargets="SeedApplicationArtifacts">
<ItemGroup>
<_ApplicationArtifactForPublish Include="@(ApplicationArtifact)" />
<_ApplicationArtifactPublishCopy
Include="@(_ApplicationArtifactForPublish->'$(MSBuildProjectDirectory)/artifacts/publish/%(Filename)%(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)" />
</ItemGroup>
</Target>
<Target Name="WritePublishedApplicationArtifactsMetadata" DependsOnTargets="RecreatePublishedApplicationArtifacts">
<WriteLinesToFile
File="$(MSBuildProjectDirectory)/published-application-artifacts.txt"
Lines="@(ApplicationArtifact->'%(Filename)%(Extension)|%(PackageFormat)|%(ApplicationTitle)|%(ApplicationName)|%(ApplicationId)|%(ApplicationIdGuid)|%(ApplicationDisplayVersion)|%(ApplicationVersion)|%(Signed)|%(PackageId)|%(Abi)')"
Overwrite="true" />
</Target>
</Project>
""");

Assert.True(DotnetInternal.Build(projectFile, "Debug", target: "WriteSeededApplicationArtifactsMetadata", framework: $"{DotNetCurrent}-android", properties: buildProps, output: _output),
$"Project {Path.GetFileName(projectFile)} failed to write seeded ApplicationArtifact metadata. Check test output/attachments for errors.");
AssertApplicationArtifactMetadata(File.ReadAllLines(seededArtifactsFile));

Assert.True(DotnetInternal.Build(projectFile, "Debug", target: "WritePublishedApplicationArtifactsMetadata", framework: $"{DotNetCurrent}-android", properties: buildProps, output: _output),
$"Project {Path.GetFileName(projectFile)} failed to write published ApplicationArtifact metadata. Check test output/attachments for errors.");
AssertApplicationArtifactMetadata(File.ReadAllLines(publishedArtifactsFile));

static void AssertApplicationArtifactMetadata(string[] artifactLines)
{
Assert.Equal(2, artifactLines.Length);

AssertArtifact(
artifactLines.Single(line => line.StartsWith("MyArtifactApp-Signed.apk|apk|", StringComparison.Ordinal)),
"MyArtifactApp-Signed.apk",
"apk",
signed: "true",
packageId: "com.example.platform",
abi: "arm64-v8a");

AssertArtifact(
artifactLines.Single(line => line.StartsWith("MyArtifactApp-Signed.aab|aab|", StringComparison.Ordinal)),
"MyArtifactApp-Signed.aab",
"aab",
signed: "true",
packageId: "com.example.platform",
abi: "");
}

static void AssertArtifact(string artifactLine, string fileName, string packageFormat, string signed, string packageId, string abi)
{
var metadata = artifactLine.Split('|');

Assert.Equal(fileName, metadata[0]);
Assert.Equal(packageFormat, metadata[1]);
Assert.Equal("My Artifact App", metadata[2]);
Assert.Equal("My Artifact App", metadata[3]);
Assert.Equal("com.example.artifacts", metadata[4]);
Assert.Equal("11111111-2222-3333-4444-555555555555", metadata[5]);
Assert.Equal("2.3", metadata[6]);
Assert.Equal("42", metadata[7]);
Assert.Equal(signed, metadata[8]);
Assert.Equal(packageId, metadata[9]);
Assert.Equal(abi, metadata[10]);
}
}

[Theory]
// Parameters: short name, target framework, build config, use pack target, additionalDotNetBuildParams
// [InlineData("maui", DotNetPrevious, "Debug", false, "")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
<ProjectCapability Include="MauiEssentials" Condition=" '$(UseMaui)' == 'true' or '$(UseMauiEssentials)' == 'true' " />
</ItemGroup>

<ItemDefinitionGroup>
<ApplicationArtifact>
<ApplicationId Condition="'$(ApplicationId)' != ''">$(ApplicationId)</ApplicationId>
<ApplicationIdGuid Condition="'$(ApplicationIdGuid)' != ''">$(ApplicationIdGuid)</ApplicationIdGuid>
<ApplicationName Condition="'$(ApplicationTitle)' != ''">$(ApplicationTitle)</ApplicationName>
<ApplicationTitle Condition="'$(ApplicationTitle)' != ''">$(ApplicationTitle)</ApplicationTitle>
<ApplicationDisplayVersion Condition="'$(ApplicationDisplayVersion)' != ''">$(ApplicationDisplayVersion)</ApplicationDisplayVersion>
<ApplicationVersion Condition="'$(ApplicationVersion)' != ''">$(ApplicationVersion)</ApplicationVersion>
</ApplicationArtifact>
</ItemDefinitionGroup>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this actually go in dotnet/android and dotnet/macios?

What if you are an Uno or MonoGame project? ... then what about WindowsAppSDK?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think maybe the android/macios stuff can get pushed down into those repos - i'll make PR's for that. For windows I'm thinking maybe another package that could be added to any windowsappsdk app that achieves the same, though I'll check with someone on that team if they might consider just a PR to their repo directly to add this too.


<!-- SingleProject-specific features -->
<ItemGroup Condition=" '$(SingleProject)' == 'true' ">
<ProjectCapability Include="Msix" />
Expand Down
Loading