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..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 @@ -75,9 +75,9 @@ <_ResizetizerOutputsFile>$(_ResizetizerIntermediateOutputPath)mauiimage.outputs <_ResizetizerStampFile>$(_ResizetizerIntermediateOutputPath)mauiimage.stamp <_MauiFontInputsFile>$(_ResizetizerIntermediateOutputPath)mauifont.inputs - <_MauiFontStampFile>$(_ResizetizerIntermediateOutputPath)mauifont.stamp + <_MauiFontOutputsFile>$(_ResizetizerIntermediateOutputPath)mauifont.outputs <_MauiSplashInputsFile>$(_ResizetizerIntermediateOutputPath)mauisplash.inputs - <_MauiSplashStampFile>$(_ResizetizerIntermediateOutputPath)mauisplash.stamp + <_MauiSplashOutputsFile>$(_ResizetizerIntermediateOutputPath)mauisplash.outputs <_MauiManifestStampFile>$(_ResizetizerIntermediateOutputPath)mauimanifest.stamp <_ResizetizerIntermediateOutputRoot>$(_ResizetizerIntermediateOutputPath)resizetizer\ @@ -120,11 +120,16 @@ ProcessMauiSplashScreens; _ReadResizetizeImagesOutputs; + + $(ProcessMauiSplashScreensDependsOnTargets); + _ReadMauiSplashOutputs; + $(ProcessMauiFontsDependsOnTargets); ResizetizeCollectItems; ProcessMauiAssets; ProcessMauiSplashScreens; + _ReadMauiFontOutputs; @@ -399,7 +404,8 @@ + Outputs="$(_MauiSplashOutputsFile);@(_MauiSplashOutputs)" + DependsOnTargets="$(ProcessMauiSplashScreensDependsOnTargets)"> <_MauiHasSplashScreens>false @@ -505,21 +511,33 @@ - - + + + - + + + + + + + @@ -531,7 +549,7 @@ SkipUnchangedFiles="true" /> - <_MauiFontCopied Include="$(_MauiIntermediateFonts)*" /> + <_MauiFontCopied Include="$(_MauiIntermediateFonts)*" Exclude="$(_MauiIntermediateFonts)MauiInfo.plist" /> @@ -595,16 +613,30 @@ - - + - + + + + + - + + + + + + + diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs index fefbe048b1ed..b8623d987f4f 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs @@ -1,3 +1,6 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Logging.StructuredLogger; + namespace Microsoft.Maui.IntegrationTests; [Trait("Category", "Build")] @@ -99,4 +102,109 @@ 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 BuildRegeneratesFontsAndSplashWhenIntermediateOutputsAreMissing() + { + SetTestIdentifier("MissingResizetizerOutputs"); + + var projectDir = TestDirectory; + var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); + const string config = "Debug"; + + Assert.True(DotnetInternal.New("maui", projectDir, DotNetCurrent, output: _output), + $"Unable to create template maui. Check test output for errors."); + + Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, output: _output), + $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); + + var intermediateOutputRoots = GetResizetizerOutputRoots(projectDir, config); + AssertBuiltTargetPlatforms(intermediateOutputRoots); + AssertIntermediateOutputsExist(intermediateOutputRoots); + + foreach (var intermediateOutputRoot in intermediateOutputRoots) + { + DeleteDirectory(Path.Combine(intermediateOutputRoot, "resizetizer", "f")); + DeleteDirectory(Path.Combine(intermediateOutputRoot, "resizetizer", "sp")); + } + + Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, output: _output), + $"Project {Path.GetFileName(projectFile)} failed to rebuild. Check test output/attachments for errors."); + + AssertIntermediateOutputsExist(intermediateOutputRoots); + + var noOpBinlogPath = Path.Combine(projectDir, "no-op.binlog"); + 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", intermediateOutputRoots.Count); + AssertTargetSkipped(noOpBinlogPath, "ProcessMauiSplashScreens", intermediateOutputRoots.Count); + AssertIntermediateOutputsExist(intermediateOutputRoots); + } + + static IReadOnlyList GetResizetizerOutputRoots(string projectDir, string config) + { + 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 AssertBuiltTargetPlatforms(IReadOnlyList intermediateOutputRoots) + { + 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 bool ContainsTargetFramework(string path, string targetFramework) => + path.Contains(targetFramework, StringComparison.OrdinalIgnoreCase); + + static void AssertIntermediateOutputsExist(IReadOnlyList intermediateOutputRoots) + { + foreach (var intermediateOutputRoot in intermediateOutputRoots) + { + 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")) + { + 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}."); + } + } + } + + static void DeleteDirectory(string path) + { + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + + static void AssertTargetSkipped(string binlogPath, string targetName, int minimumSkipCount) + { + var skipCount = new BinLogReader() + .ReadRecords(binlogPath) + .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(skipCount >= minimumSkipCount, + $"Expected target '{targetName}' to be skipped at least {minimumSkipCount} times, but found {skipCount}. See binlog: {binlogPath}"); + } }