Skip to content

feat: add a san-francisco-font feature - #20

Merged
ashwin153 merged 6 commits into
mainfrom
claude/devcontainer-san-francisco-font-46y0hu
Aug 28, 2026
Merged

feat: add a san-francisco-font feature#20
ashwin153 merged 6 commits into
mainfrom
claude/devcontainer-san-francisco-font-46y0hu

Conversation

@ashwin153

@ashwin153 ashwin153 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Softlight Overview

UX Score: 5/5

Softlight reviewed this change and found no issues.

To run Softlight again, click here or comment @softlight.

Installs Apple's San Francisco fonts and makes fontconfig answer the system font names with them, so Chromium renders pages the way macOS does instead of falling back to Liberation Sans.

What it installs

  • SF Pro and SF Mono under /usr/local/share/fonts/san-francisco, root-owned and read-only to the remote user.
  • /etc/fonts/conf.avail/59-san-francisco.conf, symlinked into conf.d so the substitution can be turned off without uninstalling the fonts.

SF Pro goes in as the variable pair (SF-Pro.ttf, SF-Pro-Italic.ttf), not the 44 static instances sitting beside them in the same download. One pair carries every weight from Ultralight to Black and every width from Compressed to Expanded, and its optical size axis is what makes text pick up the Display drawing at headline sizes and the Text drawing at body sizes the way it does on macOS. It is 40MB against 270MB for the same coverage; the image layer ends up ~41MB.

Apple ships each family as a disk image holding an installer package whose payload is a gzipped cpio archive. The build unpacks all three layers with 7-Zip, using include filters so the payload is never expanded in full, then purges the archiver again if the feature is what pulled it in.

The part that isn't obvious

Chromium never asks fontconfig for sans-serif. It resolves the CSS generic against its own default font preference — on Linux, the literal string Arial — and asks fontconfig for that. So aliasing the generic alone changes nothing a page can see:

request rules for sans-serif only with webSafeSans (default)
fc-match sans-serif SF Pro SF Pro
Chromium font-family: sans-serif Liberation Sans SF Pro

That is why webSafeSans is on by default and routes Arial, Helvetica, Helvetica Neue, Segoe UI and Roboto to SF Pro. The cost is that a page explicitly asking for Arial gets San Francisco too — there is no signal in what reaches fontconfig that separates Chromium's generic from a page's deliberate Arial. monospace needs no equivalent, because Chromium's default fixed font is Monospace, which fontconfig already treats as the generic.

Two smaller findings, both written up in NOTES.md:

  • The file is numbered 59 to land before 60-latin.conf. Both files put their preference in front of the generic name rather than at the head of the list, so between two preferences for sans-serif the one read first wins — the opposite of the usual "last config wins".
  • Arial and Helvetica already resolve, strongly, through 30-metric-aliases.conf. A weak <prefer> loses to that, so those two take the head of the family list with a matching binding instead. Everything else uses a weak preference, so a page naming a font the image actually has still gets that font.

Serifs are left alone: serif, Times New Roman, Georgia and Courier New resolve to whatever the image had, as does a page with no font-family at all.

Verification

Run against the real Chromium the playwright feature installs (measuring rendered text widths per family, since fontconfig agreeing is necessary but not sufficient):

sans-serif          => SF Pro          Arial               => SF Pro
system-ui           => SF Pro          Helvetica           => SF Pro
-apple-system       => SF Pro          "Segoe UI"          => SF Pro
BlinkMacSystemFont  => SF Pro          Roboto              => SF Pro
monospace           => SF Mono         serif               => Liberation Serif
ui-monospace        => SF Mono         "Times New Roman"   => Liberation Serif

install.sh was also run end to end on a clean host in all three option combinations, including the archiver install-and-purge path and a re-run over an existing install.

Three test scenarios: the default, mono: false + webSafeSans: false, and one alongside the playwright feature that drives headless Chromium and asserts the generics actually render as San Francisco. That last check was confirmed to fail when the fontconfig rules are removed, so it is testing something.

Licensing

These are Apple's fonts under Apple's licence, which allows use in designing and developing software but not redistribution. The feature fetches them from Apple's CDN at build time for that reason — nothing is vendored into this repository. Worth a look before the built image goes anywhere outside the team.

The download URL has no version in it and no published checksum, so there is nothing to pin; a rebuild picks up whatever Apple is shipping that day. The install fails loudly rather than half-installing if a face it expects is no longer in the download.


Generated by Claude Code


Note

Medium Risk
Build-time fetches unpinned Apple font URLs (layout/content can change), installs licensed fonts only at build time, and globally changes fontconfig resolution—including overriding common web fonts when webSafeSans is enabled.

Overview
Adds a new san-francisco-font devcontainer feature that downloads Apple's SF Pro (and optionally SF Mono) at image build time, installs the variable font pair under /usr/local/share/fonts/san-francisco, and writes 59-san-francisco.conf so fontconfig maps system/CSS font names to San Francisco.

install.sh pulls from Apple's CDN via curl, unpacks DMG → pkg → gzipped cpio with 7-Zip (installed only if needed, then purged), fails the build if expected faces are missing, and generates fontconfig rules using weak prefer aliases plus strong override matches when webSafeSans is on (default)—so Chromium's Linux default of Arial for sans-serif actually renders SF Pro, not just fc-match sans-serif. mono (default on) controls SF Mono install and monospace-related aliases; serifs are intentionally untouched.

Ships devcontainer-feature.json with those two boolean options, README.md / NOTES.md, and tests: default Ubuntu scenario (fc-match + permissions + archiver cleanup), mono: false / webSafeSans: false, and a Playwright Chromium headless canvas width probe to verify rendered generics match SF Pro/SF Mono.

Reviewed by Cursor Bugbot for commit 217769e. Configure here.

claude added 2 commits August 28, 2026 03:22
Installs Apple's San Francisco fonts and makes fontconfig answer the system
font names with them, so Chromium renders pages the way macOS does.

SF Pro ships as the variable pair rather than the 44 static instances beside
it in the same download: one pair carries every weight and width, its optical
size axis gives the Text/Display switch by size, and it costs 40MB instead of
270MB.

The webSafeSans option is on by default because Chromium never asks fontconfig
for sans-serif -- it resolves the CSS generic against its own default of
"Arial" and asks for that -- so aliasing the generic alone changes nothing a
page can see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tsvoi8deWWfYWkLMUAGbP1
Nothing behavioural. install.sh loses its comments the way the other four
have none -- the reasoning was already in NOTES.md, and the two paragraphs
that were not are there now -- and picks up their shape: one constants block,
INSTALL_PATH for the install root, a linear apt-get update at the top and the
lists removed at the end, chown and chmod last.

The tests come down to the sibling comment density, and use INSTALL_PATH like
test/grok/ubuntu.sh does. The feature name is title case alongside BuildBuddy
and TypeScript Language Server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tsvoi8deWWfYWkLMUAGbP1
@ashwin153
ashwin153 enabled auto-merge (squash) August 28, 2026 03:29
claude added 2 commits August 28, 2026 03:33
The feature test failed on ubuntu 26.04 with "no font matching SF-Pro.ttf",
having created no fonts directory at all.

How deep the fonts sit depends on the 7-Zip build. 7-Zip 23, which is what
ubuntu 24.04 ships, unwraps the payload's gzip and cpio together while
extracting the installer package. 7-Zip 26, on ubuntu 26.04, stops at the gzip
stream and needs a fourth pass -- so the extraction that was aimed at the cpio
was aimed at a gzip wrapper instead.

It failed silently rather than loudly because that extraction passed include
filters naming the faces to keep: no entry at that layer matched, so 7-Zip
extracted nothing, created no output directory, and exited 0.

Extract a layer at a time and stop at the first one holding font files, which
covers both. The filters go with it -- their only benefit was scratch space
inside a build layer that is discarded anyway, against a failure mode that is
invisible until the nesting shifts.

Verified end to end against both 7-Zip 23.01 and 26.00.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tsvoi8deWWfYWkLMUAGbP1
Function names carry it. What is left is the pair that stops someone
simplifying a test into one that cannot fail: why the render probe exists
alongside the fc-match checks, and why its marker is assembled at run time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tsvoi8deWWfYWkLMUAGbP1

Copy link
Copy Markdown
Contributor Author

Correcting one paragraph of the description above, which no longer matches the code. It says:

The build unpacks all three layers with 7-Zip, using include filters so the payload is never expanded in full

Both halves of that are now wrong, and the reason is worth recording since it's the bug the feature test caught.

The depth is not three, and it is not fixed. How deep the fonts sit depends on the 7-Zip build. 7-Zip 23, which Ubuntu 24.04 ships and which I developed against, unwraps the payload's gzip and cpio together while extracting the installer package. 7-Zip 26, on the Ubuntu 26.04 base image this repo tests on, stops at the gzip stream and needs a fourth pass — so the extraction aimed at the cpio was aimed at a gzip wrapper instead.

The include filters are gone, and they are why it failed quietly rather than loudly. That last extraction passed filters naming the faces to keep. No entry at that layer matched them, so 7-Zip extracted nothing, created no output directory, and exited 0 — surfacing three lines later as no font matching SF-Pro.ttf ... apple changed the font set, which pointed at Apple rather than at the nesting. Their only benefit was scratch space inside a build layer that is discarded anyway, against a failure mode invisible until the nesting shifts. Not a good trade.

install.sh now extracts one layer at a time and stops at the first one holding font files, which covers both versions. Verified end to end against 7-Zip 23.01 and 26.00 — 14 files, 41MB, identical fontconfig resolution and a passing Chromium render probe on each.

NOTES.md in the diff carries this; the stale wording is only in the description, which matters if the squash message is taken from it.

Everything else in the description still holds, including the webSafeSans reasoning and the ~41MB image cost.


Generated by Claude Code

claude added 2 commits August 28, 2026 03:45
Two scenarios failed on the ubuntu26.04 base while passing locally, both
because a check leaned on fonts that image does not carry.

ubuntu_sans_only asserted Arial is left alone with webSafeSans off. But
turning the option off only stops these rules sending Arial to SF Pro; it
cannot stop fontconfig. With no arial-metric font installed -- this box has
Liberation Sans, that image has nothing -- Arial matches nothing, falls
through to the sans-serif generic and reaches SF Pro on its own. Assert
instead that the rules were not written, which is what the option decides,
and narrow the monospace check to SF Mono, which cannot be satisfied by
accident because mono: false never installs it.

ubuntu_with_chromium guarded against a vacuous pass by requiring SF Pro to
measure differently from DejaVu Sans, which that image also lacks. Compare
SF Pro against SF Mono instead: both are ours, both are installed in this
scenario, and a run where the sans and mono generics land on two different
correct fonts is not a fallback collapse.

The probe now reports every measurement it took rather than a bare verdict,
so a future failure names the family that moved instead of needing a
reproduction. Moving it to its own file rather than an inline script is what
makes that readable: --dump-dom no longer prints the source next to the
result, which also retires the marker that had to be assembled at run time to
avoid matching itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tsvoi8deWWfYWkLMUAGbP1
The chromium probe failed in three milliseconds having printed nothing, which
is not a render failing -- it is the browser never being found. Playwright now
ships chrome-for-testing, which unpacks to chrome-linux64/chrome; the pattern
looked for chrome-linux/chrome, the layout of the older build still installed
on my machine. Match both, fall back to the headless shell, and say what the
directory does hold when neither is there, so the next layout change reports
itself instead of looking like a failed assertion.

Probing the headless shell then turned up a real gap. The two binaries do not
resolve monospace the same way: the full browser's default fixed font is
Monospace, which fontconfig treats as the generic, but the headless shell's is
Courier New. So font-family: monospace rendered as Liberation Mono there --
under exactly the binary playwright launches headless, which is the case this
feature exists for. Same shape as the sans-serif indirection Arial already
covers, so mono now routes Courier New to SF Mono too.

The tests probe every browser they find rather than the first, which is what
turned this up, and each report is printed per binary. Confirmed to fail on
the headless shell alone with the Courier New rule removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tsvoi8deWWfYWkLMUAGbP1
@ashwin153
ashwin153 merged commit 0bfc3a7 into main Aug 28, 2026
2 checks passed
@ashwin153
ashwin153 deleted the claude/devcontainer-san-francisco-font-46y0hu branch August 28, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants