Skip to content

Instantly share code, notes, and snippets.

@PureWeen
Last active February 6, 2026 16:23
Show Gist options
  • Select an option

  • Save PureWeen/bf06eaa554ab58eedf6075f3c15c9875 to your computer and use it in GitHub Desktop.

Select an option

Save PureWeen/bf06eaa554ab58eedf6075f3c15c9875 to your computer and use it in GitHub Desktop.
Analysis: Issue #23268 - Font assets not copied on first build (Android/Tizen)

Issue #23268: Font Assets Missing on First Build — Root Cause Analysis & Fix

PR: #33919 Branch: fix/issue-23268-font-assets-first-build Last Updated: 2026-02-06


Executive Summary

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).


Root Cause

The Bug Mechanism

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>

MSBuild Output Inference (Verified Against Official Docs)

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.

Key Directory Structure

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.

MSBuild Scheduling Semantics (Verified)

All three scheduling mechanisms are hard requirements, not hints:

  • DependsOnTargets — Pull-based: ensures deps run when declaring target runs
  • AfterTargets — Push-based: schedules target to run after referenced target
  • BeforeTargets — 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".


The Fix: Split Target Pattern

Architecture

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

Key Change: 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.

Additional Benefit: Font Removal

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.


Platform Scheduling Chains (All Verified)

iOS/MacCatalyst

CollectAppManifests (SDK)
    ↓ CollectAppManifestsDependsOn
_CollectMauiFontItems (always runs)
    ↓ DependsOnTargets
ProcessMauiFonts (incremental)
    ↓ DependsOnTargets
ResizetizeCollectItems
  • Items: BundleResource (LogicalName, TargetPath) + PartialAppManifest (Exists check)
  • iOS CollectAppManifestsDependsOn updated from ProcessMauiFonts_CollectMauiFontItems

Android

ResizetizeCollectItems (BeforeTargets=_ComputeAndroidResourcePaths)
    ↓ AfterTargets
ProcessMauiFonts → _CollectMauiFontItems
    ↓ before
_ComputeAndroidResourcePaths (SDK)
  • Items: AndroidAsset (Link)

Windows

ProcessMauiFonts → _CollectMauiFontItems (BeforeTargets=AssignTargetPaths)
    ↓ before
AssignTargetPaths (SDK)
  • Items: ContentWithTargetPath (TargetPath, CopyToPublishDirectory)

WPF

ProcessMauiFonts → _CollectMauiFontItems (BeforeTargets=FileClassification)
    ↓ before
FileClassification (SDK)
  • Items: Resource (LogicalName, Link)

Tizen

ResizetizeCollectItems (BeforeTargets=PrepareResources)
    ↓ AfterTargets
ProcessMauiFonts → _CollectMauiFontItems
    ↓ before
PrepareResources (SDK)
  • Items: TizenTpkUserIncludeFiles (TizenTpkSubDir)

Edge Case Analysis

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) ⚠️ Explicit failure (items point to missing files) 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

Verification

Models Consulted (3 rounds of review)

  • 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

MSBuild Documentation Verified

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)

Build Testing (10 scenarios)

  • Android: clean build ✅, incremental ✅, stamp deleted ✅, worst case ✅, APK verification ✅
  • iOS: clean build ✅, incremental ✅
  • MacCatalyst: clean + incremental ✅
  • Verified _GenerateAndroidAssetsDir correctly skips on incremental (no unnecessary downstream work)

Files Changed

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment