.agents/skills/update-skia/SKILL.md
Update Google Skia to a new Chrome milestone in SkiaSharp's mono/skia fork.
scripts/update-versions.ps1 — Phase 6: Updates all version files and runs verificationscripts/regenerate-bindings.ps1 — Phase 8: Regenerates bindings, reverts HarfBuzz, reports new functionsUpdating Skia is the highest-risk operation in SkiaSharp. It touches:
Go slow. Research first. Build and test before any PR.
This is an 11-phase workflow where each phase builds on the previous one. The phases exist because Skia updates touch four layers (C++ → C API → generated bindings → C# wrappers) and two repositories (mono/skia + mono/SkiaSharp). Skipping a phase doesn't just risk a build failure — it risks shipping broken binaries to customers who won't see the problem until runtime.
Each phase ends with a gate — a verification step that confirms the phase completed correctly. Re-read each phase's instructions before executing it, because the details are project-specific and easy to get wrong from memory.
A. Research (Phases 1–3)
1. Discovery & Current State
2. Breaking Change Analysis
3. Validation
B. Branch & Merge (Phases 4–5)
4. Branch Setup
5. Upstream Merge
C. Update & Build (Phases 6–7)
6. Update Version Files
7. Fix C API Shim & Build Native
D. Regenerate & Verify (Phases 8–10)
8. Regenerate Bindings
9. Fix C# Wrappers
10. Build & Test
E. Ship (Phase 11)
11. Create PRs
🛑 STOP AND ASK before: Merging PRs, Force pushing, Deleting branches, Any destructive git operations
| Repository | Protected Branches | Action Required |
|---|---|---|
| mono/SkiaSharp (parent) | main | Create feature branch first |
| mono/skia (submodule) | main, skiasharp | Create feature branch first |
| Shortcut | Why It's Wrong |
|---|---|
Push directly to skiasharp or main | Bypasses PR review and CI |
| Skip breaking change analysis | Causes runtime crashes for customers |
Use externals-download after C API changes | Causes EntryPointNotFoundException |
| Merge both PRs without updating submodule in between | Squash-merge orphans commits |
| Skip tests | Untested code = broken customers |
Identify current milestone:
grep SK_MILESTONE externals/skia/include/core/SkMilestone.h
grep "^libSkiaSharp.*milestone" scripts/VERSIONS.txt
grep chrome_milestone cgmanifest.json
Identify target milestone from user request.
Check for existing PRs — Search both mono/SkiaSharp and mono/skia for open update PRs.
Verify upstream branches exist and fetch:
cd externals/skia
git remote add upstream https://github.com/google/skia.git 2>/dev/null
git fetch upstream chrome/m{TARGET}
Note: When this phase is pre-computed by the automated workflow, you still need to add the
upstreamremote and fetchchrome/m{TARGET}— Phase 5 depends on it.
🛑 GATE: Confirm current milestone, target milestone, and that upstream branch exists.
This is the most critical phase. Thorough analysis here prevents customer-facing breakage.
Read official release notes for EVERY milestone being skipped:
https://raw.githubusercontent.com/google/skia/main/RELEASE_NOTES.mdCategorize changes by impact:
| Category | Risk | Examples |
|---|---|---|
| Removed APIs | 🔴 HIGH | Functions deleted, enums removed |
| Renamed/Moved APIs | 🟡 MEDIUM | Namespace changes, header moves |
| New APIs | 🟢 LOW | Additive changes, new factories |
| Behavior changes | 🟡 MEDIUM | Default changes, semantic shifts |
| Graphite-only | ⚪ SKIP | SkiaSharp uses Ganesh, not Graphite |
Map each HIGH/MEDIUM change to C API files:
cd externals/skia
# Check which C API files reference affected APIs
grep -r "GrMipmapped\|GrMipMapped" src/c/ include/c/
grep -r "refTypefaceOrDefault\|getTypefaceOrDefault" src/c/ include/c/
Run structural diff on include/ directory:
git diff upstream/chrome/m{CURRENT}..upstream/chrome/m{TARGET} --stat -- include/
git diff upstream/chrome/m{CURRENT}..upstream/chrome/m{TARGET} -- include/core/ include/gpu/ganesh/
👉 See references/breaking-changes-checklist.md for the full analysis template, including verification steps for struct sizes, moved files, and diff-reading traps.
🛑 GATE: Include the breaking change analysis in the PR description body. Summarize the key findings (HIGH/MEDIUM risk changes and their C API impact) for the user.
The agent performing the breaking change analysis has blind spots — it may filter out relevant changes or miss moved headers. An independent validation catches these before they become runtime crashes.
Launch an explore agent with model: "claude-opus-4.6" using the prompt template from
references/validation-prompt.md — substitute the
milestone numbers and paste your breaking change analysis table. The default explore model
(Haiku) is too weak for accurate header-level validation — use Opus for reliability.
🛑 GATE: Validation agent has run and confirmed analysis. If it found missed items, update the analysis and re-present to user before proceeding.
✅ Before proceeding to B (Branch & Merge):
- Current and target milestones confirmed
- Breaking change analysis complete
- Validation passed
⚠️ This phase creates BOTH branches before making ANY changes. You may be on a workflow branch, a feature branch, or a detached HEAD — none of which is the right base. You MUST branch from
origin/main(parent) andorigin/skiasharp(submodule).
Parent repo (SkiaSharp):
Fetch the latest main:
git fetch origin main
Create the feature branch from origin/main:
git checkout -b skia-sync/m{TARGET} origin/main
If the branch already exists on the remote, check it out instead:
git fetch origin skia-sync/m{TARGET} && git checkout skia-sync/m{TARGET}
Verify you are on the correct branch and it is based on origin/main:
git log --oneline -1 origin/main
git log --oneline -1 HEAD
These should show the same commit (or HEAD should be ahead by only your own commits).
Submodule (mono/skia):
Enter the submodule:
cd externals/skia
Align to the SHA that origin/main expects (the submodule tracks the skiasharp
branch in mono/skia, NOT main):
MAIN_SUB_SHA=$(git -C ../.. ls-tree origin/main -- externals/skia | awk '{print $3}')
git fetch origin skiasharp
git checkout "$MAIN_SUB_SHA"
Verify this SHA is on origin/skiasharp:
git branch -r --contains "$MAIN_SUB_SHA" | grep 'origin/skiasharp'
Create the submodule feature branch:
git checkout -b skia-sync/m{TARGET}
⚠️ Do NOT skip the SHA alignment step (step 5). If the submodule is at a different SHA than
origin/mainexpects, the merge will produce phantom diffs — functions that already exist onmainwill appear as new or removed.
🛑 GATE: Both branches created. Verify:
bash# In parent repo: git rev-parse --abbrev-ref HEAD # → skia-sync/m{TARGET} git merge-base HEAD origin/main # → should match origin/main tip # In submodule: git -C externals/skia rev-parse --abbrev-ref HEAD # → skia-sync/m{TARGET}
You should still be inside externals/skia from Phase 4.
Merge upstream — use --no-commit for manual conflict resolution:
git merge --no-commit upstream/chrome/m{TARGET}
Resolve conflicts — each conflict must be resolved individually.
Never use git merge -s ours or git read-tree --reset — this destroys git blame attribution.
⚠️ MANDATORY: Before resolving ANY conflict, check file history for fork-specific patches.
Run git log --oneline skiasharp -- <conflicted-file> — if the log shows intentional
fork patches, keep our version. See gotcha #15 for details.
| File Category | Strategy |
|---|---|
BUILD.gn | Combine both — keep upstream structure AND SkiaSharp's platform flags + skiasharp_build target |
DEPS | Combine — keep our dependency pins, accept upstream structure |
RELEASE_NOTES.md, infra/ | Take upstream |
C API (include/c/, src/c/) | Keep SkiaSharp — adapt includes/API calls in post-merge commits |
Other upstream source (src/, include/) | Check history first — see gotcha #15 |
Commit the merge:
git commit # Creates proper two-parent merge
Verify our C API files survived the merge:
ls src/c/*.cpp include/c/*.h # All files should still exist
Source file verification — Check for added/deleted upstream files:
git diff upstream/chrome/m{CURRENT}..upstream/chrome/m{TARGET} --diff-filter=AD --name-only -- src/ include/
Cross-reference against BUILD.gn — new source files may need to be added.
🛑 GATE: Merge complete, conflicts resolved. Verify:
bashls src/c/*.cpp include/c/*.h # C API files intact git diff --check # Zero conflict markers git blame src/c/sk_canvas.cpp | head -20 # Attribution shows original commits, not just merge
✅ Before proceeding to C (Update & Build):
- Parent branch is based on
origin/main- Submodule is at the SHA referenced by the parent's
origin/mainsubmodule pointer- Upstream merge committed with proper two-parent history
- C API files intact, zero conflict markers
⚠️ This MUST be done before any native build. The build scripts verify version consistency — if VERSIONS.txt still says the old milestone, the build will fail.
Note: The script automatically resets
SK_C_INCREMENTto0when the milestone changes. If you had a pending increment that must survive, capture it before running.
📋 This phase is handled by a script. The script updates VERSIONS.txt, cgmanifest.json, azure-templates-variables.yml, and verifies SK_C_INCREMENT — then runs the mandatory verification greps. It exits non-zero if any stale references remain.
In the SkiaSharp parent repo, run:
cd ../.. # back to parent repo (Phase 5 ends inside externals/skia)
pwsh .agents/skills/update-skia/scripts/update-versions.ps1 -Current {CURRENT} -Target {TARGET}
The script handles all of these (so you don't have to do them manually):
scripts/VERSIONS.txt: milestone, increment→0, soname, assembly, file, ALL ~30 nuget linescgmanifest.json: commitHash, version, chrome_milestone, upstream_merge_commitscripts/azure-templates-variables.yml: SKIASHARP_VERSION (must match VERSIONS.txt nuget version)SK_C_INCREMENT is 0 in externals/skia/include/c/sk_types.hgrep verification — fails if any stale references remain🛑 GATE: Script exits with ✅. If it exits with ❌, fix the reported stale references and re-run until it passes.
Note: The SK_C_INCREMENT reset modifies a file in the submodule (
externals/skia/). Don't commit it separately — it will be committed with Phase 7's C API fixes.
This is where most of the work happens. The C API (src/c/, include/c/) wraps Skia C++ and
must be updated when the underlying C++ APIs change.
❌ NEVER use
externals-downloadduring a milestone update. It downloads pre-built binaries from the OLD milestone that don't contain your C API changes. Always build from source withexternals-{platform}.
Restore tools and attempt to build to identify all compilation errors:
dotnet tool restore
dotnet cake --target=externals-{platform} --arch={arch}
Replace {platform} with your OS (macos, linux, windows) and {arch} with your architecture (arm64, x64).
Fix each error following these patterns:
| Error Type | Fix Pattern |
|---|---|
| Missing type | Add/update typedef in sk_types.h |
| Renamed function | Update call in *.cpp |
| Removed enum value | Remove from sk_enums.cpp + sk_types.h. Note this for Phase 9 — it needs [Obsolete] or documented removal |
| Changed signature | Update C wrapper function signature |
| New header required | Add #include in the relevant .cpp |
| Legacy flag breaks C API | Update C API to use replacement API (see gotcha #6). Do not just comment out the flag without a plan |
Update sk_types.h for any new enums or type changes.
Phase 6 reset SK_C_INCREMENT to 0. Only bump it if you add new C API functions in this milestone.
The build enforces that SK_C_INCREMENT matches libSkiaSharp increment in VERSIONS.txt.
Build again — iterate until clean compilation.
🛑 GATE: Native library builds successfully on at least one platform.
✅ Before proceeding to D (Regenerate & Verify):
- Version files updated (Phase 6 script passed)
- Native library builds cleanly
Prerequisite: Phase 7's native build must have completed at least once — it runs
git-sync-deps, which fetches HarfBuzz and other headers the generator needs.
📋 This phase is handled by a script. The script runs the generator, IMMEDIATELY reverts HarfBuzz bindings (HarfBuzz updates are always separate), reports what changed, and lists any new functions that may need C# wrappers.
pwsh .agents/skills/update-skia/scripts/regenerate-bindings.ps1
The script handles all of these (so you don't forget any):
pwsh ./utils/generate.ps1binding/HarfBuzzSharp/HarfBuzzApi.generated.cs (proactively, not reactively)After the script completes, build C# to verify compilation:
dotnet build binding/SkiaSharp/SkiaSharp.csproj
🛑 GATE: Script prints
✅ Phase 8 complete. C# build succeeds with 0 errors.
The C# build can pass with 0 errors while new C API functions remain invisible to users.
New functions compile fine as unused internal static methods in the generated file, but
without C# wrappers they're not part of the public API. This phase applies even when
the build succeeds.
Review new generated bindings for unwrapped functions:
git diff origin/main -- binding/SkiaSharp/SkiaApi.generated.cs | grep "^+.*internal static"
⚠️ The
git diff origin/mainmay show additional changes beyond new functions (e.g. struct renames, type changes from Phase 7 shim work). These are expected and correct. Only investigate+internal staticlines — ignore other diff noise.
Check whether each new function has a C# wrapper:
# Example: if sk_foo_bar was added, check for a wrapper
grep -rn "sk_foo_bar" binding/SkiaSharp/*.cs | grep -v generated
New functions from our custom C API additions typically need wrappers. New functions from upstream changes are usually additive and can be deferred.
Fix files in binding/SkiaSharp/ based on the breaking change analysis:
| File | When to Update |
|---|---|
Definitions.cs | New enums, types, or constants |
EnumMappings.cs | New enum values that need C#↔C mapping |
GRDefinitions.cs | Graphics context changes (Ganesh) |
SKImage.cs | SkImage factory changes |
SKTypeface.cs | SkTypeface API changes |
SKFont.cs | SkFont API changes |
SKCanvas.cs | Canvas drawing API changes |
Key rules:
[Obsolete] for deprecated APIs with migration guidancenull from factory methods on failure (don't throw)# Rebuild native only if you touched C API files in Phase 9
# (Phase 7 already built — skip if no native changes since then)
dotnet cake --target=externals-{platform} --arch={arch}
# Build C#
dotnet build binding/SkiaSharp/SkiaSharp.csproj
Smoke tests (fast gate, ~100ms):
dotnet test tests/SkiaSharp.Tests.Console/SkiaSharp.Tests.Console.csproj --filter "Category=Smoke"
Smoke tests verify basic native interop: version compatibility, object creation, drawing, image loading, fonts, codecs, effects, and more. If these fail, something fundamental is broken — go back and fix before wasting time on the full suite.
⚠️ If the version compatibility smoke test fails with "incompatible native library", you missed a version update — go back to Phase 6 and verify ALL version lines. Do NOT work around this with
--no-incrementalor by copying native libs manually.
Full test suite (required before any PR):
dotnet test tests/SkiaSharp.Tests.Console/SkiaSharp.Tests.Console.csproj 2>&1 | tee /tmp/gh-aw/agent/test-output.txt
Wait for it to finish (takes 5–7 min). Then read the summary:
tail -5 /tmp/gh-aw/agent/test-output.txt
The last line will look like: Passed! - Failed: 0, Passed: 5435, Skipped: 171, Total: 5606
This runs all test projects (core, Vulkan, Direct3D). Backend-specific tests self-skip when hardware isn't available. CI handles WASM/Android/iOS separately.
⚠️ These MUST be two separate commands. Do NOT combine them into a single pipeline like
| tee ... | tail— the piped tail runs immediately and will show nothing useful while tests are still running. Capture withteefirst, wait for completion, thentailthe output file. After the run, inspect failures with:bashgrep '^ Failed' /tmp/gh-aw/agent/test-output.txt
Smoke tests are just that — smoke. They verify the basics. The full suite MUST pass before the update can be considered complete. Do not create PRs with only smoke tests passing.
🛑 GATE: ALL tests pass (full suite, not just smoke). Do NOT skip failing tests. Do NOT proceed with failures.
✅ Before proceeding to E (Ship):
- Bindings regenerated (Phase 8 script passed)
- C# builds with 0 errors
- ALL tests pass (full suite)
Same-milestone bug-fix syncs: When
CURRENT == TARGET, onlycgmanifest.json'scommitHash/upstream_merge_commitchange. Use PR titles like[skia-sync] Merge upstream chrome/m{TARGET} bug fixesinstead of milestone-bump titles.
| Field | Value |
|---|---|
| Branch | skia-sync/m{TARGET} |
| Target | skiasharp |
| Title | [skia-sync] Merge upstream chrome/m{TARGET} |
| Field | Value |
|---|---|
| Branch | skia-sync/m{TARGET} |
| Target | main |
| Title | [skia-sync] Update skia to milestone {TARGET} |
Submodule must point to the mono/skia PR branch.
After creating BOTH PRs, update the earlier PR's description to include a link to the later one. Both PRs must reference each other.
Before proceeding to merge, verify ALL of these:
skia-sync/m{TARGET} convention in BOTH reposskiasharp branchmain branchexternals/skia submodule points to the mono/skia PR branch (git submodule status)cgmanifest.json updated with new commit hash, version, and chrome_milestonescripts/VERSIONS.txt updated (ALL version lines, not just milestone)SkiaApi.generated.cs regenerated and committedskiasharpBefore proceeding past each step, verify:
skiasharp branch to get new squashed SHAcd externals/skia && git fetch origin && git checkout {new-sha})skiasharp branch (not an orphaned branch commit)❌ NEVER merge both PRs without updating the submodule in between. ❌ NEVER assume the submodule reference is correct after squash-merging mono/skia.
These files contain lookup information — consult them when you hit a problem or need context, not necessarily upfront: