PR: #33919
Branch: fix/issue-23268-font-assets-first-build
Last Updated: 2026-02-06
Font .ttf files disappear from Android's assets/ folder during clean builds in Visual Studio due to an MSBuild output inference interaction with wildcard globs. The fix splits ProcessMauiFonts into two targets: one for file processing (incremental) and one for platform item registration (always runs).
The original ProcessMauiFonts target combines file copying with platform item registration in a single target that has Inputs/Outputs for incremental build support:
<Target Name="ProcessMauiFonts"
Inputs="@(MauiFont)" Outputs="$(_MauiFontStampFile)">
<!-- Task: Copy fonts to intermediate directory -->
<Copy SourceFiles="@(MauiFont)" DestinationFolder="$(_MauiIntermediateFonts)" />
<!-- ItemGroup: Wildcard glob to find copied fonts -->
<ItemGroup>
<_MauiFontCopied Include="$(_MauiIntermediateFonts)*" /> <!-- THE BUG -->
</ItemGroup>
<!-- Platform registrations using _MauiFontCopied... -->
<ItemGroup Condition="Android">
<AndroidAsset Include="@(_MauiFontCopied)" />
</ItemGroup>
</Target>When a target is skipped due to up-to-date Inputs/Outputs:
- Tasks are NOT executed (Copy doesn't run)
- ItemGroups ARE evaluated via output inference
"The target has no out-of-date outputs and is skipped. MSBuild evaluates the target and makes changes to items and properties as if the target ran." — Microsoft Docs
This means the wildcard glob $(_MauiIntermediateFonts)* evaluates against the current filesystem. If the intermediate directory is missing (partial clean, concurrent VS builds where inner/outer builds have different IntermediateOutputPath values, or manual deletion of resizetizer/ while stamp file survives), the glob resolves to nothing — and no platform items are registered.
obj/Release/net10.0-android/
├── mauifont.stamp ← Stamp file (PARENT directory)
├── resizetizer/
│ └── f/ ← Font intermediate directory (SUBDIRECTORY)
│ ├── OpenSans-Regular.ttf
│ └── MauiInfo.plist (iOS only)
The stamp file and intermediate fonts are in different directories. If resizetizer/ is deleted but mauifont.stamp survives → ProcessMauiFonts skips → glob finds nothing → fonts silently disappear.
All three scheduling mechanisms are hard requirements, not hints:
DependsOnTargets— Pull-based: ensures deps run when declaring target runsAfterTargets— Push-based: schedules target to run after referenced targetBeforeTargets— Push-based: schedules target to run before referenced target
Source: Target build order — Steps 4, 5, and 7 of the ordering algorithm all say "executed or skipped".
Split ProcessMauiFonts into two targets:
| Target | Inputs/Outputs | Purpose |
|---|---|---|
ProcessMauiFonts |
✅ Yes (incremental) | Copy fonts, generate iOS plist, touch stamp file |
_CollectMauiFontItems |
❌ No (always runs) | Register platform items using predictive path mapping |
Before (wildcard glob — filesystem-dependent):
<_MauiFontCopied Include="$(_MauiIntermediateFonts)*" />After (predictive mapping — filesystem-independent):
<_MauiFontCopied Include="@(MauiFont->'$(_MauiIntermediateFonts)%(Filename)%(Extension)')" />The predictive mapping transforms source @(MauiFont) items into destination paths without touching the filesystem. This works whether ProcessMauiFonts ran or was skipped.
The old wildcard glob had a subtle bug with font removal: if a font was removed from the project, the intermediate .ttf file remained on disk and the wildcard would still pick it up, causing stale fonts to be packaged. The predictive mapping only maps current @(MauiFont) items, so removed fonts are correctly excluded.
CollectAppManifests (SDK)
↓ CollectAppManifestsDependsOn
_CollectMauiFontItems (always runs)
↓ DependsOnTargets
ProcessMauiFonts (incremental)
↓ DependsOnTargets
ResizetizeCollectItems
- Items:
BundleResource(LogicalName, TargetPath) +PartialAppManifest(Exists check) - iOS
CollectAppManifestsDependsOnupdated fromProcessMauiFonts→_CollectMauiFontItems
ResizetizeCollectItems (BeforeTargets=_ComputeAndroidResourcePaths)
↓ AfterTargets
ProcessMauiFonts → _CollectMauiFontItems
↓ before
_ComputeAndroidResourcePaths (SDK)
- Items:
AndroidAsset(Link)
ProcessMauiFonts → _CollectMauiFontItems (BeforeTargets=AssignTargetPaths)
↓ before
AssignTargetPaths (SDK)
- Items:
ContentWithTargetPath(TargetPath, CopyToPublishDirectory)
ProcessMauiFonts → _CollectMauiFontItems (BeforeTargets=FileClassification)
↓ before
FileClassification (SDK)
- Items:
Resource(LogicalName, Link)
ResizetizeCollectItems (BeforeTargets=PrepareResources)
↓ AfterTargets
ProcessMauiFonts → _CollectMauiFontItems
↓ before
PrepareResources (SDK)
- Items:
TizenTpkUserIncludeFiles(TizenTpkSubDir)
| Scenario | Original (main) | Fixed | Verdict |
|---|---|---|---|
| Clean build | ✅ Works | ✅ Works | Same |
| Incremental (no changes) | ✅ Works (glob finds existing files) | ✅ Works (predictive mapping) | Same, fix more robust |
| Stamp exists, resizetizer/ deleted | ❌ Silent failure (glob empty) | Fix is better — fails loudly | |
| Font added | ✅ Inputs change → reruns | ✅ Same | Same |
| Font removed | ❌ Stale font still packaged (glob picks up old file) | ✅ Old font not registered | Fix is better |
| Concurrent VS builds | ✅ Isolated IntermediateOutputPath | ✅ Same | Same |
| No fonts in project | ✅ No-op | ✅ No-op | Same |
- Round 1: Gemini 3 Pro, GPT Codex 5.2, Claude Sonnet 4.5
- Round 2: Claude Sonnet 4.5, Gemini 3 Pro, GPT Codex 5.2, GPT Codex 5.1, Claude Sonnet 4, Claude Opus 4.5, GPT 5.1
- Round 3 (Fresh): Claude Opus 4.5, GPT Codex 5.2, Gemini 3 Pro, Claude Sonnet 4.5, GPT 5.1, Claude Sonnet 4
All claims verified against official Microsoft Learn documentation:
- ✅ Output inference: ItemGroups evaluated, Tasks skipped (source)
- ✅ Scheduling mechanisms are hard requirements (source)
- ✅ DependsOnTargets is pull-based, AfterTargets/BeforeTargets are push-based (source)
- Android: clean build ✅, incremental ✅, stamp deleted ✅, worst case ✅, APK verification ✅
- iOS: clean build ✅, incremental ✅
- MacCatalyst: clean + incremental ✅
- Verified
_GenerateAndroidAssetsDircorrectly skips on incremental (no unnecessary downstream work)
| File | Change |
|---|---|
Microsoft.Maui.Resizetizer.After.targets |
Split ProcessMauiFonts into ProcessMauiFonts + _CollectMauiFontItems; update iOS CollectAppManifestsDependsOn |
.github/instructions/resizetizer.instructions.md |
New: comprehensive Resizetizer development guidelines (verified by 13+ AI models across 3 rounds) |