From 3de8728e6e42df9704bcfa9732a4a1e0986b4e7c Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Thu, 19 Mar 2026 11:09:41 +0100 Subject: [PATCH 1/4] Fix MauiFont assets missing on first build (Android) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProcessMauiFonts used Inputs/Outputs for incremental builds, which meant the target body was skipped when the stamp file was up-to-date. Unlike ResizetizeImages (which uses LibraryResourceDirectories — a persistent directory reference), ProcessMauiFonts adds individual AndroidAsset items that only exist when the target body executes. When skipped, the items were never added, causing fonts to be missing from the APK. Additionally, ProcessMauiFonts had no explicit ordering relative to _ComputeAndroidAssetsPaths (the Android SDK target that consumes AndroidAsset items). MSBuild AfterTargets/BeforeTargets are not transitive, so the existing AfterTargets on ResizetizeCollectItems did not guarantee execution before _ComputeAndroidAssetsPaths. Fix: - Remove Inputs/Outputs from ProcessMauiFonts (matches ProcessMauiAssets pattern which works reliably). The Copy task already uses SkipUnchangedFiles=true for file-level efficiency. - Add _ComputeAndroidAssetsPaths to ProcessMauiFontsBeforeTargets on Android for explicit ordering. - Remove unused _MauiFontStampFile property and related Touch/FileWrites. Fixes #23268 Related: #33092, #23659 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Maui.Resizetizer.After.targets | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets index feb26d3d5946..71c9dee8eafd 100644 --- a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets +++ b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets @@ -75,7 +75,6 @@ <_ResizetizerOutputsFile>$(_ResizetizerIntermediateOutputPath)mauiimage.outputs <_ResizetizerStampFile>$(_ResizetizerIntermediateOutputPath)mauiimage.stamp <_MauiFontInputsFile>$(_ResizetizerIntermediateOutputPath)mauifont.inputs - <_MauiFontStampFile>$(_ResizetizerIntermediateOutputPath)mauifont.stamp <_MauiSplashInputsFile>$(_ResizetizerIntermediateOutputPath)mauisplash.inputs <_MauiSplashStampFile>$(_ResizetizerIntermediateOutputPath)mauisplash.stamp <_MauiManifestStampFile>$(_ResizetizerIntermediateOutputPath)mauimanifest.stamp @@ -175,6 +174,11 @@ $(ProcessMauiFontsAfterTargets); ResizetizeCollectItems; + + + $(ProcessMauiFontsBeforeTargets); + _ComputeAndroidAssetsPaths; + @@ -518,8 +522,6 @@ @@ -595,12 +597,8 @@ - - - - + - From 5b9889062a2937b5cd6be7b8a7de28dbdc12fc58 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Thu, 19 Mar 2026 11:30:19 +0100 Subject: [PATCH 2/4] Add regression tests for ProcessMauiFonts target structure Structural xUnit tests that verify the MSBuild targets file has the correct configuration to prevent the font copy bug (#23268): - ProcessMauiFonts must not have Inputs/Outputs attributes - Android section must order ProcessMauiFonts before _ComputeAndroidAssetsPaths - ProcessMauiFonts follows same pattern as working ProcessMauiAssets All 4 tests fail when the fix is reverted, pass when applied. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UnitTests/ProcessMauiFontsTargetTests.cs | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs diff --git a/src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs new file mode 100644 index 000000000000..4c12157c11ab --- /dev/null +++ b/src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Maui.Resizetizer.Tests +{ + /// + /// Verifies the MSBuild target structure in Microsoft.Maui.Resizetizer.After.targets + /// to prevent regression of the "fonts missing on first build" bug (#23268). + /// + /// Root cause: ProcessMauiFonts used Inputs/Outputs for incremental builds. When + /// the target was skipped (stamp file up-to-date), platform item groups + /// (AndroidAsset, BundleResource, etc.) were never populated — causing fonts + /// to silently disappear from build output. + /// + public class ProcessMauiFontsTargetTests + { + static readonly XNamespace MSBuildNs = "http://schemas.microsoft.com/developer/msbuild/2003"; + + readonly ITestOutputHelper _output; + readonly XDocument _targetsDoc; + readonly string _targetsFilePath; + + public ProcessMauiFontsTargetTests(ITestOutputHelper output) + { + _output = output; + + // Navigate from test output dir (artifacts/bin/.../net10.0/) to repo root + var repoRoot = Path.GetFullPath(Path.Combine( + Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..")); + _targetsFilePath = Path.Combine(repoRoot, + "src", "SingleProject", "Resizetizer", "src", "nuget", + "buildTransitive", "Microsoft.Maui.Resizetizer.After.targets"); + + Assert.True(File.Exists(_targetsFilePath), + $"Targets file not found at: {_targetsFilePath}"); + _output.WriteLine($"Loading targets from: {_targetsFilePath}"); + + _targetsDoc = XDocument.Load(_targetsFilePath); + } + + XElement FindTarget(string name) => + _targetsDoc.Root! + .Elements(MSBuildNs + "Target") + .FirstOrDefault(t => t.Attribute("Name")?.Value == name); + + /// + /// ProcessMauiFonts must NOT have an Inputs attribute. When Inputs/Outputs are + /// present, MSBuild skips the target body on incremental builds — meaning the + /// ItemGroups that register fonts with each platform (AndroidAsset, + /// BundleResource, ContentWithTargetPath) are never evaluated. The fonts then + /// silently disappear from the build output. + /// + [Fact] + public void ProcessMauiFonts_ShouldNotHaveInputsAttribute() + { + var target = FindTarget("ProcessMauiFonts"); + Assert.NotNull(target); + + var inputs = target.Attribute("Inputs"); + Assert.True(inputs is null, + "ProcessMauiFonts must not use Inputs for incremental builds. " + + "When the target is skipped, platform item groups (AndroidAsset, " + + "BundleResource, etc.) are never populated, causing fonts to be " + + "missing from the build. See https://github.com/dotnet/maui/issues/23268"); + } + + /// + /// ProcessMauiFonts must NOT have an Outputs attribute (counterpart to Inputs). + /// + [Fact] + public void ProcessMauiFonts_ShouldNotHaveOutputsAttribute() + { + var target = FindTarget("ProcessMauiFonts"); + Assert.NotNull(target); + + var outputs = target.Attribute("Outputs"); + Assert.True(outputs is null, + "ProcessMauiFonts must not use Outputs for incremental builds. " + + "See https://github.com/dotnet/maui/issues/23268"); + } + + /// + /// On Android, ProcessMauiFonts adds fonts to @(AndroidAsset). The Android SDK's + /// _ComputeAndroidAssetsPaths target consumes @(AndroidAsset). Without explicit + /// ordering, MSBuild may run _ComputeAndroidAssetsPaths before ProcessMauiFonts, + /// causing fonts to be missing from the APK even when the target body executes. + /// + [Fact] + public void Android_ProcessMauiFontsBeforeTargets_ShouldIncludeComputeAndroidAssetsPaths() + { + // Find the Android-only PropertyGroup (not the combined IsCompatibleApp one) + var androidPG = _targetsDoc.Root! + .Elements(MSBuildNs + "PropertyGroup") + .FirstOrDefault(pg => + { + var cond = pg.Attribute("Condition")?.Value; + return cond != null + && cond.Contains("_ResizetizerIsAndroidApp", StringComparison.Ordinal) + && !cond.Contains("_ResizetizerIsiOSApp", StringComparison.Ordinal); + }); + + Assert.NotNull(androidPG); + + var beforeTargets = androidPG.Element(MSBuildNs + "ProcessMauiFontsBeforeTargets"); + Assert.NotNull(beforeTargets); + Assert.Contains("_ComputeAndroidAssetsPaths", beforeTargets.Value, StringComparison.Ordinal); + } + + /// + /// Verifies consistency: ProcessMauiFonts should follow the same pattern as + /// ProcessMauiAssets (which has always worked). ProcessMauiAssets does NOT use + /// Inputs/Outputs — confirming this is the correct pattern for targets that + /// populate platform item groups. + /// + [Fact] + public void ProcessMauiFonts_MatchesWorkingProcessMauiAssetsPattern() + { + // ProcessMauiAssets works correctly and does NOT use Inputs/Outputs + var assetsTarget = FindTarget("ProcessMauiAssets"); + Assert.NotNull(assetsTarget); + Assert.Null(assetsTarget.Attribute("Inputs")); + Assert.Null(assetsTarget.Attribute("Outputs")); + + // ProcessMauiFonts should follow the same pattern + var fontsTarget = FindTarget("ProcessMauiFonts"); + Assert.NotNull(fontsTarget); + Assert.Null(fontsTarget.Attribute("Inputs")); + Assert.Null(fontsTarget.Attribute("Outputs")); + } + } +} From 7886195dc145311a5643f3e89d512c56d2411493 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Thu, 19 Mar 2026 13:24:57 +0100 Subject: [PATCH 3/4] Fix splash screen incremental build bug (#33092) Remove Inputs/Outputs from ProcessMauiSplashScreens target to prevent splash screens from silently disappearing on incremental builds. Same root cause as the font fix (#23268): when MSBuild skips the target body, platform item groups (BundleResource, ContentWithTargetPath, etc.) are never populated. Safe to remove because the custom tasks (GenerateSplashAndroidResources, GenerateSplashAssets, GenerateSplashStoryboard) have built-in file-level incrementality via Resizer.IsUpToDate(). Also rename test class to ResizetizeTargetStructureTests and add 3 new splash screen regression tests (7 total). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Maui.Resizetizer.After.targets | 11 +- .../UnitTests/ProcessMauiFontsTargetTests.cs | 140 +++++++++++------- 2 files changed, 85 insertions(+), 66 deletions(-) diff --git a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets index 71c9dee8eafd..8084ff847526 100644 --- a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets +++ b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets @@ -76,7 +76,6 @@ <_ResizetizerStampFile>$(_ResizetizerIntermediateOutputPath)mauiimage.stamp <_MauiFontInputsFile>$(_ResizetizerIntermediateOutputPath)mauifont.inputs <_MauiSplashInputsFile>$(_ResizetizerIntermediateOutputPath)mauisplash.inputs - <_MauiSplashStampFile>$(_ResizetizerIntermediateOutputPath)mauisplash.stamp <_MauiManifestStampFile>$(_ResizetizerIntermediateOutputPath)mauimanifest.stamp <_ResizetizerIntermediateOutputRoot>$(_ResizetizerIntermediateOutputPath)resizetizer\ @@ -401,9 +400,7 @@ + Condition="'$(EnableMauiSplashScreenProcessing)' == 'true'"> <_MauiHasSplashScreens>false @@ -509,13 +506,9 @@ - - - - + - diff --git a/src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs index 4c12157c11ab..fd4caa41d73a 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs @@ -9,37 +9,37 @@ namespace Microsoft.Maui.Resizetizer.Tests { /// /// Verifies the MSBuild target structure in Microsoft.Maui.Resizetizer.After.targets - /// to prevent regression of the "fonts missing on first build" bug (#23268). + /// to prevent regression of the "fonts missing on first build" bug (#23268) and + /// the "splash screens randomly missing" bug (#33092). /// - /// Root cause: ProcessMauiFonts used Inputs/Outputs for incremental builds. When - /// the target was skipped (stamp file up-to-date), platform item groups - /// (AndroidAsset, BundleResource, etc.) were never populated — causing fonts - /// to silently disappear from build output. + /// Root cause: ProcessMauiFonts and ProcessMauiSplashScreens used Inputs/Outputs + /// for incremental builds. When the target was skipped (stamp file up-to-date), + /// platform item groups (AndroidAsset, BundleResource, etc.) were never populated + /// — causing fonts/splash screens to silently disappear from build output. /// - public class ProcessMauiFontsTargetTests + public class ResizetizeTargetStructureTests { static readonly XNamespace MSBuildNs = "http://schemas.microsoft.com/developer/msbuild/2003"; readonly ITestOutputHelper _output; readonly XDocument _targetsDoc; - readonly string _targetsFilePath; - public ProcessMauiFontsTargetTests(ITestOutputHelper output) + public ResizetizeTargetStructureTests(ITestOutputHelper output) { _output = output; // Navigate from test output dir (artifacts/bin/.../net10.0/) to repo root var repoRoot = Path.GetFullPath(Path.Combine( Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..")); - _targetsFilePath = Path.Combine(repoRoot, + var targetsFilePath = Path.Combine(repoRoot, "src", "SingleProject", "Resizetizer", "src", "nuget", "buildTransitive", "Microsoft.Maui.Resizetizer.After.targets"); - Assert.True(File.Exists(_targetsFilePath), - $"Targets file not found at: {_targetsFilePath}"); - _output.WriteLine($"Loading targets from: {_targetsFilePath}"); + Assert.True(File.Exists(targetsFilePath), + $"Targets file not found at: {targetsFilePath}"); + _output.WriteLine($"Loading targets from: {targetsFilePath}"); - _targetsDoc = XDocument.Load(_targetsFilePath); + _targetsDoc = XDocument.Load(targetsFilePath); } XElement FindTarget(string name) => @@ -47,62 +47,45 @@ XElement FindTarget(string name) => .Elements(MSBuildNs + "Target") .FirstOrDefault(t => t.Attribute("Name")?.Value == name); - /// - /// ProcessMauiFonts must NOT have an Inputs attribute. When Inputs/Outputs are - /// present, MSBuild skips the target body on incremental builds — meaning the - /// ItemGroups that register fonts with each platform (AndroidAsset, - /// BundleResource, ContentWithTargetPath) are never evaluated. The fonts then - /// silently disappear from the build output. - /// + XElement FindAndroidPropertyGroup() => + _targetsDoc.Root! + .Elements(MSBuildNs + "PropertyGroup") + .FirstOrDefault(pg => + { + var cond = pg.Attribute("Condition")?.Value; + return cond != null + && cond.Contains("_ResizetizerIsAndroidApp", StringComparison.Ordinal) + && !cond.Contains("_ResizetizerIsiOSApp", StringComparison.Ordinal); + }); + + // ────────────────────────────────────────────────────────── + // ProcessMauiFonts — #23268 + // ────────────────────────────────────────────────────────── + [Fact] public void ProcessMauiFonts_ShouldNotHaveInputsAttribute() { var target = FindTarget("ProcessMauiFonts"); Assert.NotNull(target); - - var inputs = target.Attribute("Inputs"); - Assert.True(inputs is null, + Assert.True(target.Attribute("Inputs") is null, "ProcessMauiFonts must not use Inputs for incremental builds. " + - "When the target is skipped, platform item groups (AndroidAsset, " + - "BundleResource, etc.) are never populated, causing fonts to be " + - "missing from the build. See https://github.com/dotnet/maui/issues/23268"); + "See https://github.com/dotnet/maui/issues/23268"); } - /// - /// ProcessMauiFonts must NOT have an Outputs attribute (counterpart to Inputs). - /// [Fact] public void ProcessMauiFonts_ShouldNotHaveOutputsAttribute() { var target = FindTarget("ProcessMauiFonts"); Assert.NotNull(target); - - var outputs = target.Attribute("Outputs"); - Assert.True(outputs is null, + Assert.True(target.Attribute("Outputs") is null, "ProcessMauiFonts must not use Outputs for incremental builds. " + "See https://github.com/dotnet/maui/issues/23268"); } - /// - /// On Android, ProcessMauiFonts adds fonts to @(AndroidAsset). The Android SDK's - /// _ComputeAndroidAssetsPaths target consumes @(AndroidAsset). Without explicit - /// ordering, MSBuild may run _ComputeAndroidAssetsPaths before ProcessMauiFonts, - /// causing fonts to be missing from the APK even when the target body executes. - /// [Fact] public void Android_ProcessMauiFontsBeforeTargets_ShouldIncludeComputeAndroidAssetsPaths() { - // Find the Android-only PropertyGroup (not the combined IsCompatibleApp one) - var androidPG = _targetsDoc.Root! - .Elements(MSBuildNs + "PropertyGroup") - .FirstOrDefault(pg => - { - var cond = pg.Attribute("Condition")?.Value; - return cond != null - && cond.Contains("_ResizetizerIsAndroidApp", StringComparison.Ordinal) - && !cond.Contains("_ResizetizerIsiOSApp", StringComparison.Ordinal); - }); - + var androidPG = FindAndroidPropertyGroup(); Assert.NotNull(androidPG); var beforeTargets = androidPG.Element(MSBuildNs + "ProcessMauiFontsBeforeTargets"); @@ -110,26 +93,69 @@ public void Android_ProcessMauiFontsBeforeTargets_ShouldIncludeComputeAndroidAss Assert.Contains("_ComputeAndroidAssetsPaths", beforeTargets.Value, StringComparison.Ordinal); } - /// - /// Verifies consistency: ProcessMauiFonts should follow the same pattern as - /// ProcessMauiAssets (which has always worked). ProcessMauiAssets does NOT use - /// Inputs/Outputs — confirming this is the correct pattern for targets that - /// populate platform item groups. - /// [Fact] public void ProcessMauiFonts_MatchesWorkingProcessMauiAssetsPattern() { - // ProcessMauiAssets works correctly and does NOT use Inputs/Outputs var assetsTarget = FindTarget("ProcessMauiAssets"); Assert.NotNull(assetsTarget); Assert.Null(assetsTarget.Attribute("Inputs")); Assert.Null(assetsTarget.Attribute("Outputs")); - // ProcessMauiFonts should follow the same pattern var fontsTarget = FindTarget("ProcessMauiFonts"); Assert.NotNull(fontsTarget); Assert.Null(fontsTarget.Attribute("Inputs")); Assert.Null(fontsTarget.Attribute("Outputs")); } + + // ────────────────────────────────────────────────────────── + // ProcessMauiSplashScreens — #33092 + // ────────────────────────────────────────────────────────── + + /// + /// ProcessMauiSplashScreens must NOT have Inputs. Same root cause as fonts: + /// when skipped, platform item groups (LibraryResourceDirectories, + /// BundleResource, ContentWithTargetPath) may not be populated, causing + /// splash screens to randomly disappear from builds. + /// The custom tasks (GenerateSplashAndroidResources, etc.) have built-in + /// file-level incrementality via Resizer.IsUpToDate(), so removing + /// Inputs/Outputs does not cause expensive re-rendering. + /// + [Fact] + public void ProcessMauiSplashScreens_ShouldNotHaveInputsAttribute() + { + var target = FindTarget("ProcessMauiSplashScreens"); + Assert.NotNull(target); + Assert.True(target.Attribute("Inputs") is null, + "ProcessMauiSplashScreens must not use Inputs for incremental builds. " + + "See https://github.com/dotnet/maui/issues/33092"); + } + + [Fact] + public void ProcessMauiSplashScreens_ShouldNotHaveOutputsAttribute() + { + var target = FindTarget("ProcessMauiSplashScreens"); + Assert.NotNull(target); + Assert.True(target.Attribute("Outputs") is null, + "ProcessMauiSplashScreens must not use Outputs for incremental builds. " + + "See https://github.com/dotnet/maui/issues/33092"); + } + + /// + /// All three content-producing targets should follow the same pattern + /// as ProcessMauiAssets (no Inputs/Outputs). + /// + [Fact] + public void AllContentTargets_FollowProcessMauiAssetsPattern() + { + foreach (var targetName in new[] { "ProcessMauiAssets", "ProcessMauiFonts", "ProcessMauiSplashScreens" }) + { + var target = FindTarget(targetName); + Assert.NotNull(target); + Assert.True(target.Attribute("Inputs") is null, + $"{targetName} must not use Inputs. See #23268 / #33092"); + Assert.True(target.Attribute("Outputs") is null, + $"{targetName} must not use Outputs. See #23268 / #33092"); + } + } } } From 6e1efe9903292d08a1aa1f887fd7d850ab1391a4 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Thu, 19 Mar 2026 14:13:00 +0100 Subject: [PATCH 4/4] Rename test file to match class name Rename ProcessMauiFontsTargetTests.cs to ResizetizeTargetStructureTests.cs to match the contained class name and reflect its broader scope covering both font and splash screen target structure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...sMauiFontsTargetTests.cs => ResizetizeTargetStructureTests.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/SingleProject/Resizetizer/test/UnitTests/{ProcessMauiFontsTargetTests.cs => ResizetizeTargetStructureTests.cs} (100%) diff --git a/src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/ResizetizeTargetStructureTests.cs similarity index 100% rename from src/SingleProject/Resizetizer/test/UnitTests/ProcessMauiFontsTargetTests.cs rename to src/SingleProject/Resizetizer/test/UnitTests/ResizetizeTargetStructureTests.cs