diff --git a/src/SingleProject/Resizetizer/src/AndroidAdaptiveIconGenerator.cs b/src/SingleProject/Resizetizer/src/AndroidAdaptiveIconGenerator.cs index 3c3e486f2d26..1dda941b443c 100644 --- a/src/SingleProject/Resizetizer/src/AndroidAdaptiveIconGenerator.cs +++ b/src/SingleProject/Resizetizer/src/AndroidAdaptiveIconGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -52,7 +53,7 @@ public IEnumerable Generate() void ProcessBackground(List results, DirectoryInfo fullIntermediateOutputPath) { var backgroundFile = Info.Filename; - var backgroundExists = File.Exists(backgroundFile); + var (backgroundExists, backgroundModified) = Utils.FileExists(backgroundFile); var backgroundDestFilename = AppIconName + "_background.png"; if (backgroundExists) @@ -64,8 +65,16 @@ void ProcessBackground(List results, DirectoryInfo fullInterme { var dir = Path.Combine(fullIntermediateOutputPath.FullName, dpi.Path); var destination = Path.Combine(dir, backgroundDestFilename); + var (destinationExists, destinationModified) = Utils.FileExists(destination); Directory.CreateDirectory(dir); + if (destinationModified > backgroundModified) + { + Logger.Log($"Skipping `{backgroundFile}` => `{destination}` file is up to date."); + results.Add(new ResizedImageInfo { Dpi = dpi, Filename = destination }); + continue; + } + Logger.Log($"App Icon Background Part: " + destination); if (backgroundExists) @@ -88,7 +97,7 @@ void ProcessBackground(List results, DirectoryInfo fullInterme void ProcessForeground(List results, DirectoryInfo fullIntermediateOutputPath) { var foregroundFile = Info.ForegroundFilename; - var foregroundExists = File.Exists(foregroundFile); + var (foregroundExists, foregroundModified) = Utils.FileExists(foregroundFile); var foregroundDestFilename = AppIconName + "_foreground.png"; if (foregroundExists) @@ -100,8 +109,16 @@ void ProcessForeground(List results, DirectoryInfo fullInterme { var dir = Path.Combine(fullIntermediateOutputPath.FullName, dpi.Path); var destination = Path.Combine(dir, foregroundDestFilename); + var (destinationExists, destinationModified) = Utils.FileExists(destination); Directory.CreateDirectory(dir); + if (destinationModified > foregroundModified) + { + Logger.Log($"Skipping `{foregroundFile}` => `{destination}` file is up to date."); + results.Add(new ResizedImageInfo { Dpi = dpi, Filename = destination }); + continue; + } + Logger.Log($"App Icon Foreground Part: " + destination); if (foregroundExists) @@ -123,14 +140,20 @@ void ProcessForeground(List results, DirectoryInfo fullInterme void ProcessAdaptiveIcon(List results, DirectoryInfo fullIntermediateOutputPath) { - var adaptiveIconXmlStr = AdaptiveIconDrawableXml - .Replace("{name}", AppIconName); - var dir = Path.Combine(fullIntermediateOutputPath.FullName, "mipmap-anydpi-v26"); var adaptiveIconDestination = Path.Combine(dir, AppIconName + ".xml"); var adaptiveIconRoundDestination = Path.Combine(dir, AppIconName + "_round.xml"); Directory.CreateDirectory(dir); + if (File.Exists(adaptiveIconDestination) && File.Exists(adaptiveIconRoundDestination)) { + results.Add(new ResizedImageInfo { Dpi = new DpiPath("mipmap-anydpi-v26", 1), Filename = adaptiveIconDestination }); + results.Add(new ResizedImageInfo { Dpi = new DpiPath("mipmap-anydpi-v26", 1, "_round"), Filename = adaptiveIconRoundDestination }); + return; + } + + var adaptiveIconXmlStr = AdaptiveIconDrawableXml + .Replace("{name}", AppIconName); + // Write out the adaptive icon xml drawables File.WriteAllText(adaptiveIconDestination, adaptiveIconXmlStr); File.WriteAllText(adaptiveIconRoundDestination, adaptiveIconXmlStr); diff --git a/src/SingleProject/Resizetizer/src/AppleIconAssetsGenerator.cs b/src/SingleProject/Resizetizer/src/AppleIconAssetsGenerator.cs index f919382c34f8..1fa48ddc8fe1 100644 --- a/src/SingleProject/Resizetizer/src/AppleIconAssetsGenerator.cs +++ b/src/SingleProject/Resizetizer/src/AppleIconAssetsGenerator.cs @@ -41,6 +41,17 @@ public IEnumerable Generate() var assetContentsFile = Path.Combine(outputAssetsDir, "Contents.json"); var appIconSetContentsFile = Path.Combine(outputAppIconSetDir, "Contents.json"); + var (sourceExists, sourceModified) = Utils.FileExists(Info.Filename); + var (destinationExists, destinationModified) = Utils.FileExists(appIconSetContentsFile); + + if (destinationModified > sourceModified) + { + Logger.Log($"Skipping `{Info.Filename}` => `{appIconSetContentsFile}` file is up to date."); + return new List { + new ResizedImageInfo { Dpi = new DpiPath("", 1), Filename = appIconSetContentsFile } + }; + } + var infoJsonProp = new JsonObject { ["info"] = new JsonObject diff --git a/src/SingleProject/Resizetizer/src/ResizetizeImages.cs b/src/SingleProject/Resizetizer/src/ResizetizeImages.cs index a6e4788eb8be..51670cbb0f98 100644 --- a/src/SingleProject/Resizetizer/src/ResizetizeImages.cs +++ b/src/SingleProject/Resizetizer/src/ResizetizeImages.cs @@ -170,9 +170,17 @@ void ProcessAppIcon(ResizeImageInfo img, ConcurrentBag resized var destination = Resizer.GetRasterFileDestination(img, dpi, IntermediateOutputPath) .Replace("{name}", appIconName); + var (sourceExists, sourceModified) = Utils.FileExists(img.Filename); + var (destinationExists, destinationModified) = Utils.FileExists(destination); LogDebugMessage($"App Icon Destination: " + destination); + if (destinationModified > sourceModified) + { + Logger.Log($"Skipping `{img.Filename}` => `{destination}` file is up to date."); + continue; + } + appTool.Resize(dpi, destination); } } diff --git a/src/SingleProject/Resizetizer/src/Utils.cs b/src/SingleProject/Resizetizer/src/Utils.cs index 115f7fdde517..0ae95d9d8c11 100644 --- a/src/SingleProject/Resizetizer/src/Utils.cs +++ b/src/SingleProject/Resizetizer/src/Utils.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Text.RegularExpressions; using SkiaSharp; @@ -49,5 +50,12 @@ public static bool IsValidResourceFilename(string filename) return null; } + + public static (bool Exists, DateTime Modified) FileExists(string path) + { + var exists = File.Exists(path); + var modified = exists ? File.GetLastWriteTimeUtc(path) : System.DateTime.MinValue; + return (exists, modified); + } } } diff --git a/src/SingleProject/Resizetizer/src/WindowsIconGenerator.cs b/src/SingleProject/Resizetizer/src/WindowsIconGenerator.cs index c051fb07ad27..907b235803d7 100644 --- a/src/SingleProject/Resizetizer/src/WindowsIconGenerator.cs +++ b/src/SingleProject/Resizetizer/src/WindowsIconGenerator.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using SkiaSharp; namespace Microsoft.Maui.Resizetizer @@ -27,11 +28,20 @@ public ResizedImageInfo Generate() string destination = Path.Combine(destinationFolder, $"{fileName}.ico"); Directory.CreateDirectory(destinationFolder); + var (sourceExists, sourceModified) = Utils.FileExists(Info.Filename); + var (destinationExists, destinationModified) = Utils.FileExists(destination); + Logger.Log($"Generating ICO: {destination}"); var tools = new SkiaSharpAppIconTools(Info, Logger); var dpi = new DpiPath(fileName, 1.0m, size: new SKSize(64, 64)); + if (destinationModified > sourceModified) + { + Logger.Log($"Skipping `{Info.Filename}` => `{destination}` file is up to date."); + return new ResizedImageInfo { Dpi = dpi, Filename = destination }; + } + MemoryStream memoryStream = new MemoryStream(); tools.Resize(dpi, destination, () => memoryStream); memoryStream.Position = 0; diff --git a/src/SingleProject/Resizetizer/test/UnitTests/ResizetizeImagesTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/ResizetizeImagesTests.cs index 7dff630d8c33..8bfe2acecac8 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/ResizetizeImagesTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/ResizetizeImagesTests.cs @@ -1362,6 +1362,36 @@ public void ShouldResize(string filename, string baseSize, bool resize) var size = ResizeImageInfo.Parse(item); Assert.Equal(resize, size.Resize); } + + [Theory] + [InlineData("android")] + [InlineData("uwp")] + [InlineData("ios")] + public void GenerationSkippedOnIncrementalBuild(string platform) + { + var items = new[] + { + new TaskItem("images/dotnet_logo.svg", new Dictionary + { + ["IsAppIcon"] = bool.TrueString, + ["ForegroundFile"] = $"images/dotnet_foreground.svg", + ["Link"] = "appicon", + ["BackgroundFile"] = $"images/dotnet_background.svg", + }), + }; + + var task = GetNewTask(platform, items); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + LogErrorEvents.Clear(); + LogMessageEvents.Clear(); + task = GetNewTask(platform, items); + success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + Assert.True(LogMessageEvents.Any(x => x.Message.Contains("Skipping ", StringComparison.OrdinalIgnoreCase)), $"Image generation should have been skipped."); + } } } }