Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
20 changes: 20 additions & 0 deletions docs/design/ApplicationArtifact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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, MAUI participates through `$(GetApplicationArtifactsDependsOn)` and updates existing `@(ApplicationArtifact)` items with MAUI project metadata after the platform SDK `GetApplicationArtifacts` target has run `Build` and platform-produced items exist.

The MAUI metadata enrichment target adds these metadata values when the matching project properties are set:

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

`GetApplicationArtifacts` and `Publish` remain platform-owned result paths. Platform SDK `GetApplicationArtifacts` depends on `Build`, then executes targets appended to `$(GetApplicationArtifactsDependsOn)` before returning `@(ApplicationArtifact)` items. `Publish` uses the same post-`Build` extension path before returning items. Replacing `$(GetApplicationArtifactsDependsOn)` must not bypass platform build or platform artifact population; platform SDKs keep `Build` outside that extensibility property.
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,115 @@ 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 getArtifactsFile = Path.Combine(projectDir, "get-application-artifacts.txt");
var publishArtifactsFile = Path.Combine(projectDir, "publish-application-artifacts.txt");
var dependsOnFile = Path.Combine(projectDir, "get-application-artifacts-depends-on.txt");

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

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="$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\..\src\Workload\Microsoft.Maui.Sdk\Sdk\Microsoft.Maui.Sdk.After.targets'))" />

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.

This import is rooted at the generated test project's MSBuildThisFileDirectory and walks up three directories to find src/Workload. That happens to work for local fallback TestDirectory under the repo's bin/test-dir, but in Azure Pipelines TestEnvironment.GetTestDirectoryRoot() uses AGENT_TEMPDIRECTORY/test-dir, so ......\src does not point at the MAUI checkout and the new Build integration test cannot import Microsoft.Maui.Sdk.After.targets. The current CI failures in the Build integration lanes are consistent with this PR-introduced test path issue.

(found by: 5, 4)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[major] Build & MSBuild — This import is resolved relative to the generated test project. In CI TestDirectory is under $(AGENT_TEMPDIRECTORY)/test-dir, so $(MSBuildThisFileDirectory)......\src... points outside the checkout (for example /home/vsts/work/src/...) and the test fails with MSB4019 before it can validate the target. Resolve the import from TestEnvironment.GetMauiDirectory()/a repo-root property instead of the temp project location.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

❌ Error — This import is resolved relative to the generated temp test project via $(MSBuildThisFileDirectory), not relative to the MAUI repo. In CI, TestDirectory is under AGENT_TEMPDIRECTORY, so ..\..\..\src\Workload\... points outside /home/vsts/work/1/s and the test fails before exercising the PR fix. Please build the import path from TestEnvironment.GetMauiDirectory() or otherwise pass an absolute repo-rooted path into the generated project.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

❌ Error — This import resolves from the generated temp project directory, not the MAUI repo root. In CI TestDirectory is under the agent temp/test-dir (and locally under bin/test-dir), so ..\\..\\..\\src\\Workload... points at a non-existent sibling src tree and the test will fail before exercising the target. Build the path from TestEnvironment.GetMauiDirectory() or pass the repo-root path as an MSBuild property instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[critical] Build & MSBuild — Import path is broken on CI and on Linux/macOS

$(MSBuildThisFileDirectory) evaluates to the generated project directory under the integration-test temp root, not the MAUI repo root. On CI that makes ..\..\..\src\Workload\... resolve outside the checkout, and on Linux/macOS the backslashes are literal path characters rather than directory separators. The test can hard-fail before exercising the metadata target.

Resolve the targets file from C# using TestEnvironment.GetMauiDirectory() and inject/pass an absolute path with platform-correct separators.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ [major] Regression Prevention — Test double-imports a targets file already imported by the MAUI workload

The generated maui template project already imports Microsoft.Maui.Sdk.After.targets through the installed workload. The explicit source import can append _AddMauiApplicationArtifactMetadata a second time and makes the GetApplicationArtifactsDependsOn string assertion ambiguous. Prefer a minimal non-MAUI MSBuild project for this unit-style target test, or otherwise ensure the source targets are imported exactly once.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[critical] Build & MSBuild — This import resolves from $(MSBuildThisFileDirectory), which is the generated temp test project directory, not the repo root. In CI the path walks up from the temp project and can miss src/Workload/Microsoft.Maui.Sdk/Sdk/Microsoft.Maui.Sdk.After.targets, causing MSB4019 before the regression test can run. Use the repo MAUI directory/test environment path instead of a project-relative path.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[major] Build & MSBuild / integration test correctness — This import resolves relative to the temporary test project directory, not the MAUI repo root. In CI TestDirectory is under AGENT_TEMPDIRECTORY/test-dir, and locally it is under bin/test-dir, so $(MSBuildThisFileDirectory)..\..\..\src\Workload\... points at a non-existent src sibling and the test will fail before exercising the metadata target. It can also double-import Microsoft.Maui.Sdk.After.targets when the SDK already imports it. Use TestEnvironment.GetMauiDirectory() to build an absolute path if the test must import source targets, or rely on the SDK import from the built workload.

<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>
</ApplicationArtifact>
<ApplicationArtifact Include="$(MSBuildProjectDirectory)/artifacts/platform/apple/MyArtifactApp.app">
<PackageFormat>app</PackageFormat>
<IsDirectory>true</IsDirectory>
<PlatformName>iOS</PlatformName>
<BundleIdentifier>com.example.platform</BundleIdentifier>
</ApplicationArtifact>
</ItemGroup>
</Target>
<Target Name="WriteGetApplicationArtifactsMetadata" DependsOnTargets="SeedApplicationArtifacts;_AddMauiApplicationArtifactMetadata">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[moderate] Regression Prevention — The test target depends on _AddMauiApplicationArtifactMetadata directly, so it validates the private target body rather than the GetApplicationArtifactsDependsOn extension path this PR adds. A regression where the hook is not actually invoked by GetApplicationArtifacts/Publish could still pass because lines 157 and 167 call the target manually. Seed the item, invoke the targets through $(GetApplicationArtifactsDependsOn)/the public artifacts target, and assert the metadata after that path runs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Warning — The test drives the private _AddMauiApplicationArtifactMetadata target directly, so it can pass even if the public $(GetApplicationArtifactsDependsOn) hook is not honored by GetApplicationArtifacts/Publish or runs in the wrong order. Please exercise the public target path, or make this target depend on $(GetApplicationArtifactsDependsOn), so the regression test covers the actual integration contract added in Microsoft.Maui.Sdk.After.targets.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Warning — The test seeds fake ApplicationArtifact items and directly invokes the private _AddMauiApplicationArtifactMetadata target, so it does not prove the public GetApplicationArtifacts/publish extension path actually returns enriched platform artifacts. A regression in target ordering or platform artifact production could still ship while this test passes; prefer invoking the real public targets or depending through $(GetApplicationArtifactsDependsOn) in the harness.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ [major] Regression Prevention — Test calls the private implementation target instead of the public hook

WriteGetApplicationArtifactsMetadata and WritePublishApplicationArtifactsMetadata depend directly on _AddMauiApplicationArtifactMetadata, so the test would still pass if GetApplicationArtifactsDependsOn were removed, misspelled, or pointed at the wrong target. Invoke the public target/dependency chain and verify the enriched @(ApplicationArtifact) items are returned through that path.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[major] Regression Prevention/Test Coverage — The test directly depends on the private _AddMauiApplicationArtifactMetadata target here and again in WritePublishApplicationArtifactsMetadata, so it can pass even if the production GetApplicationArtifactsDependsOn hook is not actually used by GetApplicationArtifacts/Publish. Exercise the public depends-on path rather than invoking the private target directly.

<WriteLinesToFile
File="$(MSBuildProjectDirectory)/get-application-artifacts-depends-on.txt"
Lines="$(GetApplicationArtifactsDependsOn)"
Overwrite="true" />
<WriteLinesToFile
File="$(MSBuildProjectDirectory)/get-application-artifacts.txt"
Lines="@(ApplicationArtifact->'%(Filename)%(Extension)|%(PackageFormat)|%(ApplicationTitle)|%(ApplicationName)|%(ApplicationId)|%(ApplicationIdGuid)|%(ApplicationDisplayVersion)|%(ApplicationVersion)|%(Signed)|%(PackageId)|%(PlatformName)|%(BundleIdentifier)')"
Overwrite="true" />
</Target>
<Target Name="WritePublishApplicationArtifactsMetadata" DependsOnTargets="SeedApplicationArtifacts;_AddMauiApplicationArtifactMetadata">

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.

Both WriteGetApplicationArtifactsMetadata and WritePublishApplicationArtifactsMetadata depend directly on SeedApplicationArtifacts;_AddMauiApplicationArtifactMetadata, so they exercise the metadata target in isolation rather than through a real GetApplicationArtifacts/Publish invocation; the actual wiring is only checked indirectly by the string Assert.Contains on the depends-on file (line 178). The Publish variant therefore adds no coverage beyond the Get variant. This is understandable while the platform GetApplicationArtifacts target is unavailable in net11, but consider collapsing the duplicate target (or asserting the real wiring) once the platform producers land.

<WriteLinesToFile
File="$(MSBuildProjectDirectory)/publish-application-artifacts.txt"
Lines="@(ApplicationArtifact->'%(Filename)%(Extension)|%(PackageFormat)|%(ApplicationTitle)|%(ApplicationName)|%(ApplicationId)|%(ApplicationIdGuid)|%(ApplicationDisplayVersion)|%(ApplicationVersion)|%(Signed)|%(PackageId)|%(PlatformName)|%(BundleIdentifier)')"
Overwrite="true" />
</Target>
</Project>
""");

Assert.True(DotnetInternal.Build(projectFile, "Debug", target: "WriteGetApplicationArtifactsMetadata", framework: $"{DotNetCurrent}-android", properties: BuildProps, output: _output),
$"Project {Path.GetFileName(projectFile)} failed to write GetApplicationArtifacts metadata. Check test output/attachments for errors.");
Assert.Contains("_AddMauiApplicationArtifactMetadata", File.ReadAllText(dependsOnFile), StringComparison.Ordinal);
AssertApplicationArtifactMetadata(File.ReadAllLines(getArtifactsFile));

Assert.True(DotnetInternal.Build(projectFile, "Debug", target: "WritePublishApplicationArtifactsMetadata", framework: $"{DotNetCurrent}-android", properties: BuildProps, output: _output),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ [moderate] Logic and Correctness — Publish coverage duplicates the GetApplicationArtifacts path

The publish assertion builds a custom target with the same direct dependency on _AddMauiApplicationArtifactMetadata, so it does not verify whether the real Publish path triggers artifact enrichment. Either document and test that Publish consumes the same GetApplicationArtifactsDependsOn extension path, or add the missing publish hook and verify it through the real publish target.

$"Project {Path.GetFileName(projectFile)} failed to write Publish metadata. Check test output/attachments for errors.");
AssertApplicationArtifactMetadata(File.ReadAllLines(publishArtifactsFile));

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",
platformName: "",
bundleIdentifier: "");

AssertArtifact(
artifactLines.Single(line => line.StartsWith("MyArtifactApp.app|app|", StringComparison.Ordinal)),
"MyArtifactApp.app",
"app",
signed: "",
packageId: "",
platformName: "iOS",
bundleIdentifier: "com.example.platform");
}

static void AssertArtifact(string artifactLine, string fileName, string packageFormat, string signed, string packageId, string platformName, string bundleIdentifier)
{
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(platformName, metadata[10]);
Assert.Equal(bundleIdentifier, metadata[11]);
}
}

[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,13 @@
<ProjectCapability Include="MauiEssentials" Condition=" '$(UseMaui)' == 'true' or '$(UseMauiEssentials)' == 'true' " />
</ItemGroup>

<PropertyGroup>
<GetApplicationArtifactsDependsOn>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💡 [minor] Build & MSBuild — Consider guarding the dependency-chain extension

The property group appends _AddMauiApplicationArtifactMetadata for every project importing Microsoft.Maui.Sdk.After.targets, including configurations such as UseMauiEssentials=true that may never produce application artifacts. The target is a no-op when @(ApplicationArtifact) is empty, so this is likely harmless, but a UseMaui/artifact-intent condition or documentation would make the global extension explicit.

$(GetApplicationArtifactsDependsOn);
_AddMauiApplicationArtifactMetadata
</GetApplicationArtifactsDependsOn>
</PropertyGroup>

<!-- SingleProject-specific features -->
<ItemGroup Condition=" '$(SingleProject)' == 'true' ">
<ProjectCapability Include="Msix" />
Expand All @@ -30,4 +37,17 @@
<AndroidManifest Condition=" Exists('Platforms\Android\AndroidManifest.xml') ">Platforms\Android\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>

<Target Name="_AddMauiApplicationArtifactMetadata" Condition="'@(ApplicationArtifact)' != ''">
<ItemGroup>
<ApplicationArtifact Update="@(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>
</ItemGroup>
</Target>

</Project>
Loading