Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8696c89
Enable Resizetizer external backends
Copilot Jul 18, 2026
f94cfe0
Track external backend images in FileWrites
Copilot Jul 18, 2026
b55f592
Strengthen Resizetizer external backend tests
Copilot Jul 18, 2026
ec48b80
Add late-import regression coverage for external Resizetizer backends
Copilot Jul 18, 2026
de0c762
Keep late Resizetizer preparation targets independent
Copilot Jul 18, 2026
326f64a
Remove duplicate platform FileWrites entries
Copilot Jul 18, 2026
61c131a
Gate Resizetizer hooks and overwrite test output
Copilot Jul 18, 2026
45c7e9c
Address review: incremental fallback from outputs file; ProcessMauiAs…
Copilot Jul 18, 2026
546a585
Fix zero-image Resizetizer incrementality
Copilot Jul 19, 2026
9019808
Deduplicate processed font outputs
Copilot Jul 19, 2026
7eefa8f
Exclude stale processed font outputs
Copilot Jul 19, 2026
da316d8
Preserve processed fonts on no-op builds
Copilot Jul 19, 2026
c7d518f
Fix Tizen Resizetizer output tracking
Copilot Jul 19, 2026
dfb0539
Recover missing Resizetizer output lists
Copilot Jul 19, 2026
fa4720c
Merge net11.0 into Resizetizer extensibility PR
Copilot Jul 25, 2026
7fef2c1
Format merged Resizetizer integration tests
Copilot Jul 25, 2026
5ee75fb
Make Resizetizer no-op test deterministic
Copilot Jul 25, 2026
8d63ac1
Fix Resizetizer incremental-build regression: advance outputs-file mt…
kubaflo Jul 27, 2026
2eefa6d
Heal missing processed font outputs
Copilot Jul 28, 2026
4f7ec2f
Merge branch 'net11.0' into kubaflo/35022-resizetizer-extensibility
kubaflo Jul 28, 2026
881f5ca
Fix font plist cache recovery
Copilot Jul 28, 2026
500cc32
Merge net11.0 into PR #36653
Copilot Jul 29, 2026
26f818e
Harden Resizetizer timestamp sentinels
Copilot Jul 29, 2026
8145eba
Harden Resizetizer timestamp comparisons
Copilot Jul 29, 2026
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
44 changes: 36 additions & 8 deletions src/SingleProject/Resizetizer/src/DpiPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,34 @@ public static DpiPath[] SplashScreen

}

public static class Generic
{
public static DpiPath Original =>
new DpiPath("", 1.0m);

public static DpiPath[] Image
=> new[]
{
new DpiPath("", 1.0m),
new DpiPath("", 2.0m, null, "@2x"),
};

public static DpiPath[] AppIcon
=> new[]
{
new DpiPath("", 1.0m, null, "-16x16", new SKSize(16, 16)),
new DpiPath("", 1.0m, null, "-32x32", new SKSize(32, 32)),
new DpiPath("", 1.0m, null, "-48x48", new SKSize(48, 48)),
new DpiPath("", 1.0m, null, "-128x128", new SKSize(128, 128)),
new DpiPath("", 1.0m, null, "-256x256", new SKSize(256, 256)),
new DpiPath("", 1.0m, null, "-512x512", new SKSize(512, 512)),
new DpiPath("", 1.0m, null, "-1024x1024", new SKSize(1024, 1024)),
};
}

public static DpiPath GetOriginal(string platform)
{
switch (platform.ToLowerInvariant())
switch ((platform ?? string.Empty).ToLowerInvariant())
Comment thread
kubaflo marked this conversation as resolved.
{
case "ios":
return DpiPath.Ios.Original;
Expand All @@ -326,14 +351,14 @@ public static DpiPath GetOriginal(string platform)
return DpiPath.Wpf.Original;
case "tizen":
return DpiPath.Tizen.Original;
default:
return DpiPath.Generic.Original;
}

return null;
}

public static DpiPath[] GetDpis(string platform)
{
switch (platform.ToLowerInvariant())
switch ((platform ?? string.Empty).ToLowerInvariant())
{
case "ios":
return DpiPath.Ios.Image;
Expand All @@ -345,16 +370,16 @@ public static DpiPath[] GetDpis(string platform)
return DpiPath.Wpf.Image;
case "tizen":
return DpiPath.Tizen.Image;
default:
return DpiPath.Generic.Image;
}

return null;
}

public static DpiPath[] GetAppIconDpis(string platform, string appIconName)
{
DpiPath[] result = null;
DpiPath[] result;

switch (platform.ToLowerInvariant())
switch ((platform ?? string.Empty).ToLowerInvariant())
{
case "ios":
result = DpiPath.Ios.AppIcon;
Expand All @@ -371,6 +396,9 @@ public static DpiPath[] GetAppIconDpis(string platform, string appIconName)
case "tizen":
result = DpiPath.Tizen.AppIcon;
break;
default:
result = DpiPath.Generic.AppIcon;
Comment thread
kubaflo marked this conversation as resolved.
break;
}

foreach (var r in result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,14 @@

<PropertyGroup Condition="'$(_ResizetizerIsAndroidApp)' == 'True' Or '$(_ResizetizerIsiOSApp)' == 'True' Or '$(_ResizetizerIsWPFApp)' == 'True' Or '$(_ResizetizerIsWindowsAppSdk)' == 'True' Or '$(_ResizetizerIsTizenApp)' == 'True'">
<_ResizetizerIsCompatibleApp>True</_ResizetizerIsCompatibleApp>
</PropertyGroup>

<!-- External backends opt in by setting ResizetizerPlatformType in their targets. -->
<PropertyGroup Condition="'$(_ResizetizerIsCompatibleApp)' != 'True' And '$(ResizetizerPlatformType)' != ''">
Comment thread
kubaflo marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Logic — External opt-in schedules splash processing but silently produces no output

Flagged by: 2/3 reviewers after dispute

This new compatibility branch also activates the existing ResizetizeDependsOnTargets, which includes ProcessMauiSplashScreens. For an external ResizetizerPlatformType, however, every splash generator in that target is conditioned on a built-in platform flag; none runs, yet the target still touches _MauiSplashStampFile. There is also no MauiProcessedSplashScreen or post-processing hook. The result is a successful-looking build that drops the configured splash output.

Issue #35022 explicitly lists "MauiSplashScreen processed by the same pipeline" as a success criterion. Add a generic splash-processing/output contract and backend hook with regression coverage, or avoid marking external projects compatible with this stage and narrow the linked issue/PR contract accordingly.

<_ResizetizerIsCompatibleApp>True</_ResizetizerIsCompatibleApp>
</PropertyGroup>

<PropertyGroup Condition="'$(_ResizetizerIsCompatibleApp)' == 'True'">
<ResizetizeDependsOnTargets>
$(ResizetizeDependsOnTargets);
ResizetizeCollectItems;
Expand Down Expand Up @@ -254,7 +261,7 @@

<!-- Collect images from referenced projects -->
Comment thread
kubaflo marked this conversation as resolved.
Outdated
<Target Name="ResizetizeCollectItems"
Condition="'$(_ResizetizerIsCompatibleApp)' == 'True' And '$(DisableResizetizer)' != 'true'"
Condition="'$(DisableResizetizer)' != 'true'"
BeforeTargets="$(ResizetizeCollectItemsBeforeTargets)"
AfterTargets="$(ResizetizeCollectItemsAfterTargets)">

Comment thread
kubaflo marked this conversation as resolved.
Expand Down Expand Up @@ -375,6 +382,7 @@
<_MauiAssetItemMetadata Condition="'$(_ResizetizerIsiOSApp)' == 'True'">Link</_MauiAssetItemMetadata>
<_MauiAssetItemMetadata Condition="'$(_ResizetizerIsWindowsAppSdk)' == 'True'">TargetPath</_MauiAssetItemMetadata>
<_MauiAssetItemMetadata Condition="'$(_ResizetizerIsTizenApp)' == 'True'">TizenTpkFileName</_MauiAssetItemMetadata>
<_MauiAssetItemMetadata Condition="'$(_MauiAssetItemMetadata)' == ''">Link</_MauiAssetItemMetadata>
Comment thread
kubaflo marked this conversation as resolved.
</PropertyGroup>
<GetMauiAssetPath
ProjectDirectory="$(MSBuildProjectDirectory)"
Expand All @@ -395,8 +403,17 @@
<ItemGroup Condition="'$(_ResizetizerIsTizenApp)' == 'True'">
<TizenResource Include="@(_MauiAssetItemWithMetadata)" />
</ItemGroup>
<ItemGroup>
<MauiProcessedAsset Include="@(_MauiAssetItemWithMetadata)" />
</ItemGroup>
</Target>

<!-- External backends add packaging targets to ResizetizerAfterAssetProcessingTargets. -->
<Target Name="ResizetizerAfterAssetProcessing"
AfterTargets="ProcessMauiAssets"
DependsOnTargets="$(ResizetizerAfterAssetProcessingTargets)"
Condition="'$(EnableMauiAssetProcessing)' == 'true'" />
Comment thread
kubaflo marked this conversation as resolved.
Outdated

<Target Name="ProcessMauiSplashScreens"
Condition="'$(EnableMauiSplashScreenProcessing)' == 'true'"
Inputs="$(MSBuildThisFileFullPath);$(_ResizetizerTaskAssemblyName);$(_MauiSplashInputsFile);@(MauiSplashScreen)"
Expand Down Expand Up @@ -533,6 +550,7 @@

<ItemGroup>
<_MauiFontCopied Include="$(_MauiIntermediateFonts)*" />
<MauiProcessedFont Include="@(MauiFont->'$(_MauiIntermediateFonts)%(Filename)%(Extension)')" />
Comment thread
kubaflo marked this conversation as resolved.
Outdated
</ItemGroup>

<!-- iOS -->
Expand Down Expand Up @@ -606,6 +624,12 @@
</ItemGroup>
</Target>

<!-- External backends add packaging targets to ResizetizerAfterFontProcessingTargets. -->
<Target Name="ResizetizerAfterFontProcessing"
AfterTargets="ProcessMauiFonts"
DependsOnTargets="$(ResizetizerAfterFontProcessingTargets)"
Condition="'$(EnableMauiFontProcessing)' == 'true'" />
Comment thread
kubaflo marked this conversation as resolved.
Outdated

<Target Name="_ReadResizetizeImagesOutputs">
<ReadLinesFromFile File="$(_ResizetizerOutputsFile)" Condition="Exists ('$(_ResizetizerOutputsFile)')">
<Output TaskParameter="Lines" ItemName="_ResizetizerOutputs" />
Expand Down Expand Up @@ -657,6 +681,38 @@
Files="@(_ResizetizerImagesToDelete)"
/>

<ItemGroup>
<MauiProcessedImage Include="@(_ResizetizerCollectedImages)" />
Comment thread
kubaflo marked this conversation as resolved.
Comment thread
kubaflo marked this conversation as resolved.
</ItemGroup>
Comment thread
kubaflo marked this conversation as resolved.

<!-- Touch/create our stamp file for outputs -->
<Touch Files="$(_ResizetizerStampFile)" AlwaysCreate="True" />
<WriteLinesToFile
File="$(_ResizetizerOutputsFile)"
Lines="@(_ResizetizerCollectedImages->'%(Identity)')"
Overwrite="true"
WriteOnlyWhenDifferent="true"
Comment thread
kubaflo marked this conversation as resolved.
Outdated
/>

<!-- Include our images and stamp file as filewrites so they don't get rm'd -->
<ItemGroup>
<FileWrites Include="@(_ResizetizerCollectedImages)" />
<FileWrites Include="$(_ResizetizerStampFile)" />
<FileWrites Include="$(_ResizetizerOutputsFile)" />
</ItemGroup>
Comment thread
kubaflo marked this conversation as resolved.
</Target>

<!-- External backends add packaging targets to ResizetizerAfterImageProcessingTargets. -->
<Target Name="ResizetizerAfterImageProcessing"
AfterTargets="ResizetizeImages"
BeforeTargets="_ResizetizeInjectPlatformItems"
DependsOnTargets="$(ResizetizerAfterImageProcessingTargets)"
Condition="'$(EnableMauiImageProcessing)' == 'true'" />
Comment thread
kubaflo marked this conversation as resolved.
Outdated

<Target Name="_ResizetizeInjectPlatformItems"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Regression — Preserve the existing AfterTargets="ResizetizeImages" completion contract

Flagged by: 2/3 reviewers after dispute

This extraction makes _ResizetizeInjectPlatformItems a sibling AfterTargets="ResizetizeImages" target. Real MSBuild probes reproduced that sibling AfterTargets targets run in declaration/import order, so an existing consumer target declared in the project before the package import now executes first and observes empty BundleResource, ContentWithTargetPath, LibraryResourceDirectories, etc. Before this PR, injection happened inside ResizetizeImages, so every AfterTargets="ResizetizeImages" consumer necessarily ran after those items existed.

Keep ResizetizeImages as the compatibility completion target: move processing to a private inner target, run the new backend hook and built-in injection from that inner target, and let the public ResizetizeImages target complete only after injection. That preserves the new seam without changing legacy observer ordering.

AfterTargets="ResizetizeImages"
Condition="'$(EnableMauiImageProcessing)' == 'true' And ('$(_ResizetizerIsAndroidApp)' == 'True' Or '$(_ResizetizerIsiOSApp)' == 'True' Or '$(_ResizetizerIsWindowsAppSdk)' == 'True' Or '$(_ResizetizerIsWPFApp)' == 'True' Or '$(_ResizetizerIsTizenApp)' == 'True')">

<!-- iOS -->
<ItemGroup Condition="'$(_ResizetizerIsiOSApp)' == 'True'">
<!-- Batch the collectd items into BundleResource which iOS expects -->
Comment thread
kubaflo marked this conversation as resolved.
Outdated
Expand Down Expand Up @@ -729,20 +785,6 @@
<FileWrites Include="@(_ResizetizerCollectedImages)" />
</ItemGroup>

<!-- Touch/create our stamp file for outputs -->
<Touch Files="$(_ResizetizerStampFile)" AlwaysCreate="True" />
<WriteLinesToFile
File="$(_ResizetizerOutputsFile)"
Lines="@(_ResizetizerCollectedImages->'%(Identity)')"
Overwrite="true"
WriteOnlyWhenDifferent="true"
/>

<!-- Include our images and stamp file as filewrites so they don't get rm'd -->
<ItemGroup>
<FileWrites Include="$(_ResizetizerStampFile)" />
<FileWrites Include="$(_ResizetizerOutputsFile)" />
</ItemGroup>
</Target>

<!--
Expand Down
54 changes: 48 additions & 6 deletions src/SingleProject/Resizetizer/test/UnitTests/DpiPathTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,56 @@ public void MatchesAnyCase(string platform)
}

[Theory]
[InlineData("ANDROID123")]
[InlineData("Cars")]
[InlineData("123")]
public void ReturnsNullOnInvalidPlatform(string platform)
[InlineData("macos-appkit")]
[InlineData("gtk")]
[InlineData("custom-platform")]
public void ReturnsFallbackForUnknownPlatform(string platform)
{
var paths = DpiPath.GetOriginal(platform);
var path = DpiPath.GetOriginal(platform);

Assert.Equal("", path.Path);
Assert.Equal(1.0m, path.Scale);
}
}

public class GetDpis
{
[Theory]
[InlineData("macos-appkit")]
[InlineData("gtk")]
[InlineData("custom-platform")]
public void ReturnsGenericDesktopFallback(string platform)
{
var paths = DpiPath.GetDpis(platform);

Assert.Collection(paths,
path => Assert.Equal(1.0m, path.Scale),
path =>
{
Assert.Equal(2.0m, path.Scale);
Assert.Equal("@2x", path.ScaleSuffix);
});
}
}

public class GetAppIconDpis
{
[Theory]
[InlineData("macos-appkit")]
[InlineData("gtk")]
[InlineData("custom-platform")]
public void ReturnsGenericDesktopFallback(string platform)
{
var paths = DpiPath.GetAppIconDpis(platform, "appicon");

Assert.Null(paths);
Assert.Collection(paths,
path => Assert.Equal(16, path.Size!.Value.Width),
path => Assert.Equal(32, path.Size!.Value.Width),
path => Assert.Equal(48, path.Size!.Value.Width),
path => Assert.Equal(128, path.Size!.Value.Width),
path => Assert.Equal(256, path.Size!.Value.Width),
path => Assert.Equal(512, path.Size!.Value.Width),
path => Assert.Equal(1024, path.Size!.Value.Width));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1824,5 +1824,26 @@ public void ShouldResize(string filename, string baseSize, bool resize)
Assert.Equal(resize, size.Resize);
}
}

public class ExecuteForCustomPlatform : ExecuteForApp
{
public ExecuteForCustomPlatform(ITestOutputHelper output)
: base(output)
{
}

[Fact]
public void UsesGenericDesktopFallback()
{
var task = GetNewTask("custom-platform", new TaskItem("images/camera.svg", ResizeMetadata));

var success = task.Execute();

Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message);
AssertFileSize("camera.png", 1792, 1792);
AssertFileSize("camera@2x.png", 3584, 3584);
Assert.Equal(2, task.CopiedResources.Length);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,43 @@ 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 CustomBackendProcessesImagesWithoutBuiltInOutputInjection()
{
SetTestIdentifier("custom-backend");
var projectDir = TestDirectory;
var projectFile = Path.Combine(projectDir, "CustomBackend.csproj");
var imageFile = Path.Combine(projectDir, "image.svg");

File.WriteAllText(imageFile, BlankSvgContents);
File.WriteAllText(projectFile,
$$"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>{{DotNetCurrent}}</TargetFramework>
<ResizetizerPlatformType>custom-backend</ResizetizerPlatformType>
<ResizetizerAfterImageProcessingTargets>VerifyCustomBackendImages</ResizetizerAfterImageProcessingTargets>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Resizetizer" Version="{{MauiPackageVersion}}" />
<MauiImage Include="image.svg" />
</ItemGroup>
<Target Name="VerifyCustomBackendImages">
<Error Condition="'@(MauiProcessedImage)' == ''" Text="Custom backends must receive processed images." />
<Error Condition="'@(ContentWithTargetPath)' != ''" Text="Custom backends must not receive built-in output injection." />
<WriteLinesToFile File="$(_MauiIntermediateImages)custom-backend.items" Lines="@(MauiProcessedImage)" />
Comment thread
kubaflo marked this conversation as resolved.
Outdated
Comment thread
kubaflo marked this conversation as resolved.
Outdated
</Target>
<Target Name="VerifyCustomBackendResources" DependsOnTargets="ResizetizeImages" />
</Project>
""");

Assert.True(DotnetInternal.Build(projectFile, "Debug", target: "VerifyCustomBackendResources", properties: BuildProps, output: _output),
$"Custom backend project failed to process images. Check test output for errors.");

var outputsFile = Path.Combine(projectDir, "obj", "Debug", DotNetCurrent, "resizetizer", "r", "custom-backend.items");
var processedImages = File.ReadAllLines(outputsFile);
Comment thread
kubaflo marked this conversation as resolved.
Assert.Equal(2, processedImages.Length);
Assert.All(processedImages, path => Assert.True(File.Exists(path), $"Processed image does not exist: {path}"));
}
}
Loading