Skip to content

Commit 1b5ae6c

Browse files
PureWeenCopilotjfversluiskubaflo
committed
Fix font/splash assets missing on first build and when incremental outputs are deleted (#33919)
<!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change The Resizetizer copies and registers `MauiFont` / `MauiSplashScreen` assets during the build. Two incremental-build gaps could leave an app packaged **without** its fonts or splash screen: 1. **First build (Android/Tizen)** — item *registration* (`AndroidAsset`, `BundleResource`, …) lived **inside** the incremental `ProcessMauiFonts` target. On a clean build the target's output-inference glob was empty, so the platform items were never registered and fonts were missing until a *second* build. The fix splits registration into an always-run `_CollectMauiFontItems` target that maps font paths predictively from `@(MauiFont)`. 2. **Incremental build (all platforms)** — `ProcessMauiFonts` / `ProcessMauiSplashScreens` tracked freshness with `mauifont.stamp` / `mauisplash.stamp` files. A stamp could stay newer than a generated output that was later deleted (partial `obj` clean or concurrent build), so MSBuild skipped the target and the package shipped without the missing font/splash. The fix replaces stamps with `mauifont.outputs` / `mauisplash.outputs` manifests. `_ReadMauiFontOutputs` / `_ReadMauiSplashOutputs` run before freshness evaluation, delete the manifest when a listed generated output is missing, and each processor uses the manifest as its sole `Outputs`. This makes only the affected processor rerun without re-stamping unchanged generated assets and unnecessarily invalidating downstream consumers such as Android aapt2. This PR **consolidates** #35962 (closed): it drops the font/splash stamps, adds `ProcessMauiSplashScreensDependsOnTargets`, and de-duplicates fonts by intermediate filename before `CreatePartialInfoPlistTask`, so colliding names (for example an app and a `ProjectReference` both shipping `OpenSans.ttf`) do not emit duplicate `UIAppFonts` entries. The related runtime-side symptom (noisy missing-font fallback logging) is intentionally out of scope here and handled separately in #35963. ### Issues Fixed Fixes #23268 Fixes #33092 ### Tests - `ResizetizerTests.FontsAreCopiedToAndroidAssetsOnFirstBuild` — clean Release build of the `maui` template asserts the font lands in the Android `assets` folder on the **first** build, then an incremental build confirms `ProcessMauiFonts` is skipped while the always-run `_CollectMauiFontItems` still registers the asset. - `ResizetizerTests.BuildRegeneratesFontsAndSplashWhenIntermediateOutputsAreMissing` (macOS-gated) — builds Android/iOS/MacCatalyst template targets, deletes only the generated iOS/MacCatalyst `MauiInfo.plist` files and verifies both font processors rerun and restore them, then deletes generated font/splash folders, verifies recovery, and finally verifies a no-op build skips both processors. ### Validation - A focused MSBuild sequence verified initial generation, no-op skipping, regeneration after deleting a recorded output, and a subsequent no-op skip using the same manifest-invalidation protocol. - The full integration workflow could not complete locally because Android workload installation exhausted the shared disk; the updated Build integration test will validate on CI. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Gerald Versluis <939291+jfversluis@users.noreply.github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
1 parent 2f2f40f commit 1b5ae6c

3 files changed

Lines changed: 732 additions & 21 deletions

File tree

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
---
2+
applyTo:
3+
- "src/SingleProject/Resizetizer/**"
4+
- ".buildtasks/Microsoft.Maui.Resizetizer.After.targets"
5+
---
6+
7+
# Resizetizer MSBuild Targets Guidelines
8+
9+
Guidance for working with .NET MAUI's Resizetizer build system, which processes images, fonts, splash screens, and assets at build time.
10+
11+
> **See also:** .NET for Android's [MSBuild Best Practices](https://github.com/dotnet/android/blob/main/Documentation/guides/MSBuildBestPractices.md) is the canonical guide for MSBuild target authoring. Its *Incremental Builds*, *Stamp Files*, *FileWrites and IncrementalClean*, *When to not use Inputs and Outputs?*, and *Should I use BeforeTargets or AfterTargets?* sections directly inform the patterns documented below.
12+
13+
## Architecture Overview
14+
15+
The Resizetizer is an MSBuild-integrated pipeline that processes `MauiImage`, `MauiFont`, `MauiSplashScreen`, and `MauiAsset` items into platform-specific resources during the build. All core logic lives in a single file:
16+
17+
**`src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets`**
18+
19+
### File Loading Order
20+
21+
These files are auto-imported by the NuGet package (`<PackageId>.props` / `<PackageId>.targets`), which then pulls in the named hook files:
22+
23+
| File | Loaded | Purpose |
24+
|------|--------|---------|
25+
| `Microsoft.Maui.Resizetizer.props` | auto (package) | Early property defaults — currently empty (`<Project />`) |
26+
| `Microsoft.Maui.Resizetizer.targets` | auto (package) | **SDK registration**: sets `UsingMicrosoftMauiResizetizerSdk`, imports `Before.targets`, and appends `After.targets` to `AfterMicrosoftNETSdkTargets` |
27+
| `Microsoft.Maui.Resizetizer.Before.targets` | imported by `.targets` | Pre-SDK target hooks — currently empty (`<Project />`) |
28+
| `Microsoft.Maui.Resizetizer.After.targets` | via `AfterMicrosoftNETSdkTargets` | **All logic** — targets, item registration, platform dispatch |
29+
30+
`Microsoft.Maui.Resizetizer.targets` appends `After.targets` to `AfterMicrosoftNETSdkTargets`, ensuring the logic runs after the .NET SDK targets are loaded.
31+
32+
### Local Testing with `.buildtasks/`
33+
34+
The `.buildtasks/` directory at the repo root contains a local copy of the Resizetizer targets used by Sandbox and sample builds. It is **NOT git-tracked**. When testing MSBuild target changes locally:
35+
36+
1. Edit the source file in `src/SingleProject/Resizetizer/src/nuget/buildTransitive/`
37+
2. Copy it to `.buildtasks/Microsoft.Maui.Resizetizer.After.targets`
38+
3. Build the Sandbox or sample project to test
39+
40+
⚠️ Always remember to update both files. The `.buildtasks/` copy is what actually runs during local Sandbox builds.
41+
42+
## Key Properties
43+
44+
### Intermediate Output Paths
45+
46+
```xml
47+
<_ResizetizerIntermediateOutputPath>$(IntermediateOutputPath)</_ResizetizerIntermediateOutputPath>
48+
<_ResizetizerIntermediateOutputRoot>$(_ResizetizerIntermediateOutputPath)resizetizer\</_ResizetizerIntermediateOutputRoot>
49+
<_MauiIntermediateImages>...\resizetizer\r\</_MauiIntermediateImages>
50+
<_MauiIntermediateFonts>...\resizetizer\f\</_MauiIntermediateFonts>
51+
<_MauiIntermediateSplashScreen>...\resizetizer\sp\</_MauiIntermediateSplashScreen>
52+
```
53+
54+
**⚠️ CRITICAL**: `_ResizetizerIntermediateOutputPath` defaults to `$(IntermediateOutputPath)`, which **differs between outer and inner builds** in multi-targeting scenarios:
55+
- Outer build: `obj/Release/net10.0-android/`
56+
- Inner build (arm64): `obj/Release/net10.0-android/android-arm64/`
57+
58+
This means stamp/manifest files, input tracking files, and intermediate outputs are at **different paths** in outer vs inner builds.
59+
60+
### Incremental Build Tracking (Stamp & Output-Manifest Files)
61+
62+
Two mechanisms drive the `Inputs`/`Outputs` up-to-date checks:
63+
64+
| File | Tracks | Mechanism |
65+
|------|--------|-----------|
66+
| `mauifont.outputs` | Font processing (`ProcessMauiFonts`) | Output manifest |
67+
| `mauisplash.outputs` | Splash screen processing (`ProcessMauiSplashScreens`) | Output manifest |
68+
| `mauiimage.stamp` + `mauiimage.outputs` | Image resizing (`ResizetizeImages`) | Stamp + output manifest |
69+
| `mauimanifest.stamp` | Platform manifest generation | Stamp |
70+
71+
Each processing target (except `mauimanifest.stamp`) has a companion `.inputs` file containing serialized metadata for change detection.
72+
73+
**⚠️ Prefer output manifests over bare stamp files.** A bare stamp (`<Touch>` + `Outputs="$(stamp)"`) only records *when* a target last ran — its timestamp can stay newer than a generated output that was later deleted (e.g. a partial `obj` clean or concurrent build), so MSBuild wrongly treats the target as up-to-date and the package ships without the missing font/splash (regression #33092). Instead:
74+
75+
1. Write the list of generated files to a `*.outputs` manifest with `WriteLinesToFile`.
76+
2. Read it back **before** the up-to-date check via a `_Read*Outputs` target wired through `DependsOnTargets`.
77+
3. Before the incremental check, detect missing files from the manifest and delete the
78+
manifest when any are absent.
79+
4. Use the manifest as the target's sole `Outputs`, e.g. `Outputs="$(_MauiFontOutputsFile)"`.
80+
81+
Now if any generated file disappears, the read target invalidates the manifest, MSBuild sees
82+
the missing output, and re-runs *only* that target. The target does not re-stamp unchanged
83+
generated assets, so downstream consumers such as Android aapt2 avoid unnecessary work.
84+
`ProcessMauiFonts` / `ProcessMauiSplashScreens` use this pattern; `ResizetizeImages` keeps
85+
its legacy stamp alongside its own `mauiimage.outputs`.
86+
87+
## Target Pipeline
88+
89+
### Main Targets (Execution Order)
90+
91+
```
92+
ResizetizeCollectItems ← Collects items from project + references
93+
├── ProcessMauiAssets ← Computes asset paths and registers platform items
94+
├── ProcessMauiSplashScreens ← Generates splash resources
95+
├── ProcessMauiFonts ← Copies font files (incremental)
96+
│ └── _CollectMauiFontItems ← Registers platform items (ALWAYS runs)
97+
└── ResizetizeImages ← Resizes images (incremental)
98+
```
99+
100+
### Platform-Specific Scheduling
101+
102+
| Platform | ResizetizeCollectItems | ProcessMauiFonts / _CollectMauiFontItems | ResizetizeImages |
103+
|----------|----------------------|-----------------|-----------------|
104+
| **iOS** | `CollectBundleResourcesDependsOn`, `CompileImageAssetsDependsOn` | `_CollectMauiFontItems` via `CollectAppManifestsDependsOn` | `AfterTargets=ResizetizeCollectItems` |
105+
| **Android** | `BeforeTargets=_ComputeAndroidResourcePaths` | `AfterTargets=ResizetizeCollectItems` | `AfterTargets=ResizetizeCollectItems` |
106+
| **Windows** | Via `DependsOnTargets` (from `ResizetizeImages`/`ProcessMauiFonts`) | `BeforeTargets=AssignTargetPaths` | `BeforeTargets=AssignTargetPaths` |
107+
| **WPF** | Via `DependsOnTargets` (from `ResizetizeImages`/`ProcessMauiFonts`) | `BeforeTargets=FileClassification` | `BeforeTargets=FileClassification` |
108+
| **Tizen** | Via `DependsOnTargets` (from `ResizetizeImages`/`ProcessMauiFonts`) | `AfterTargets=ResizetizeCollectItems` | `AfterTargets=ResizetizeCollectItems` |
109+
110+
## ⚠️ Critical Pattern: Inputs/Outputs and Item Registration
111+
112+
### The Problem
113+
114+
MSBuild's `Inputs`/`Outputs` incremental check skips **targets** when outputs are up-to-date, but **still evaluates ItemGroups and PropertyGroups** via [output inference](https://learn.microsoft.com/en-us/visualstudio/msbuild/incremental-builds#output-inference). However, this is dangerous when ItemGroups depend on side-effects of skipped tasks:
115+
116+
- Wildcard globs (`$(_MauiIntermediateFonts)*`) depend on files created by `Copy` tasks — if the intermediate directory is missing (partial clean, concurrent builds), the glob evaluates to nothing
117+
- Tasks like `CreatePartialInfoPlistTask` are genuinely skipped — their output files won't exist if they haven't run
118+
119+
### The Solution: Split Target Pattern
120+
121+
**ALWAYS separate file-processing work from item registration into two targets:**
122+
123+
1. **Processing target** (with `Inputs`/`Outputs`): Does the actual work (copy, resize, generate)
124+
2. **Collection target** (NO `Inputs`/`Outputs`): Registers platform-specific items — always runs
125+
126+
```xml
127+
<!-- Target 1: Does work, can be skipped by incremental build. Tracks its generated outputs
128+
in a manifest (read back by _ReadMauiFontOutputs) so a deleted output re-triggers it. -->
129+
<Target Name="ProcessMauiFonts"
130+
Inputs="@(MauiFont);$(_MauiFontInputsFile)"
131+
Outputs="$(_MauiFontOutputsFile)"
132+
DependsOnTargets="$(ProcessMauiFontsDependsOnTargets)"
133+
...>
134+
<Copy SourceFiles="@(MauiFont)" DestinationFolder="$(_MauiIntermediateFonts)" />
135+
<ItemGroup>
136+
<_MauiFontOutput Include="@(MauiFont->'$(_MauiIntermediateFonts)%(Filename)%(Extension)')" />
137+
</ItemGroup>
138+
<WriteLinesToFile File="$(_MauiFontOutputsFile)" Lines="@(_MauiFontOutput->'%(FullPath)')"
139+
Overwrite="true" WriteOnlyWhenDifferent="true" />
140+
<Touch Files="$(_MauiFontOutputsFile)" AlwaysCreate="True" />
141+
</Target>
142+
143+
<!-- Reads the previous manifest and invalidates it if a generated output is missing. -->
144+
<Target Name="_ReadMauiFontOutputs">
145+
<ReadLinesFromFile File="$(_MauiFontOutputsFile)" Condition="Exists('$(_MauiFontOutputsFile)')">
146+
<Output TaskParameter="Lines" ItemName="_MauiFontOutputs" />
147+
</ReadLinesFromFile>
148+
<ItemGroup>
149+
<_MauiMissingFontOutput Include="@(_MauiFontOutputs)"
150+
Condition="!Exists('%(_MauiFontOutputs.Identity)')" />
151+
</ItemGroup>
152+
<Delete Files="$(_MauiFontOutputsFile)"
153+
Condition="'@(_MauiMissingFontOutput)' != ''" />
154+
</Target>
155+
156+
<!-- Target 2: Registers items, ALWAYS runs (no Inputs/Outputs) -->
157+
<Target Name="_CollectMauiFontItems"
158+
DependsOnTargets="ProcessMauiFonts"
159+
...>
160+
<ItemGroup>
161+
<AndroidAsset Include="@(MauiFont->'$(_MauiIntermediateFonts)%(Filename)%(Extension)')" />
162+
</ItemGroup>
163+
</Target>
164+
```
165+
166+
### Item Collection Best Practices
167+
168+
**✅ DO**: Use predictive path mapping from source items:
169+
```xml
170+
<_MauiFontCopied Include="@(MauiFont->'$(_MauiIntermediateFonts)%(Filename)%(Extension)')" />
171+
```
172+
173+
**❌ DON'T**: Use wildcard globs on intermediate directories — they can pick up stale files from deleted sources:
174+
```xml
175+
<!-- AVOID: May include stale files from previously deleted fonts -->
176+
<_MauiFontCopied Include="$(_MauiIntermediateFonts)*" />
177+
```
178+
179+
**Exception**: `ResizetizeImages` uses wildcard globs (`$(_MauiIntermediateImages)**\*`) because image resizing produces multiple output files per input (different sizes/densities). It compensates by explicitly deleting orphaned files.
180+
181+
## Platform Item Registration
182+
183+
### How Each Platform Receives Assets
184+
185+
**Font Items** (from `_CollectMauiFontItems`):
186+
187+
| Platform | Item Type | Metadata |
188+
|----------|-----------|----------|
189+
| **iOS** | `BundleResource` | `LogicalName`, `TargetPath` |
190+
| **Android** | `AndroidAsset` | `Link` |
191+
| **Windows** | `ContentWithTargetPath` | `TargetPath`, `CopyToPublishDirectory` |
192+
| **WPF** | `Resource` | `LogicalName`, `Link` |
193+
| **Tizen** | `TizenTpkUserIncludeFiles` | `TizenTpkSubDir` |
194+
195+
**Image Items** (from `ResizetizeImages`):
196+
197+
| Platform | Item Type | Metadata |
198+
|----------|-----------|----------|
199+
| **iOS** | `BundleResource` or `ImageAsset` | `LogicalName`, `TargetPath` (+ `Link` for ImageAsset) |
200+
| **Android** | `LibraryResourceDirectories` | `StampFile` |
201+
| **Windows** | `ContentWithTargetPath` | `TargetPath`, `CopyToPublishDirectory` |
202+
| **WPF** | `Resource` | `LogicalName`, `Link` |
203+
| **Tizen** | `TizenTpkUserIncludeFiles` | `TizenTpkSubDir` |
204+
205+
### iOS-Specific: Info.plist Font Registration
206+
207+
iOS requires fonts to be declared in Info.plist via `UIAppFonts`. The `CreatePartialInfoPlistTask` generates a `MauiInfo.plist` fragment, which is then added to `PartialAppManifest` for merging.
208+
209+
**Important**: The plist generation (`CreatePartialInfoPlistTask`) is inside `ProcessMauiFonts` (the incremental target), while the `PartialAppManifest` registration is in `_CollectMauiFontItems` (always runs). This is correct because:
210+
- The plist only needs regeneration when fonts change (handled by Inputs/Outputs)
211+
- The plist FILE registration must happen every build (handled by always-run target using `Exists()` check)
212+
- The generated `MauiInfo.plist` is added to `mauifont.outputs`, so deleting it also re-triggers `ProcessMauiFonts`
213+
- Fonts are de-duplicated by intermediate filename before `CreatePartialInfoPlistTask` so colliding names (e.g. a project and a `ProjectReference` both shipping `OpenSans.ttf`) don't emit duplicate `UIAppFonts` entries
214+
215+
## ResizetizeCollectItems
216+
217+
This target is the starting point for the pipeline. It:
218+
219+
1. Calls `GetMauiItems` on the project itself (if `ResizetizerIncludeSelfProject='True'`)
220+
2. Calls `GetMauiItems` on all `@(ProjectReference)` projects (parallel MSBuild calls)
221+
3. Aggregates `MauiImage`, `MauiIcon`, `MauiFont`, `MauiAsset`, `MauiSplashScreen` from all sources
222+
4. Serializes item metadata to `.inputs` files for incremental change detection
223+
5. Computes hashes for splash screen filename stability
224+
225+
## MSBuild Scheduling Semantics
226+
227+
### Understanding AfterTargets / BeforeTargets / DependsOnTargets
228+
229+
**All three are hard requirements.** None of them are "hints" or "suggestions."
230+
231+
| Mechanism | Meaning | When to Use |
232+
|-----------|---------|-------------|
233+
| `DependsOnTargets="X"` | "When I run, run X first (if it hasn't run)" | Hard dependency chain |
234+
| `AfterTargets="X"` | "After X runs, run me" | Scheduling — ensures ordering |
235+
| `BeforeTargets="X"` | "Before X runs, run me" | Scheduling — ensures ordering |
236+
237+
**Key difference**: `DependsOnTargets` is **pull-based** (only runs if the depending target runs). `AfterTargets`/`BeforeTargets` are **push-based** (registers the target to run whenever the referenced target runs).
238+
239+
**⚠️ CRITICAL**: `DependsOnTargets` alone does NOT trigger a target. Something must invoke the target first (via `AfterTargets`, `BeforeTargets`, or another target's `DependsOnTargets`).
240+
241+
**✅ Prefer `DependsOnTargets` via an overridable property where possible.** Express ordering as `DependsOnTargets="$(_MyTargetDependsOn)"` and define the list as a property so consumers can extend the chain without editing the target. This is how the Resizetizer wires its read-manifest targets, e.g.:
242+
243+
```xml
244+
<PropertyGroup>
245+
<ProcessMauiFontsDependsOnTargets>
246+
$(ProcessMauiFontsDependsOnTargets);
247+
_ReadMauiFontOutputs;
248+
</ProcessMauiFontsDependsOnTargets>
249+
</PropertyGroup>
250+
<Target Name="ProcessMauiFonts" DependsOnTargets="$(ProcessMauiFontsDependsOnTargets)" ... />
251+
```
252+
253+
Reserve `AfterTargets`/`BeforeTargets` for one-off push-based hooks into SDK targets you don't own (e.g. `AssignTargetPaths`, `_ComputeAndroidResourcePaths`). See [Should I use BeforeTargets or AfterTargets?](https://github.com/dotnet/android/blob/main/Documentation/guides/MSBuildBestPractices.md#should-i-use-beforetargets-or-aftertargets).
254+
255+
### Common Pitfall: Items Dependent on Task Side-Effects
256+
257+
A wildcard glob over an intermediate directory populated by a task in the **same** target is usually fine on a normal incremental build: when the target is skipped as up-to-date, the files from the previous run are still on disk, so the glob still finds them. It becomes a problem in two specific cases:
258+
259+
1. **The generated files are deleted out-of-band while the target still skips** — e.g. a partial `obj` clean, a concurrent/parallel build racing on the same intermediate folder, or a tool deleting intermediate files. If the up-to-date check is driven by a bare stamp whose timestamp stays newer than the (now missing) outputs, MSBuild skips the target, the glob finds nothing, and the item is silently dropped. **Fix:** track the real generated files in an output manifest (see [Incremental Build Tracking](#incremental-build-tracking-stamp--output-manifest-files)) so a missing output re-runs the target (issue #33092).
260+
2. **A consumer needs the registered items even when the processing target is skipped or runs later in the schedule** — item registration that lives inside the incremental processing target is not guaranteed to be visible to the target that packages it. **Fix:** register platform items in a companion always-run collection target (issue #23268).
261+
262+
Predictive mapping from the source items is also preferred over a filesystem glob because it never picks up stale files left over from deleted sources:
263+
264+
```xml
265+
<!-- ⚠️ Glob over the intermediate dir: correct only while the files are on disk,
266+
and retains stale outputs from removed sources -->
267+
<Copy SourceFiles="@(MauiFont)" DestinationFolder="$(_MauiIntermediateFonts)" />
268+
<ItemGroup>
269+
<_MauiFontCopied Include="$(_MauiIntermediateFonts)*" />
270+
</ItemGroup>
271+
272+
<!-- ✅ Predictive mapping from source items: filesystem-independent and stale-free -->
273+
<ItemGroup>
274+
<_MauiFontCopied Include="@(MauiFont->'$(_MauiIntermediateFonts)%(Filename)%(Extension)')" />
275+
</ItemGroup>
276+
```
277+
278+
## Platform Detection Properties
279+
280+
| Property | Detects |
281+
|----------|---------|
282+
| `_ResizetizerIsAndroidApp` | Android application (`AndroidApplication='True'`) |
283+
| `_ResizetizerIsiOSApp` | iOS/MacCatalyst application (includes both) |
284+
| `_ResizetizerIsWindowsAppSdk` | Windows App SDK (WinUI) |
285+
| `_ResizetizerIsWPFApp` | WPF application |
286+
| `_ResizetizerIsTizenApp` | Tizen application |
287+
| `_ResizetizerIsCompatibleApp` | Any of the above |
288+
289+
## Testing MSBuild Target Changes
290+
291+
### Build Verification
292+
293+
```bash
294+
# 1. Copy updated targets to .buildtasks/
295+
cp src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets \
296+
.buildtasks/Microsoft.Maui.Resizetizer.After.targets
297+
298+
# 2. Clean build test
299+
rm -rf artifacts/obj/Maui.Controls.Sample.Sandbox/Release/net10.0-android/
300+
dotnet build src/Controls/samples/Controls.Sample.Sandbox/Maui.Controls.Sample.Sandbox.csproj \
301+
-f:net10.0-android -c:Release --no-restore
302+
303+
# 3. Verify fonts exist
304+
find artifacts/obj/Maui.Controls.Sample.Sandbox/Release/net10.0-android/ -path "*/assets/*.ttf" | wc -l
305+
306+
# 4. Incremental build test (no changes)
307+
dotnet build ... (same command)
308+
# Verify fonts still present
309+
310+
# 5. Diagnostic build to verify target execution
311+
dotnet build ... -v:diag 2>&1 | grep -E "ProcessMauiFonts|_CollectMauiFontItems|Skipping"
312+
```
313+
314+
### Key Things to Verify
315+
316+
- **Clean build**: All assets appear in output
317+
- **Incremental build**: Processing targets SKIP, collection targets RUN, assets still present
318+
- **No unnecessary downstream work**: Platform asset targets (e.g., `_GenerateAndroidAssetsDir`) should skip when fonts haven't changed
319+
- **Modified input**: Touching a font source file should cause `ProcessMauiFonts` to re-run
320+
321+
## Common Mistakes
322+
323+
| Mistake | Impact | Correct Approach |
324+
|---------|--------|-----------------|
325+
| Use wildcard glob dependent on task output | Glob finds nothing if task was skipped (output inference) | Use predictive path mapping from source items |
326+
| Put task-dependent logic in same target as work | During output inference, tasks are skipped but ItemGroups evaluate | Use split target pattern |
327+
| Forget to copy changes to `.buildtasks/` | Local testing uses old code | Always copy after editing source |
328+
| Assume `DependsOnTargets` triggers execution | Target never runs | Add `AfterTargets` or `BeforeTargets` trigger |
329+
| Mix up `AfterTargets` vs `DependsOnTargets` | Both are hard requirements, but serve different purposes | `DependsOnTargets` = pull, `AfterTargets` = push |
330+
| Assume target body is fully skipped by Inputs/Outputs | ItemGroups ARE evaluated via output inference; only tasks are skipped | Be aware of output inference; don't rely on it for task-dependent items |

0 commit comments

Comments
 (0)