From be8b896314726265daf453a12e4b7b174306be55 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Wed, 17 Jun 2026 11:00:44 +0200 Subject: [PATCH 1/5] Fix Resizetizer font and splash incremental outputs Track generated font and splash outputs alongside their stamp files so missing intermediates invalidate the narrow Resizetizer targets instead of producing packages without resources. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Maui.Resizetizer.After.targets | 39 ++++++++- .../ResizetizerTests.cs | 82 +++++++++++++++++++ 2 files changed, 119 insertions(+), 2 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..9eb0dc457640 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,8 +75,10 @@ <_ResizetizerOutputsFile>$(_ResizetizerIntermediateOutputPath)mauiimage.outputs <_ResizetizerStampFile>$(_ResizetizerIntermediateOutputPath)mauiimage.stamp <_MauiFontInputsFile>$(_ResizetizerIntermediateOutputPath)mauifont.inputs + <_MauiFontOutputsFile>$(_ResizetizerIntermediateOutputPath)mauifont.outputs <_MauiFontStampFile>$(_ResizetizerIntermediateOutputPath)mauifont.stamp <_MauiSplashInputsFile>$(_ResizetizerIntermediateOutputPath)mauisplash.inputs + <_MauiSplashOutputsFile>$(_ResizetizerIntermediateOutputPath)mauisplash.outputs <_MauiSplashStampFile>$(_ResizetizerIntermediateOutputPath)mauisplash.stamp <_MauiManifestStampFile>$(_ResizetizerIntermediateOutputPath)mauimanifest.stamp @@ -125,6 +127,7 @@ ResizetizeCollectItems; ProcessMauiAssets; ProcessMauiSplashScreens; + _ReadMauiFontOutputs; @@ -399,7 +402,8 @@ + Outputs="$(_MauiSplashStampFile);$(_MauiSplashOutputsFile);@(_MauiSplashOutputs)" + DependsOnTargets="_ReadMauiSplashOutputs"> <_MauiHasSplashScreens>false @@ -508,18 +512,33 @@ + + + + + + + + + + @@ -595,16 +614,32 @@ + + + + + + + + + + + diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs index fefbe048b1ed..25892982eb47 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs @@ -1,3 +1,6 @@ +using System.IO.Compression; +using System.Text.RegularExpressions; + namespace Microsoft.Maui.IntegrationTests; [Trait("Category", "Build")] @@ -99,4 +102,83 @@ public void CollectsAssets(string id, string libid, bool unpackaged) Assert.True(File.Exists(Path.Combine(appDir, $"obj\\Debug\\{DotNetCurrent}-windows10.0.19041.0\\win-x64\\resizetizer\\r\\the_image.scale-100.png")), "Windows was missing the image file."); } + + [Fact] + public void AndroidPackageRegeneratesFontsAndSplashWhenIntermediateOutputsAreMissing() + { + SetTestIdentifier("AndroidMissingResizetizerOutputs"); + + var projectDir = TestDirectory; + var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); + var framework = $"{DotNetCurrent}-android"; + const string config = "Debug"; + + Assert.True(DotnetInternal.New("maui", projectDir, DotNetCurrent, output: _output), + $"Unable to create template maui. Check test output for errors."); + + StripNonAndroidTfms(projectFile, framework); + + var buildProps = BuildProps; + buildProps.Add("AndroidPackageFormat=apk"); + + Assert.True(DotnetInternal.Build(projectFile, config, target: "SignAndroidPackage", framework: framework, properties: buildProps, output: _output), + $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); + + AssertAndroidPackageContainsFontsAndSplash(projectDir, config, framework); + + var intermediateOutputPath = Path.Combine(projectDir, "obj", config, framework); + DeleteDirectory(Path.Combine(intermediateOutputPath, "resizetizer", "f")); + DeleteDirectory(Path.Combine(intermediateOutputPath, "resizetizer", "sp")); + DeleteDirectory(Path.Combine(intermediateOutputPath, "android")); + DeleteDirectory(Path.Combine(intermediateOutputPath, "lp")); + DeleteDirectory(Path.Combine(intermediateOutputPath, "stamp")); + + foreach (var package in Directory.GetFiles(projectDir, "*.apk", SearchOption.AllDirectories)) + File.Delete(package); + + Assert.True(DotnetInternal.Build(projectFile, config, target: "SignAndroidPackage", framework: framework, properties: buildProps, output: _output), + $"Project {Path.GetFileName(projectFile)} failed to rebuild. Check test output/attachments for errors."); + + AssertAndroidPackageContainsFontsAndSplash(projectDir, config, framework); + } + + static void AssertAndroidPackageContainsFontsAndSplash(string projectDir, string config, string framework) + { + var apkSearchDir = Path.Combine(projectDir, "bin", config, framework); + var apkPath = Directory.GetFiles(apkSearchDir, "*-Signed.apk", SearchOption.AllDirectories) + .OrderByDescending(File.GetLastWriteTimeUtc) + .FirstOrDefault(); + + Assert.True(apkPath is not null, $"No signed APK found in {apkSearchDir}"); + + using var apk = ZipFile.OpenRead(apkPath!); + var entries = apk.Entries + .Select(entry => entry.FullName) + .ToHashSet(StringComparer.Ordinal); + + Assert.Contains("assets/OpenSans-Regular.ttf", entries); + Assert.Contains("assets/OpenSans-Semibold.ttf", entries); + Assert.True( + entries.Any(entry => entry.StartsWith("res/drawable-", StringComparison.Ordinal) && + entry.EndsWith("/splash.png", StringComparison.Ordinal)), + $"APK {apkPath} does not contain generated splash screen raster assets."); + } + + static void DeleteDirectory(string path) + { + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + + static void StripNonAndroidTfms(string projectFile, string androidTfm) + { + var content = File.ReadAllText(projectFile); + content = Regex.Replace(content, + @"\s*[^<]*", + ""); + content = Regex.Replace(content, + @"[^<]*", + $"{androidTfm}"); + File.WriteAllText(projectFile, content); + } } From 8dca939d61fd9be5b125c2598fda312a8c5f87e2 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Wed, 17 Jun 2026 11:44:32 +0200 Subject: [PATCH 2/5] Clean up Resizetizer output manifest writes Write font and splash output manifests normally instead of combining WriteOnlyWhenDifferent with an unconditional Touch. The manifests are declared target outputs, so their timestamps should advance when the target runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../buildTransitive/Microsoft.Maui.Resizetizer.After.targets | 4 ---- 1 file changed, 4 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 9eb0dc457640..d373cba3803a 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 @@ -516,10 +516,8 @@ File="$(_MauiSplashOutputsFile)" Lines="@(_MauiSplashAssets->'%(FullPath)')" Overwrite="true" - WriteOnlyWhenDifferent="true" /> - @@ -622,9 +620,7 @@ File="$(_MauiFontOutputsFile)" Lines="@(_MauiFontCopied->'%(FullPath)');@(_MauiFontPListFiles->'%(FullPath)')" Overwrite="true" - WriteOnlyWhenDifferent="true" /> - From 4979d7f33e0a2cc64d50884368b27eb29bba3466 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Thu, 18 Jun 2026 10:46:11 +0200 Subject: [PATCH 3/5] Address Resizetizer output review feedback Stop creating separate font and splash stamp files now that the generated output manifests drive incremental checks, add a splash DependsOnTargets extension point, and verify no-op builds skip font and splash targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Maui.Resizetizer.After.targets | 20 +++---- .../ResizetizerTests.cs | 60 ++++++++++++++++--- 2 files changed, 59 insertions(+), 21 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 d373cba3803a..aa1b1bd70033 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,10 +76,8 @@ <_ResizetizerStampFile>$(_ResizetizerIntermediateOutputPath)mauiimage.stamp <_MauiFontInputsFile>$(_ResizetizerIntermediateOutputPath)mauifont.inputs <_MauiFontOutputsFile>$(_ResizetizerIntermediateOutputPath)mauifont.outputs - <_MauiFontStampFile>$(_ResizetizerIntermediateOutputPath)mauifont.stamp <_MauiSplashInputsFile>$(_ResizetizerIntermediateOutputPath)mauisplash.inputs <_MauiSplashOutputsFile>$(_ResizetizerIntermediateOutputPath)mauisplash.outputs - <_MauiSplashStampFile>$(_ResizetizerIntermediateOutputPath)mauisplash.stamp <_MauiManifestStampFile>$(_ResizetizerIntermediateOutputPath)mauimanifest.stamp <_ResizetizerIntermediateOutputRoot>$(_ResizetizerIntermediateOutputPath)resizetizer\ @@ -122,6 +120,10 @@ ProcessMauiSplashScreens; _ReadResizetizeImagesOutputs; + + $(ProcessMauiSplashScreensDependsOnTargets); + _ReadMauiSplashOutputs; + $(ProcessMauiFontsDependsOnTargets); ResizetizeCollectItems; @@ -402,8 +404,8 @@ + Outputs="$(_MauiSplashOutputsFile);@(_MauiSplashOutputs)" + DependsOnTargets="$(ProcessMauiSplashScreensDependsOnTargets)"> <_MauiHasSplashScreens>false @@ -509,9 +511,7 @@ - - - @@ -536,7 +535,7 @@ @@ -614,17 +613,14 @@ - - - + - diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs index 25892982eb47..a104e94d21ae 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs @@ -1,5 +1,6 @@ using System.IO.Compression; -using System.Text.RegularExpressions; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging.StructuredLogger; namespace Microsoft.Maui.IntegrationTests; @@ -140,6 +141,14 @@ public void AndroidPackageRegeneratesFontsAndSplashWhenIntermediateOutputsAreMis $"Project {Path.GetFileName(projectFile)} failed to rebuild. Check test output/attachments for errors."); AssertAndroidPackageContainsFontsAndSplash(projectDir, config, framework); + + var noOpBinlogPath = Path.Combine(projectDir, "no-op.binlog"); + Assert.True(DotnetInternal.Build(projectFile, config, target: "SignAndroidPackage", framework: framework, properties: buildProps, binlogPath: noOpBinlogPath, output: _output), + $"Project {Path.GetFileName(projectFile)} failed to no-op rebuild. Check test output/attachments for errors."); + + AssertTargetSkipped(noOpBinlogPath, "ProcessMauiFonts"); + AssertTargetSkipped(noOpBinlogPath, "ProcessMauiSplashScreens"); + AssertAndroidPackageContainsFontsAndSplash(projectDir, config, framework); } static void AssertAndroidPackageContainsFontsAndSplash(string projectDir, string config, string framework) @@ -172,13 +181,46 @@ static void DeleteDirectory(string path) static void StripNonAndroidTfms(string projectFile, string androidTfm) { - var content = File.ReadAllText(projectFile); - content = Regex.Replace(content, - @"\s*[^<]*", - ""); - content = Regex.Replace(content, - @"[^<]*", - $"{androidTfm}"); - File.WriteAllText(projectFile, content); + var tempFile = Path.GetTempFileName(); + try + { + using (var reader = File.OpenText(projectFile)) + using (var writer = File.CreateText(tempFile)) + { + string? line; + while ((line = reader.ReadLine()) is not null) + { + if (line.Contains("", StringComparison.Ordinal)) + { + var indentation = line[..line.IndexOf('<', StringComparison.Ordinal)]; + line = $"{indentation}{androidTfm}"; + } + + writer.WriteLine(line); + } + } + + File.Copy(tempFile, projectFile, overwrite: true); + } + finally + { + File.Delete(tempFile); + } + } + + static void AssertTargetSkipped(string binlogPath, string targetName) + { + var skipped = new BinLogReader() + .ReadRecords(binlogPath) + .Any(record => record.Args is BuildMessageEventArgs { Message: string message } && + message.Contains($"Skipping target \"{targetName}\"", StringComparison.Ordinal)); + + Assert.True(skipped, $"Expected target '{targetName}' to be skipped. See binlog: {binlogPath}"); } } From df49e70a25c0760d301ab85ce9d46eda66cb2a42 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Fri, 19 Jun 2026 09:56:17 +0200 Subject: [PATCH 4/5] Address Resizetizer review feedback Use a plain multi-target build in the Resizetizer regression test, covering the font and splash intermediate outputs for each built platform instead of Android packaging only. Also make font and splash output manifest writes defensive and consistent with the image output manifest pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Maui.Resizetizer.After.targets | 5 + .../ResizetizerTests.cs | 146 ++++++++---------- 2 files changed, 70 insertions(+), 81 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 aa1b1bd70033..d2b59d35e6a0 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 @@ -516,7 +516,9 @@ File="$(_MauiSplashOutputsFile)" Lines="@(_MauiSplashAssets->'%(FullPath)')" Overwrite="true" + WriteOnlyWhenDifferent="true" /> + @@ -613,11 +615,14 @@ + + diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs index a104e94d21ae..b8623d987f4f 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs @@ -1,4 +1,3 @@ -using System.IO.Compression; using Microsoft.Build.Framework; using Microsoft.Build.Logging.StructuredLogger; @@ -105,122 +104,107 @@ public void CollectsAssets(string id, string libid, bool unpackaged) } [Fact] - public void AndroidPackageRegeneratesFontsAndSplashWhenIntermediateOutputsAreMissing() + public void BuildRegeneratesFontsAndSplashWhenIntermediateOutputsAreMissing() { - SetTestIdentifier("AndroidMissingResizetizerOutputs"); + SetTestIdentifier("MissingResizetizerOutputs"); var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - var framework = $"{DotNetCurrent}-android"; const string config = "Debug"; Assert.True(DotnetInternal.New("maui", projectDir, DotNetCurrent, output: _output), $"Unable to create template maui. Check test output for errors."); - StripNonAndroidTfms(projectFile, framework); - - var buildProps = BuildProps; - buildProps.Add("AndroidPackageFormat=apk"); - - Assert.True(DotnetInternal.Build(projectFile, config, target: "SignAndroidPackage", framework: framework, properties: buildProps, output: _output), + Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); - AssertAndroidPackageContainsFontsAndSplash(projectDir, config, framework); - - var intermediateOutputPath = Path.Combine(projectDir, "obj", config, framework); - DeleteDirectory(Path.Combine(intermediateOutputPath, "resizetizer", "f")); - DeleteDirectory(Path.Combine(intermediateOutputPath, "resizetizer", "sp")); - DeleteDirectory(Path.Combine(intermediateOutputPath, "android")); - DeleteDirectory(Path.Combine(intermediateOutputPath, "lp")); - DeleteDirectory(Path.Combine(intermediateOutputPath, "stamp")); + var intermediateOutputRoots = GetResizetizerOutputRoots(projectDir, config); + AssertBuiltTargetPlatforms(intermediateOutputRoots); + AssertIntermediateOutputsExist(intermediateOutputRoots); - foreach (var package in Directory.GetFiles(projectDir, "*.apk", SearchOption.AllDirectories)) - File.Delete(package); + foreach (var intermediateOutputRoot in intermediateOutputRoots) + { + DeleteDirectory(Path.Combine(intermediateOutputRoot, "resizetizer", "f")); + DeleteDirectory(Path.Combine(intermediateOutputRoot, "resizetizer", "sp")); + } - Assert.True(DotnetInternal.Build(projectFile, config, target: "SignAndroidPackage", framework: framework, properties: buildProps, output: _output), + Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, output: _output), $"Project {Path.GetFileName(projectFile)} failed to rebuild. Check test output/attachments for errors."); - AssertAndroidPackageContainsFontsAndSplash(projectDir, config, framework); + AssertIntermediateOutputsExist(intermediateOutputRoots); var noOpBinlogPath = Path.Combine(projectDir, "no-op.binlog"); - Assert.True(DotnetInternal.Build(projectFile, config, target: "SignAndroidPackage", framework: framework, properties: buildProps, binlogPath: noOpBinlogPath, output: _output), + Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, binlogPath: noOpBinlogPath, output: _output), $"Project {Path.GetFileName(projectFile)} failed to no-op rebuild. Check test output/attachments for errors."); - AssertTargetSkipped(noOpBinlogPath, "ProcessMauiFonts"); - AssertTargetSkipped(noOpBinlogPath, "ProcessMauiSplashScreens"); - AssertAndroidPackageContainsFontsAndSplash(projectDir, config, framework); + AssertTargetSkipped(noOpBinlogPath, "ProcessMauiFonts", intermediateOutputRoots.Count); + AssertTargetSkipped(noOpBinlogPath, "ProcessMauiSplashScreens", intermediateOutputRoots.Count); + AssertIntermediateOutputsExist(intermediateOutputRoots); } - static void AssertAndroidPackageContainsFontsAndSplash(string projectDir, string config, string framework) + static IReadOnlyList GetResizetizerOutputRoots(string projectDir, string config) { - var apkSearchDir = Path.Combine(projectDir, "bin", config, framework); - var apkPath = Directory.GetFiles(apkSearchDir, "*-Signed.apk", SearchOption.AllDirectories) - .OrderByDescending(File.GetLastWriteTimeUtc) - .FirstOrDefault(); - - Assert.True(apkPath is not null, $"No signed APK found in {apkSearchDir}"); - - using var apk = ZipFile.OpenRead(apkPath!); - var entries = apk.Entries - .Select(entry => entry.FullName) - .ToHashSet(StringComparer.Ordinal); - - Assert.Contains("assets/OpenSans-Regular.ttf", entries); - Assert.Contains("assets/OpenSans-Semibold.ttf", entries); - Assert.True( - entries.Any(entry => entry.StartsWith("res/drawable-", StringComparison.Ordinal) && - entry.EndsWith("/splash.png", StringComparison.Ordinal)), - $"APK {apkPath} does not contain generated splash screen raster assets."); + var intermediateOutputPath = Path.Combine(projectDir, "obj", config); + var outputRoots = Directory + .GetFiles(intermediateOutputPath, "mauifont.outputs", SearchOption.AllDirectories) + .Select(Path.GetDirectoryName) + .Where(root => root is not null && File.Exists(Path.Combine(root, "mauisplash.outputs"))) + .Cast() + .OrderBy(root => root, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + Assert.NotEmpty(outputRoots); + return outputRoots; } - static void DeleteDirectory(string path) + static void AssertBuiltTargetPlatforms(IReadOnlyList intermediateOutputRoots) { - if (Directory.Exists(path)) - Directory.Delete(path, recursive: true); + Assert.Contains(intermediateOutputRoots, root => ContainsTargetFramework(root, $"{DotNetCurrent}-android")); + Assert.Contains(intermediateOutputRoots, root => ContainsTargetFramework(root, $"{DotNetCurrent}-ios")); + Assert.Contains(intermediateOutputRoots, root => ContainsTargetFramework(root, $"{DotNetCurrent}-maccatalyst")); + + if (TestEnvironment.IsWindows) + Assert.Contains(intermediateOutputRoots, root => ContainsTargetFramework(root, $"{DotNetCurrent}-windows")); } - static void StripNonAndroidTfms(string projectFile, string androidTfm) + static bool ContainsTargetFramework(string path, string targetFramework) => + path.Contains(targetFramework, StringComparison.OrdinalIgnoreCase); + + static void AssertIntermediateOutputsExist(IReadOnlyList intermediateOutputRoots) { - var tempFile = Path.GetTempFileName(); - try + foreach (var intermediateOutputRoot in intermediateOutputRoots) { - using (var reader = File.OpenText(projectFile)) - using (var writer = File.CreateText(tempFile)) + var fontsDir = Path.Combine(intermediateOutputRoot, "resizetizer", "f"); + Assert.True(File.Exists(Path.Combine(fontsDir, "OpenSans-Regular.ttf")), + $"Missing OpenSans-Regular.ttf in {fontsDir}."); + Assert.True(File.Exists(Path.Combine(fontsDir, "OpenSans-Semibold.ttf")), + $"Missing OpenSans-Semibold.ttf in {fontsDir}."); + + if (!ContainsTargetFramework(intermediateOutputRoot, $"{DotNetCurrent}-maccatalyst")) { - string? line; - while ((line = reader.ReadLine()) is not null) - { - if (line.Contains("", StringComparison.Ordinal)) - { - var indentation = line[..line.IndexOf('<', StringComparison.Ordinal)]; - line = $"{indentation}{androidTfm}"; - } - - writer.WriteLine(line); - } + var splashDir = Path.Combine(intermediateOutputRoot, "resizetizer", "sp"); + Assert.True( + Directory.Exists(splashDir) && Directory.EnumerateFiles(splashDir, "*", SearchOption.AllDirectories).Any(), + $"Missing generated splash screen files in {splashDir}."); } - - File.Copy(tempFile, projectFile, overwrite: true); - } - finally - { - File.Delete(tempFile); } } - static void AssertTargetSkipped(string binlogPath, string targetName) + static void DeleteDirectory(string path) + { + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + + static void AssertTargetSkipped(string binlogPath, string targetName, int minimumSkipCount) { - var skipped = new BinLogReader() + var skipCount = new BinLogReader() .ReadRecords(binlogPath) - .Any(record => record.Args is BuildMessageEventArgs { Message: string message } && - message.Contains($"Skipping target \"{targetName}\"", StringComparison.Ordinal)); + .Count(record => record.Args is BuildMessageEventArgs { Message: string message } && + message.Contains($"Skipping target \"{targetName}\"", StringComparison.Ordinal) && + message.Contains("because all output files are up-to-date", StringComparison.OrdinalIgnoreCase)); - Assert.True(skipped, $"Expected target '{targetName}' to be skipped. See binlog: {binlogPath}"); + Assert.True(skipCount >= minimumSkipCount, + $"Expected target '{targetName}' to be skipped at least {minimumSkipCount} times, but found {skipCount}. See binlog: {binlogPath}"); } } From cd570fc5fbefcba42b1f98f3635ae809306fbf54 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Mon, 22 Jun 2026 14:31:41 +0200 Subject: [PATCH 5/5] Avoid duplicate font output manifest entries Exclude the generated iOS MauiInfo.plist from the copied-font glob so it is only tracked through the explicit plist output item. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../buildTransitive/Microsoft.Maui.Resizetizer.After.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d2b59d35e6a0..e4ed9a712ceb 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 @@ -549,7 +549,7 @@ SkipUnchangedFiles="true" /> - <_MauiFontCopied Include="$(_MauiIntermediateFonts)*" /> + <_MauiFontCopied Include="$(_MauiIntermediateFonts)*" Exclude="$(_MauiIntermediateFonts)MauiInfo.plist" />