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