nerd-fonts-apple-hinted

MEDIUM
maintainer gaougalos 0 votes scanned 2026-09-22 11:37:52.799854
View on AUR
Why flagged

One or more source=() URLs point to a host outside the trusted allowlist (github.com, gitlab.com, codeberg.org, pypi.org, …).

Triggered rules

Medium source=() URL on a non-standard host source_untrusted_domain

One or more source=() URLs point to a host outside the trusted allowlist (github.com, gitlab.com, codeberg.org, pypi.org, …).

  • PKGBUILD:20 "SF-Pro.dmg::https://devimages-cdn.apple.com/design/resources/download/SF-Pro.dmg"

PKGBUILD

1 offending line(s) highlighted
1# Maintainer: gaou-piou <i.am.piou@gmail.com>
2pkgname=nerd-fonts-apple-hinted
3_nfver=3.5.1
4# pkgver is derived at build time by pkgver() below: the highest font version
5# found inside the selected Apple fonts, plus the Nerd Fonts patcher version
6# (e.g. 22.0.5.4+nf3.5.1). The literal here is only a placeholder for the first
7# run and for tooling that parses the PKGBUILD without executing it.
8pkgver=22.0.5.4+nf3.5.1
9pkgrel=1
10pkgdesc="Apple fonts (SF Pro, SF Compact, SF Mono, SF Arabic, NY), optionally Nerd Fonts patched and/or autohinted (ttfautohint / CFF) with gasp tuning"
11arch=(any)
12url="https://developer.apple.com/fonts/"
13license=("custom")
14makedepends=("p7zip" "python" "python-fonttools" "python-skia-pathops" "parallel" "fontforge" "ttfautohint" "libarchive")
15optdepends=("afdko: better CFF/OTF hinting via otfautohint (HINT_ENGINE=cff)")
16options=(!strip !zipman)
17conflicts=("nerd-fonts-apple" "apple-fonts" "nerd-fonts-sf-mono")
18provides=("nerd-fonts-apple" "ttf-font-nerd")
19source=(
20 "SF-Pro.dmg::https://devimages-cdn.apple.com/design/resources/download/SF-Pro.dmg"
21 "SF-Compact.dmg::https://devimages-cdn.apple.com/design/resources/download/SF-Compact.dmg"
22 "SF-Mono.dmg::https://devimages-cdn.apple.com/design/resources/download/SF-Mono.dmg"
23 "SF-Arabic.dmg::https://devimages-cdn.apple.com/design/resources/download/SF-Arabic.dmg"
24 "NY.dmg::https://devimages-cdn.apple.com/design/resources/download/NY.dmg"
25 "font-patcher-${_nfver}::https://github.com/ryanoasis/nerd-fonts/releases/download/v${_nfver}/FontPatcher.zip"
26 "otf2ttf.py"
27 "set_gasp.py"
28 "cff_hint.py"
29 "dedup_blues.py"
30 "font_meta.py"
31)
32# Apple re-spins the DMGs without versioning, so those legitimately stay SKIP —
33# the real identity check is pkgver(), which reads the version out of the font
34# files themselves. FontPatcher is a tagged GitHub release — pin it once you have
35# a known-good hash (replace the 6th SKIP). The *.py helpers are vendored locally
36# (no network fetch), so they are covered by makepkg's local-file hashing —
37# fill these in with `updpkgsums`.
38sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP'
39 'SKIP'
40 'SKIP'
41 'SKIP'
42 'SKIP'
43 'SKIP'
44 'SKIP')
45
46# ── Versioning ──────────────────────────────────────────────────────────────
47#
48# pkgver() composes the version from the fonts themselves plus the patcher:
49#
50# <highest Apple font version>+nf<nerd fonts version> e.g. 22.0.5.4+nf3.5.1
51#
52# The Apple part is read out of the OpenType `name` table (nameID 5, e.g.
53# "Version 22.0d5e4" → 22.0.5.4) of the *selected* families, taking the highest.
54# Apple versions each family independently (SF Pro 22.0d5e4, SF Mono 18.0d1e1,
55# New York 16.0d2e2), so taking the max keeps pkgver monotonic: any family Apple
56# bumps raises the package version, while a re-spin that changes nothing leaves
57# it untouched. Note that pkgver therefore depends on which families you select.
58#
59# ── Build-time options (all settable via env vars) ──────────────────────────
60#
61# NERD_PATCH true/false Patch fonts with Nerd Fonts glyphs (default: false)
62# false → install fonts as-is (static OTF or variable TTF)
63#
64# WANT_VARIABLE true/false Include variable TTF fonts when NERD_PATCH=false.
65# (Variable fonts are excluded when patching — font-patcher
66# destroys fvar/gvar axes during generation.)
67# Ignored when NERD_PATCH=true.
68#
69# HINTING true/false Apply autohinting (default: true)
70# When NERD_PATCH=true: hints the patched TTF output.
71# When NERD_PATCH=false + ttfautohint engine: converts
72# static OTF→TTF first, then hints. Variable TTFs are
73# hinted directly (axes are flattened to a static TTF).
74# When NERD_PATCH=false + cff engine: hints OTFs in place,
75# output stays OTF (no TTF conversion).
76#
77# HINT_ENGINE ttfautohint|cff Hinting backend (default: ttfautohint)
78# ttfautohint: TrueType bytecode hints (best for screen/
79# terminal/Latin; requires TTF, converts OTF→TTF).
80# cff: PostScript/CFF hints, OTF output preserved. Uses
81# otfautohint (afdko) if present, else falls back
82# to FontForge AutoHint. Forced to ttfautohint when
83# NERD_PATCH=true (patched fonts are always TTF).
84#
85# ── ttfautohint tuning ──
86# HINT_PRESET name Named bundle of ttfautohint flags (default: balanced)
87# balanced stem=qsq range 8-50 x-height 14 (upstream defaults)
88# terminal stem=qss range 6-50 x-height 10 +gasp (Hack-style, small mono)
89# ui stem=qsq range 8-60 x-height 14 +gasp (proportional screen text)
90# print stem=nnn range 8-50 x-height 0 (natural stems, no x-snap)
91# light stem=nnn range 12-50 x-height 0 (HiDPI, minimal interference)
92# strong stem=sss range 6-50 x-height 14 +gasp (aggressive low-DPI)
93# custom use the individual HINT_* vars below verbatim
94#
95# Individual ttfautohint knobs (override the preset; empty = use preset value):
96# HINT_MODE nnn|qqq|qsq|sss 3-char --stem-width-mode (grayscale/GDI/DW; n/q/s)
97# HINT_RANGE_MIN N --hinting-range-min (default 8)
98# HINT_RANGE_MAX N --hinting-range-max (default 50)
99# HINT_LIMIT N --hinting-limit (default 200; 0 = no limit)
100# HINT_XHEIGHT N --increase-x-height (default 14; 0 = disable x-height snapping bump)
101# HINT_XSNAP_EXC STR --x-height-snapping-exceptions (e.g. "13-17", "" = none)
102# HINT_WINCOMPAT true/false --windows-compatibility (artificial blue zones)
103# HINT_TTFA_TABLE true/false -t/--ttfa-table (embed a TTFA table with all params)
104# HINT_FAMILY_SUFFIX STR -F/--family-suffix (distinguish differently-hinted builds)
105# (PUA/Nerd icon glyphs are always handled with --fallback-script=none --fallback-scaling.)
106#
107# ── gasp grid-fitting / anti-aliasing tuning (TTF output only) ──
108# GASP_MODE keep|sized|smooth|gridfit (default: keep)
109# keep leave the gasp table as ttfautohint/font-patcher wrote it
110# sized MS strategy: <=8 grayscale, 9-16 gridfit, 17+ both
111# smooth all sizes anti-aliased ({0xFFFF:15}; gftools style)
112# gridfit all sizes gridfit+grayscale ({0xFFFF:3})
113# Some presets enable a sensible GASP_MODE automatically (see above);
114# an explicit GASP_MODE always wins.
115#
116# Font family selection:
117# WANT_SF_PRO true/false (default: true)
118# WANT_SF_COMPACT true/false (default: false)
119# WANT_SF_MONO true/false (default: true)
120# WANT_SF_ARABIC true/false (default: false)
121# WANT_NY true/false (default: true)
122#
123# Subfamily selection (space-separated, empty = all):
124# SF_PRO_SUBS e.g. "Display Text Rounded"
125# SF_COMPACT_SUBS e.g. "Display Text Rounded"
126# NY_SUBS e.g. "Small Medium Large ExtraLarge"
127#
128# Non-interactive examples:
129# HINTING=true HINT_PRESET=terminal WANT_SF_MONO=true WANT_SF_PRO=false makepkg -si
130# NERD_PATCH=false HINTING=true HINT_ENGINE=cff makepkg -si
131# HINTING=true HINT_PRESET=custom HINT_MODE=qss HINT_RANGE_MIN=6 GASP_MODE=sized makepkg -si
132# ────────────────────────────────────────────────────────────────────────────
133
134_validate_hint_mode() {
135 if [[ ! "$HINT_MODE" =~ ^[nqs]{3}$ ]]; then
136 echo "Error: HINT_MODE='$HINT_MODE' is invalid."
137 echo " Must be 3 chars from: n (natural) q (quantized) s (strong)"
138 echo " Examples: nnn qqq qsq sss"
139 exit 1
140 fi
141}
142
143# Map a named preset into the individual HINT_* / GASP_MODE vars.
144# Individual vars already set in the environment are preserved (preset never
145# overrides an explicit user value).
146_apply_preset() {
147 local _p="$1"
148 local _m _lo _hi _lim _xh _gasp
149 case "$_p" in
150 balanced) _m=qsq; _lo=8; _hi=50; _lim=200; _xh=14; _gasp=keep ;;
151 terminal) _m=qss; _lo=6; _hi=50; _lim=200; _xh=10; _gasp=sized ;;
152 ui) _m=qsq; _lo=8; _hi=60; _lim=200; _xh=14; _gasp=sized ;;
153 print) _m=nnn; _lo=8; _hi=50; _lim=200; _xh=0; _gasp=keep ;;
154 light) _m=nnn; _lo=12; _hi=50; _lim=200; _xh=0; _gasp=keep ;;
155 strong) _m=sss; _lo=6; _hi=50; _lim=200; _xh=14; _gasp=smooth ;;
156 custom) _m=qsq; _lo=8; _hi=50; _lim=200; _xh=14; _gasp=keep ;;
157 *)
158 echo "Error: unknown HINT_PRESET='$_p'."
159 echo " Valid: balanced terminal ui print light strong custom"
160 exit 1 ;;
161 esac
162 HINT_MODE="${HINT_MODE:-$_m}"
163 HINT_RANGE_MIN="${HINT_RANGE_MIN:-$_lo}"
164 HINT_RANGE_MAX="${HINT_RANGE_MAX:-$_hi}"
165 HINT_LIMIT="${HINT_LIMIT:-$_lim}"
166 HINT_XHEIGHT="${HINT_XHEIGHT:-$_xh}"
167 GASP_MODE="${GASP_MODE:-$_gasp}"
168}
169
170# ── DMG extraction ──────────────────────────────────────────────────────────
171# Apple ships two different DMG layouts and re-spins the images without bumping
172# any version, so both must be handled (verified by 7z l):
173#
174# "folder" layout (SF-Mono, SF-Arabic, NY — and SF-Pro/SF-Compact before
175# Sept 2026): the DMG holds a <Family>Fonts/ directory containing a
176# "<Family> Fonts.pkg" metapackage plus a <Family>Fonts.pkg/Payload:
177# NY.dmg → NYFonts/ "NY Fonts.pkg" → NYFonts.pkg/Payload
178#
179# "flat" layout (SF-Pro, SF-Compact as of Sept 2026): the DMG holds a single
180# <Family>Fonts.pkg *file* at the root, itself an xar archive whose inner
181# component package carries the payload:
182# SF-Pro.dmg → SFProFonts.pkg → SFProFontsPackage.pkg/Payload
183#
184# Rather than hard-coding either shape, we extract the DMG and then locate the
185# Payload(s) by search, which should survive the next re-spin too. Extraction
186# happens in prepare() so that pkgver() can read the fonts' own version strings.
187_extract_dmg() {
188 local _archive="$1" _dest="$2"
189 local _stem _work _pass _pkg _payload _found=0
190 _stem="$(basename "$_archive" .dmg)"
191 _work="$srcdir/dmg/$_stem"
192
193 rm -rf "$_work"; mkdir -p "$_work" "$_dest"
194 (
195 set -e
196 cd "$_work"
197 7z x "$_archive" >/dev/null
198
199 # Unwrap every xar/pkg we can find, repeatedly: the "folder" layout nests a
200 # metapackage one level deep, the "flat" layout wraps the component package
201 # in an outer one. Three passes cover both with room to spare; each pass only
202 # touches .pkg entries that are regular files (the *.pkg/ payload directories
203 # are left alone).
204 for _pass in 1 2 3; do
205 while IFS= read -r -d '' _pkg; do
206 # No -P: do not preserve absolute paths from Apple's installer payload.
207 bsdtar xf "$_pkg" -C "$(dirname "$_pkg")" 2>/dev/null || true
208 rm -f "$_pkg"
209 done < <(find . -type f -name '*.pkg' ! -name '._*' -print0)
210 done
211
212 # Payload is a gzip'd cpio; there may be more than one (e.g. a family whose
213 # installer splits Display/Text into separate component packages).
214 while IFS= read -r -d '' _payload; do
215 bsdtar xf "$_payload" -C "$(dirname "$_payload")" 2>/dev/null || continue
216 _found=1
217 done < <(find . -type f -name 'Payload' ! -path '*/._*' -print0)
218
219 if (( _found == 0 )); then
220 echo "Error: no installer Payload found in $(basename "$_archive")." >&2
221 echo " Apple may have changed the DMG layout again. Inspect with:" >&2
222 echo " 7z l '$_archive'" >&2
223 exit 1
224 fi
225
226 find . \( -iname '*.ttf' -o -iname '*.otf' -o -iname '*.ttc' \) \
227 ! -path '*/._*' ! -name '._*' -exec cp -n {} "$_dest/" \;
228 ) || return 1
229
230 rm -rf "$_work"
231 return 0
232}
233
234# Extract every selected DMG into $srcdir/extracted. Used by prepare(); build()
235# reuses the result rather than extracting twice.
236_extract_selected() {
237 local _archive _stem _before _after
238 mkdir -p "$srcdir/extracted" "$srcdir/dmg"
239
240 declare -A _dmg_want=(
241 [SF-Pro]="$WANT_SF_PRO"
242 [SF-Compact]="$WANT_SF_COMPACT"
243 [SF-Mono]="$WANT_SF_MONO"
244 [SF-Arabic]="$WANT_SF_ARABIC"
245 [NY]="$WANT_NY"
246 )
247
248 for _archive in "$srcdir"/*.dmg; do
249 _stem="$(basename "$_archive" .dmg)"
250 if [[ "${_dmg_want[$_stem]:-false}" != true ]]; then
251 echo "==> Skipping: $(basename "$_archive") (not selected)"
252 continue
253 fi
254 echo "==> Extracting: $(basename "$_archive")"
255 _before=$(find "$srcdir/extracted" -type f | wc -l)
256 _extract_dmg "$_archive" "$srcdir/extracted" \
257 || { echo "Error: extraction failed for $_stem."; exit 1; }
258 # Fail loud per family rather than letting an empty extraction surface much
259 # later as a confusing "no fonts matched the selection".
260 _after=$(find "$srcdir/extracted" -type f | wc -l)
261 if (( _after == _before )); then
262 echo "Error: $_stem extracted no font files."; exit 1
263 fi
264 echo " -> $(( _after - _before )) font file(s)"
265 done
266 rm -rf "$srcdir/dmg"
267}
268
269_show() { printf " [%s] %s\n" "$1" "$2"; }
270_ask_yn() { local _default="${2:-n}"; read -p " $1? (default: $_default) " _yn; _yn="${_yn:-$_default}"; }
271_ask_multi() {
272 local _label="$1"; shift; local _opts=("$@"); local _n=${#_opts[@]}
273 echo " Subfamilies (space-separated numbers, or Enter/a for all):"
274 for (( i=0; i<_n; i++ )); do printf " %d) %s\n" "$((i+1))" "${_opts[$i]}"; done
275 read -p " > " _raw; _sel=""
276 if [[ "$_raw" =~ ^[aA]$ || -z "$_raw" ]]; then
277 _sel="${_opts[*]}"
278 else
279 for _tok in $_raw; do
280 local _idx=$(( _tok - 1 ))
281 (( _idx >= 0 && _idx < _n )) && _sel="$_sel ${_opts[$_idx]}"
282 done
283 _sel="${_sel# }"
284 fi
285}
286
287# Resolve every build option: prompt when interactive, otherwise fall back to
288# environment variables and defaults. Called from prepare() (so the answers are
289# known before pkgver() needs them) and re-read from disk by build().
290_resolve_options() {
291 # ══════════════════════════════════════════════════════════════════════════
292 # Interactivity guard: only prompt when connected to a terminal. In a clean
293 # chroot / CI / --noconfirm build stdin is not a TTY; prompting there would
294 # read EOF and silently fall through, so we go straight to env vars/defaults.
295 # ══════════════════════════════════════════════════════════════════════════
296 local _interactive=false
297 if [[ -t 0 ]]; then _interactive=true; fi
298
299 if [[ "$_interactive" == true ]]; then
300 echo ""
301 echo "╔══════════════════════════════════════════════════════╗"
302 echo "║ nerd-fonts-apple-hinted — build options ║"
303 echo "╚══════════════════════════════════════════════════════╝"
304 else
305 echo "==> Non-interactive build (no TTY): using environment variables / defaults."
306 fi
307
308 # ── Nerd Fonts patching ──────────────────────────────────────────────────
309 if [[ "$_interactive" == true && -z "${NERD_PATCH:-}" ]]; then
310 _ask_yn "Patch fonts with Nerd Fonts glyphs" n
311 [[ "$_yn" =~ ^[yY]$ ]] && NERD_PATCH=true || NERD_PATCH=false
312 fi
313 NERD_PATCH="${NERD_PATCH:-false}"
314
315 # ── Variable fonts (only relevant when not patching) ─────────────────────
316 if [[ "$_interactive" == true && "$NERD_PATCH" == false && -z "${WANT_VARIABLE:-}" ]]; then
317 echo ""
318 echo " Variable fonts (SF Pro, SF Compact, New York) contain all weights"
319 echo " in a single file with fvar/gvar axes. They are excluded when"
320 echo " patching because font-patcher destroys variable axes."
321 _ask_yn "Include variable TTF fonts" n
322 [[ "$_yn" =~ ^[yY]$ ]] && WANT_VARIABLE=true || WANT_VARIABLE=false
323 fi
324 # Variable fonts are never compatible with Nerd Fonts patching
325 if [[ "$NERD_PATCH" == true ]]; then WANT_VARIABLE=false; fi
326 WANT_VARIABLE="${WANT_VARIABLE:-false}"
327
328 # ── Hinting ──────────────────────────────────────────────────────────────
329 if [[ "$_interactive" == true && -z "${HINTING:-}" ]]; then
330 echo ""
331 if [[ "$NERD_PATCH" == false ]]; then
332 echo " Note: ttfautohint engine hints TrueType, so static OTFs are"
333 echo " converted OTF→TTF first; variable TTFs are flattened to static."
334 echo " The 'cff' engine hints OTFs in place and keeps them as OTF."
335 fi
336 _ask_yn "Apply autohinting" y
337 [[ "$_yn" =~ ^[yY]$ ]] && HINTING=true || HINTING=false
338 fi
339 HINTING="${HINTING:-true}"
340
341 # ── Hinting engine ─────────────────────────────────────────────────────────
342 if [[ "$HINTING" == true && "$NERD_PATCH" == false && "$_interactive" == true && -z "${HINT_ENGINE:-}" ]]; then
343 echo ""
344 echo " Hinting engine:"
345 PS3=" > "
346 select _choice in "ttfautohint (TrueType bytecode, converts OTF→TTF)" \
347 "cff (PostScript/CFF hints, keeps OTF)"; do
348 case "$REPLY" in
349 1) HINT_ENGINE=ttfautohint; break ;;
350 2) HINT_ENGINE=cff; break ;;
351 *) echo " Please enter 1 or 2." ;;
352 esac
353 [[ -z "$REPLY" ]] && { echo " No selection; defaulting to ttfautohint."; HINT_ENGINE=ttfautohint; break; }
354 done
355 fi
356 HINT_ENGINE="${HINT_ENGINE:-ttfautohint}"
357 # Patched fonts are always TTF → ttfautohint only.
358 if [[ "$NERD_PATCH" == true ]]; then HINT_ENGINE=ttfautohint; fi
359 if [[ "$HINT_ENGINE" != ttfautohint && "$HINT_ENGINE" != cff ]]; then
360 echo "Error: HINT_ENGINE='$HINT_ENGINE' invalid (use 'ttfautohint' or 'cff')."; exit 1
361 fi
362
363 # ── Hinting preset (ttfautohint only) ──────────────────────────────────────
364 if [[ "$HINTING" == true && "$HINT_ENGINE" == ttfautohint ]]; then
365 if [[ "$_interactive" == true && -z "${HINT_PRESET:-}" && -z "${HINT_MODE:-}" ]]; then
366 echo ""
367 echo " Hinting preset:"
368 PS3=" > "
369 select _choice in "balanced (upstream defaults, qsq 8-50 x14)" \
370 "terminal (mono/small, qss 6-50 x10 +gasp)" \
371 "ui (proportional screen, qsq 8-60 x14 +gasp)" \
372 "print (natural stems, nnn, no x-snap)" \
373 "light (HiDPI minimal, nnn 12-50)" \
374 "strong (aggressive low-DPI, sss +smooth gasp)" \
375 "custom (set HINT_* vars yourself)"; do
376 case "$REPLY" in
377 1) HINT_PRESET=balanced; break ;;
378 2) HINT_PRESET=terminal; break ;;
379 3) HINT_PRESET=ui; break ;;
380 4) HINT_PRESET=print; break ;;
381 5) HINT_PRESET=light; break ;;
382 6) HINT_PRESET=strong; break ;;
383 7) HINT_PRESET=custom; break ;;
384 *) echo " Please enter 1-7." ;;
385 esac
386 [[ -z "$REPLY" ]] && { echo " No selection; defaulting to balanced."; HINT_PRESET=balanced; break; }
387 done
388 fi
389 HINT_PRESET="${HINT_PRESET:-balanced}"
390 _apply_preset "$HINT_PRESET"
391 _validate_hint_mode
392
393 # Validate numeric knobs.
394 for _v in HINT_RANGE_MIN HINT_RANGE_MAX HINT_LIMIT HINT_XHEIGHT; do
395 if [[ ! "${!_v}" =~ ^[0-9]+$ ]]; then
396 echo "Error: $_v='${!_v}' must be a non-negative integer."; exit 1
397 fi
398 done
399 if (( HINT_RANGE_MIN > HINT_RANGE_MAX )); then
400 echo "Error: HINT_RANGE_MIN ($HINT_RANGE_MIN) > HINT_RANGE_MAX ($HINT_RANGE_MAX)."; exit 1
401 fi
402 fi
403
404 # gasp default + validation (used by both engines, but only meaningful for TTF)
405 GASP_MODE="${GASP_MODE:-keep}"
406 case "$GASP_MODE" in
407 keep|sized|smooth|gridfit) ;;
408 *) echo "Error: GASP_MODE='$GASP_MODE' invalid (keep|sized|smooth|gridfit)."; exit 1 ;;
409 esac
410
411 HINT_WINCOMPAT="${HINT_WINCOMPAT:-false}"
412 HINT_TTFA_TABLE="${HINT_TTFA_TABLE:-false}"
413
414 # ── Font family & subfamily selection ────────────────────────────────────
415 if [[ "$_interactive" == true && \
416 -z "${WANT_SF_PRO:-}${WANT_SF_COMPACT:-}${WANT_SF_MONO:-}${WANT_SF_ARABIC:-}${WANT_NY:-}" ]]; then
417 echo ""
418 echo " ── Font families ────────────────────────────────────────"
419
420 _ask_yn "Include SF Pro" y
421 if [[ "$_yn" =~ ^[yY]$ ]]; then
422 WANT_SF_PRO=true
423 if [[ -z "${SF_PRO_SUBS:-}" ]]; then
424 echo " SF Pro subfamilies:"
425 _ask_multi "SF Pro" "Display" "Text" "Rounded"
426 SF_PRO_SUBS="$_sel"
427 fi
428 else
429 WANT_SF_PRO=false
430 fi
431
432 _ask_yn "Include SF Compact" n
433 if [[ "$_yn" =~ ^[yY]$ ]]; then
434 WANT_SF_COMPACT=true
435 if [[ -z "${SF_COMPACT_SUBS:-}" ]]; then
436 echo " SF Compact subfamilies:"
437 _ask_multi "SF Compact" "Display" "Text" "Rounded"
438 SF_COMPACT_SUBS="$_sel"
439 fi
440 else
441 WANT_SF_COMPACT=false
442 fi
443
444 _ask_yn "Include SF Mono" y
445 [[ "$_yn" =~ ^[yY]$ ]] && WANT_SF_MONO=true || WANT_SF_MONO=false
446
447 _ask_yn "Include SF Arabic" n
448 [[ "$_yn" =~ ^[yY]$ ]] && WANT_SF_ARABIC=true || WANT_SF_ARABIC=false
449
450 _ask_yn "Include New York" y
451 if [[ "$_yn" =~ ^[yY]$ ]]; then
452 WANT_NY=true
453 if [[ -z "${NY_SUBS:-}" ]]; then
454 echo " New York subfamilies:"
455 _ask_multi "New York" "Small" "Medium" "Large" "ExtraLarge"
456 NY_SUBS="$_sel"
457 fi
458 else
459 WANT_NY=false
460 fi
461 fi
462
463 WANT_SF_PRO="${WANT_SF_PRO:-true}"
464 WANT_SF_COMPACT="${WANT_SF_COMPACT:-false}"
465 WANT_SF_MONO="${WANT_SF_MONO:-true}"
466 WANT_SF_ARABIC="${WANT_SF_ARABIC:-false}"
467 WANT_NY="${WANT_NY:-true}"
468
469 if [[ "$WANT_SF_PRO$WANT_SF_COMPACT$WANT_SF_MONO$WANT_SF_ARABIC$WANT_NY" != *true* ]]; then
470 echo "Error: no font families selected. Aborting."; exit 1
471 fi
472
473 # ── Build summary ─────────────────────────────────────────────────────────
474 echo ""
475 echo " ── Build summary ───────────────────────────────────────"
476 printf " NERD_PATCH: %s\n" "$NERD_PATCH"
477 printf " WANT_VARIABLE: %s\n" "$WANT_VARIABLE"
478 if [[ "$HINTING" == true ]]; then
479 printf " HINTING: true (engine=%s)\n" "$HINT_ENGINE"
480 if [[ "$HINT_ENGINE" == ttfautohint ]]; then
481 printf " preset=%s stem=%s range=%s-%s limit=%s x-height=%s\n" \
482 "$HINT_PRESET" "$HINT_MODE" "$HINT_RANGE_MIN" "$HINT_RANGE_MAX" "$HINT_LIMIT" "$HINT_XHEIGHT"
483 printf " x-snap-exc='%s' win-compat=%s ttfa-table=%s%s\n" \
484 "${HINT_XSNAP_EXC-<default>}" "$HINT_WINCOMPAT" "$HINT_TTFA_TABLE" \
485 "${HINT_FAMILY_SUFFIX:+ family-suffix='$HINT_FAMILY_SUFFIX'}"
486 fi
487 printf " gasp=%s\n" "$GASP_MODE"
488 else
489 printf " HINTING: false\n"
490 fi
491 echo ""
492 _show "$( [[ "$WANT_SF_PRO" == true ]] && echo "x" || echo " " )" "SF Pro ${SF_PRO_SUBS:+(subfamilies: $SF_PRO_SUBS)}"
493 _show "$( [[ "$WANT_SF_COMPACT" == true ]] && echo "x" || echo " " )" "SF Compact ${SF_COMPACT_SUBS:+(subfamilies: $SF_COMPACT_SUBS)}"
494 _show "$( [[ "$WANT_SF_MONO" == true ]] && echo "x" || echo " " )" "SF Mono"
495 _show "$( [[ "$WANT_SF_ARABIC" == true ]] && echo "x" || echo " " )" "SF Arabic"
496 _show "$( [[ "$WANT_NY" == true ]] && echo "x" || echo " " )" "New York ${NY_SUBS:+(subfamilies: $NY_SUBS)}"
497 echo " ────────────────────────────────────────────────────────"
498 echo ""
499
500 # Persist the resolved options so build() and package() reuse the same answers
501 # instead of re-prompting (makepkg runs each function in a fresh shell).
502 {
503 echo "NERD_PATCH=$NERD_PATCH"
504 echo "WANT_VARIABLE=$WANT_VARIABLE"
505 echo "HINTING=$HINTING"
506 echo "HINT_ENGINE=$HINT_ENGINE"
507 echo "HINT_PRESET=${HINT_PRESET:-}"
508 echo "HINT_MODE=${HINT_MODE:-}"
509 echo "HINT_RANGE_MIN=${HINT_RANGE_MIN:-}"
510 echo "HINT_RANGE_MAX=${HINT_RANGE_MAX:-}"
511 echo "HINT_LIMIT=${HINT_LIMIT:-}"
512 echo "HINT_XHEIGHT=${HINT_XHEIGHT:-}"
513 echo "HINT_WINCOMPAT=$HINT_WINCOMPAT"
514 echo "HINT_TTFA_TABLE=$HINT_TTFA_TABLE"
515 echo "GASP_MODE=$GASP_MODE"
516 echo "WANT_SF_PRO=$WANT_SF_PRO"
517 echo "WANT_SF_COMPACT=$WANT_SF_COMPACT"
518 echo "WANT_SF_MONO=$WANT_SF_MONO"
519 echo "WANT_SF_ARABIC=$WANT_SF_ARABIC"
520 echo "WANT_NY=$WANT_NY"
521 echo "SF_PRO_SUBS=${SF_PRO_SUBS:-}"
522 echo "SF_COMPACT_SUBS=${SF_COMPACT_SUBS:-}"
523 echo "NY_SUBS=${NY_SUBS:-}"
524 # These two are optional and must distinguish "unset" from "set to empty".
525 # `if` rather than `&&` so an unset variable is not a non-zero exit status
526 # for the whole group under makepkg's `set -e`.
527 if [[ "${HINT_XSNAP_EXC+set}" == set ]]; then
528 echo "HINT_XSNAP_EXC=$HINT_XSNAP_EXC"
529 fi
530 if [[ -n "${HINT_FAMILY_SUFFIX:-}" ]]; then
531 echo "HINT_FAMILY_SUFFIX=$HINT_FAMILY_SUFFIX"
532 fi
533 } > "$srcdir/.build_opts"
534}
535
536# Load the options persisted by _resolve_options(). Keys are whitelisted so a
537# stray line in the file can never inject an arbitrary variable.
538_load_options() {
539 local _k _v
540 [[ -f "$srcdir/.build_opts" ]] || { echo "Error: missing .build_opts; run prepare() first."; exit 1; }
541 while IFS='=' read -r _k _v; do
542 case "$_k" in
543 NERD_PATCH|WANT_VARIABLE|HINTING|HINT_ENGINE|HINT_PRESET|HINT_MODE| \
544 HINT_RANGE_MIN|HINT_RANGE_MAX|HINT_LIMIT|HINT_XHEIGHT|HINT_WINCOMPAT| \
545 HINT_TTFA_TABLE|HINT_XSNAP_EXC|HINT_FAMILY_SUFFIX|GASP_MODE| \
546 WANT_SF_PRO|WANT_SF_COMPACT|WANT_SF_MONO|WANT_SF_ARABIC|WANT_NY| \
547 SF_PRO_SUBS|SF_COMPACT_SUBS|NY_SUBS)
548 printf -v "$_k" '%s' "$_v" ;;
549 esac
550 done < "$srcdir/.build_opts"
551}
552
553prepare() {
554 _resolve_options
555
556 # ══════════════════════════════════════════════════════════════════════════
557 # Step 1 — Extract fonts from Apple DMG archives (selected families only)
558 # ══════════════════════════════════════════════════════════════════════════
559 # Extraction lives in prepare() rather than build() so pkgver() — which runs
560 # in between — can read the version out of the extracted font files.
561 rm -rf "$srcdir/extracted" "$srcdir/converted" "$srcdir/patched" \
562 "$srcdir/selected" "$srcdir/dmg"
563 mkdir -p "$srcdir/extracted" "$srcdir/converted" "$srcdir/patched" \
564 "$srcdir/selected"
565 _extract_selected
566}
567
568pkgver() {
569 # Compose the version from what the fonts themselves declare, combined with
570 # the Nerd Fonts patcher version:
571 #
572 # <highest Apple font version>+nf<nerd fonts version> e.g. 22.0.5.4+nf3.5.1
573 #
574 # Apple's per-family versions differ (SF Pro 22.0d5e4, SF Mono 18.0d1e1,
575 # NY 16.0d2e2), so we take the highest across the *selected* families. That
576 # keeps pkgver monotonic: any family Apple bumps raises the package version,
577 # and re-spins that change nothing leave it alone.
578 local _apple
579 _apple="$(python "$srcdir/font_meta.py" version "$srcdir/extracted/"* 2>/dev/null)" || _apple=""
580 if [[ -z "$_apple" ]]; then
581 # Never fail the build over a version string: fall back to the previous
582 # pkgver so the package still builds if Apple ships unparseable metadata.
583 echo "Warning: could not read a font version; keeping $pkgver" >&2
584 echo "$pkgver"
585 return 0
586 fi
587 # '-' is not allowed in pkgver; '+' and '.' are.
588 printf '%s+nf%s\n' "${_apple//-/.}" "${_nfver//-/.}"
589}
590
591build() {
592 _load_options
593
594
595 # ══════════════════════════════════════════════════════════════════════════
596 # Step 2 — Filter by family/subfamily and variable-font preference
597 # ══════════════════════════════════════════════════════════════════════════
598 # Selection is driven by each font's OpenType `name` table (typographic family,
599 # nameID 16, falling back to nameID 1) rather than by filename patterns. Apple
600 # has already changed both the installer layout and file naming between
601 # re-spins, but the internal family names ("SF Pro Text", "New York Small",
602 # "SF Mono") have been stable across every release we have seen — and they are
603 # what fontconfig ends up showing the user anyway.
604 #
605 # The family map is built once with a single python call; the shell then only
606 # does string matching, so a new weight or a renamed file is picked up for free.
607 declare -A _fam_of=()
608 local _file _fam
609 while IFS=$'\t' read -r _file _fam; do
610 if [[ -n "$_file" ]]; then _fam_of["$_file"]="$_fam"; fi
611 done < <(python "$srcdir/font_meta.py" families "$srcdir/extracted/"* 2>/dev/null)
612
613 if (( ${#_fam_of[@]} == 0 )); then
614 echo "Error: could not read family names from any extracted font." >&2
615 echo " Is python-fonttools installed and are the fonts intact?" >&2
616 exit 1
617 fi
618
619 # Does family string $1 belong to base family $2 with one of subfamilies $3?
620 # Empty $3 means "any subfamily of this base family".
621 _family_matches() {
622 local _fam="$1" _base="$2" _subs="$3" _sub
623 # Exact base family (e.g. "SF Mono", or the variable "SF Pro") …
624 [[ "$_fam" == "$_base" ]] && { [[ -z "$_subs" ]] && return 0 || return 1; }
625 # … or "<base> <subfamily>" (e.g. "SF Pro Text", "New York Small").
626 [[ "$_fam" == "$_base "* ]] || return 1
627 [[ -z "$_subs" ]] && return 0
628 local _rest="${_fam#"$_base" }"
629 for _sub in $_subs; do
630 # Compare ignoring spaces so "ExtraLarge" matches "Extra Large".
631 [[ "${_rest// /}" == "${_sub// /}" ]] && return 0
632 done
633 return 1
634 }
635
636 # Copy every extracted font whose family matches, honouring the static/variable
637 # split: variable fonts are TTFs with an fvar axis and are only wanted when
638 # NERD_PATCH=false and WANT_VARIABLE=true (font-patcher destroys fvar/gvar).
639 _select_family() {
640 local _want="$1" _base="$2" _subs="$3"
641 [[ "$_want" != true ]] && return 0
642 local _f _bn _fam _n=0
643 for _f in "$srcdir/extracted"/*; do
644 [[ -f "$_f" ]] || continue
645 _bn="$(basename "$_f")"
646 [[ "$_bn" == ._* ]] && continue
647 _fam="${_fam_of[$_bn]:-}"
648 [[ -n "$_fam" ]] || continue
649 _family_matches "$_fam" "$_base" "$_subs" || continue
650 # Apple ships the variable faces as .ttf and the static faces as .otf.
651 if [[ "$_bn" == *.ttf || "$_bn" == *.TTF ]]; then
652 [[ "$NERD_PATCH" == true || "$WANT_VARIABLE" != true ]] && continue
653 fi
654 cp -n "$_f" "$srcdir/selected/" && _n=$(( _n + 1 ))
655 done
656 if (( _n == 0 )); then
657 echo "Warning: no fonts matched '$_base'${_subs:+ (subfamilies: $_subs)}." >&2
658 # List what was actually available so a renamed subfamily is obvious.
659 local _avail
660 _avail="$(printf '%s\n' "${_fam_of[@]}" | grep -F "$_base" | sort -u | paste -sd', ')"
661 if [[ -n "$_avail" ]]; then echo " Available: $_avail" >&2; fi
662 fi
663 return 0
664 }
665
666 # SF Mono and SF Arabic have no subfamily split; the rest are filtered by the
667 # *_SUBS variables (empty = every subfamily).
668 _select_family "$WANT_SF_MONO" "SF Mono" ""
669 _select_family "$WANT_SF_ARABIC" "SF Arabic" ""
670 _select_family "$WANT_SF_PRO" "SF Pro" "$SF_PRO_SUBS"
671 _select_family "$WANT_SF_COMPACT" "SF Compact" "$SF_COMPACT_SUBS"
672 _select_family "$WANT_NY" "New York" "$NY_SUBS"
673
674 local _sel_count; _sel_count=$(find "$srcdir/selected" -type f | wc -l)
675 echo "==> Selected $_sel_count font files for processing."
676 if (( _sel_count == 0 )); then
677 echo "Error: no fonts matched the selection. Check subfamily names."; exit 1
678 fi
679
680 # ══════════════════════════════════════════════════════════════════════════
681 # Step 3 — OTF → TTF conversion (only for the ttfautohint engine, no patch)
682 # ══════════════════════════════════════════════════════════════════════════
683 # ttfautohint requires TrueType glyf outlines. otf2ttf.py (cu2qu) converts CFF
684 # cubics to quadratic splines while preserving the name table. The 'cff' engine
685 # keeps OTFs intact and is handled in step 5.
686 local _patchsrc
687 if [[ "$NERD_PATCH" == false && "$HINTING" == true && "$HINT_ENGINE" == ttfautohint ]]; then
688 printf "%b" "\e[1;34m==> INFO:\e[0m Converting static OTF→TTF for ttfautohint...\n"
689 find "$srcdir/selected" -name '*.otf' ! -name '._*' | sort | \
690 parallel --will-cite -j"$(nproc)" \
691 'python "'"$srcdir/otf2ttf.py"'" --post-format 2.0 --max-error 1.0 \
692 -o "'"$srcdir/converted"'/{/.}.ttf" "{}"'
693 # Variable TTFs and native TTFs pass through unchanged
694 find "$srcdir/selected" -name '*.ttf' ! -name '._*' \
695 -exec cp -n {} "$srcdir/converted/" \;
696 _patchsrc="$srcdir/converted"
697 else
698 # NERD_PATCH=true → font-patcher converts implicitly.
699 # cff engine / no-hint → operate on selected fonts directly.
700 _patchsrc="$srcdir/selected"
701 fi
702
703 # ══════════════════════════════════════════════════════════════════════════
704 # Step 4 — Nerd Fonts patching (skipped when NERD_PATCH=false)
705 # ══════════════════════════════════════════════════════════════════════════
706 local _hintsrc
707 if [[ "$NERD_PATCH" == true ]]; then
708 printf "%b" "\e[1;33m==> WARNING:\e[0m Patching with Nerd Fonts glyphs — this takes a while...\n"
709
710 # SF Mono: -s (--mono) preserves monospace cell-width metrics.
711 # stderr is kept (only stdout silenced) so patch failures are visible, and we
712 # assert non-empty output afterwards rather than masking with `|| true`.
713 find "$_patchsrc" \( -name 'SF-Mono-*.otf' -o -name 'SF-Mono-*.ttf' \) | sort | \
714 parallel --will-cite -j4 python "$srcdir/font-patcher" \
715 --variable-width-glyphs -q -c -s {} \
716 -out "$srcdir/patched" '>/dev/null'
717
718 find "$_patchsrc" \( -name '*.otf' -o -name '*.ttf' \) ! -name 'SF-Mono-*' | sort | \
719 parallel --will-cite -j4 python "$srcdir/font-patcher" \
720 --variable-width-glyphs -q -c {} \
721 -out "$srcdir/patched" '>/dev/null'
722
723 # font-patcher preserves the input outline format: OTF/CFF inputs (all the
724 # Apple static fonts, including SF Mono) yield patched *.otf; only TrueType
725 # inputs yield *.ttf. Count both, and fail loud if nothing came out.
726 local _patched_count
727 _patched_count=$(find "$srcdir/patched" -maxdepth 1 \( -name '*.ttf' -o -name '*.otf' \) | wc -l)
728 if (( _patched_count == 0 )); then
729 echo "Error: font-patcher produced no output. See messages above."; exit 1
730 fi
731 echo "==> Patched $_patched_count font(s)."
732
733 # The ttfautohint engine needs TrueType glyf outlines, so convert any patched
734 # OTFs to TTF in place. (The cff engine only runs when NERD_PATCH=false, so a
735 # patched tree never feeds it.)
736 if [[ "$HINTING" == true && "$HINT_ENGINE" == ttfautohint ]] && \
737 compgen -G "$srcdir/patched/*.otf" >/dev/null; then
738 printf "%b" "\e[1;34m==> INFO:\e[0m Converting patched OTF→TTF for ttfautohint...\n"
739 find "$srcdir/patched" -maxdepth 1 -name '*.otf' ! -name '._*' | sort | \
740 parallel --will-cite -j"$(nproc)" \
741 'python "'"$srcdir/otf2ttf.py"'" --post-format 2.0 --max-error 1.0 \
742 -o "'"$srcdir/patched"'/{/.}.ttf" "{}" && rm -f "{}"'
743 fi
744 _hintsrc="$srcdir/patched"
745 else
746 _hintsrc="$_patchsrc"
747 fi
748
749 # ══════════════════════════════════════════════════════════════════════════
750 # Step 5 — Hinting
751 # ══════════════════════════════════════════════════════════════════════════
752 if [[ "$HINTING" == true && "$HINT_ENGINE" == cff && "$NERD_PATCH" == false ]]; then
753 # ── CFF/OTF hinting: keep OTF outlines, no TTF conversion ──
754 mkdir -p "$srcdir/patched/hinted"
755 local _have_otfautohint=false
756 command -v otfautohint >/dev/null 2>&1 && _have_otfautohint=true
757 if [[ "$_have_otfautohint" == true ]]; then
758 printf "%b" "\e[1;32m==> INFO:\e[0m CFF hinting via otfautohint (AFDKO)...\n"
759 else
760 printf "%b" "\e[1;32m==> INFO:\e[0m CFF hinting via FontForge AutoHint (install afdko for otfautohint)...\n"
761 fi
762 # Two Apple-specific quirks make stock otfautohint abort on these fonts:
763 #
764 # 1. Overlapping alignment zones. otfautohint reads the CFF Private-dict
765 # zones and FATALLY rejects any that overlap or sit within 1+2*BlueFuzz
766 # units. Apple's SF Pro/Compact ship DUPLICATE/OVERLAPPING zones (e.g.
767 # SF Pro Text BlueValues lists (1443,1477) twice; OtherBlues has
768 # (-34,0)⊃(-19,0)) → "top of zone X overlaps zone Y". dedup_blues.py
769 # collapses those redundant pairs into a spec-conformant, non-overlapping
770 # set (outlines untouched).
771 #
772 # 2. closeSegs assertion on overlapping contours. otfautohint has an open
773 # bug (afdko #1795, unfixed through 5.0.1) where `assert n and p` in
774 # hinter.py crashes on glyphs with overlapping/degenerate contours — the
775 # heavy SF *-Black weights hit this (e.g. uni10094E.large). Passing
776 # --force-overlap routes glyphs through otfautohint's overlap-correction
777 # path during analysis and avoids the crash WITHOUT altering the emitted
778 # outlines (verified bit-identical on normal glyphs).
779 #
780 # otfautohint preserves the name table; the FontForge AutoHint path
781 # (cff_hint.py) is kept only as a last-resort fallback (it repairs the names
782 # FontForge mangles) so a font is never silently dropped from the package.
783 for _f in "$_hintsrc"/*.otf; do
784 [[ -f "$_f" ]] || continue
785 local _out="$srcdir/patched/hinted/$(basename "$_f")"
786 if [[ "$_have_otfautohint" == true ]]; then
787 python "$srcdir/dedup_blues.py" "$_f"
788 if ! otfautohint --force-overlap -o "$_out" "$_f" 2>/dev/null; then
789 printf "%b" "\e[1;33m==> WARNING:\e[0m otfautohint failed on $(basename "$_f"); using FontForge AutoHint instead.\n"
790 rm -f "$_out"
791 python "$srcdir/cff_hint.py" "$_f" "$_out"
792 fi
793 else
794 python "$srcdir/cff_hint.py" "$_f" "$_out"
795 fi
796 done
797 # Variable / native TTFs cannot get CFF hints — pass them through untouched.
798 for _f in "$_hintsrc"/*.ttf; do
799 [[ -f "$_f" ]] || continue
800 cp -n "$_f" "$srcdir/patched/hinted/"
801 done
802
803 elif [[ "$HINTING" == true ]]; then
804 # ── ttfautohint engine (TTF) ──
805 # PUA glyphs (SF Symbols / Nerd icons): --fallback-script=none +
806 # --fallback-scaling scale them proportionally instead of mis-hinting as Latin.
807 printf "%b" "\e[1;32m==> INFO:\e[0m ttfautohint (preset=$HINT_PRESET, stem=$HINT_MODE, range=$HINT_RANGE_MIN-$HINT_RANGE_MAX, x-height=$HINT_XHEIGHT)...\n"
808 mkdir -p "$srcdir/patched/hinted"
809
810 # Assemble optional flags.
811 local -a _ta_opts=(
812 --hinting-range-min="$HINT_RANGE_MIN"
813 --hinting-range-max="$HINT_RANGE_MAX"
814 --hinting-limit="$HINT_LIMIT"
815 --increase-x-height="$HINT_XHEIGHT"
816 --fallback-script=none
817 --fallback-scaling
818 --stem-width-mode="$HINT_MODE"
819 --ignore-restrictions
820 )
821 # x-height-snapping-exceptions: honour an explicit value (including empty
822 # string = no exceptions); omit entirely if unset to use ttfautohint's default.
823 if [[ "${HINT_XSNAP_EXC+set}" == set ]]; then
824 _ta_opts+=(--x-height-snapping-exceptions="$HINT_XSNAP_EXC")
825 fi
826 if [[ "$HINT_WINCOMPAT" == true ]]; then _ta_opts+=(--windows-compatibility); fi
827 if [[ -n "${HINT_FAMILY_SUFFIX:-}" ]]; then _ta_opts+=(--family-suffix="$HINT_FAMILY_SUFFIX"); fi
828 if [[ "$HINT_TTFA_TABLE" == true ]]; then
829 _ta_opts+=(--ttfa-table)
830 else
831 _ta_opts+=(--no-info)
832 fi
833
834 for _f in "$_hintsrc"/*.ttf; do
835 [[ -f "$_f" ]] || continue
836 ttfautohint "${_ta_opts[@]}" "$_f" "$srcdir/patched/hinted/$(basename "$_f")"
837 done
838 fi
839
840 # ══════════════════════════════════════════════════════════════════════════
841 # Step 6 — gasp grid-fitting / anti-aliasing tuning (TTF output only)
842 # ══════════════════════════════════════════════════════════════════════════
843 if [[ "$HINTING" == true && "$GASP_MODE" != keep ]]; then
844 local _gasp_dir
845 if [[ "$HINT_ENGINE" == cff && "$NERD_PATCH" == false ]]; then
846 _gasp_dir="$srcdir/patched/hinted" # only the .ttf passthroughs here have a gasp
847 else
848 _gasp_dir="$srcdir/patched/hinted"
849 fi
850 if compgen -G "$_gasp_dir/*.ttf" >/dev/null; then
851 printf "%b" "\e[1;36m==> INFO:\e[0m Applying gasp mode '$GASP_MODE' to TTF output...\n"
852 python "$srcdir/set_gasp.py" "$GASP_MODE" "$_gasp_dir"/*.ttf
853 fi
854 fi
855
856}
857
858package() {
859 # Reuse the single source of truth written by prepare(); makepkg runs each
860 # function in a fresh shell, so nothing is inherited.
861 local HINTING NERD_PATCH WANT_VARIABLE HINT_ENGINE
862 _load_options
863
864 install -d "$pkgdir/usr/share/fonts/apple"
865
866 if [[ "$HINTING" == true ]]; then
867 # Hinted output (TTF and/or OTF for the cff engine) lives in patched/hinted/.
868 # Fail loud if it is empty — never silently ship a fontless package.
869 if compgen -G "$srcdir/patched/hinted/*.ttf" >/dev/null; then
870 install -m644 "$srcdir/patched/hinted/"*.ttf "$pkgdir/usr/share/fonts/apple"
871 fi
872 if compgen -G "$srcdir/patched/hinted/*.otf" >/dev/null; then
873 install -m644 "$srcdir/patched/hinted/"*.otf "$pkgdir/usr/share/fonts/apple"
874 fi
875 if ! compgen -G "$pkgdir/usr/share/fonts/apple/*" >/dev/null; then
876 echo "Error: hinting was requested but produced no fonts."; exit 1
877 fi
878
879 elif [[ "$NERD_PATCH" == true ]]; then
880 # Patched but not hinted: font-patcher emits TTF for TTF inputs and OTF for
881 # OTF inputs (e.g. SF Mono), so install both. patched/hinted/ doesn't exist
882 # on this path, so the shallow globs won't pick up hinted output.
883 if compgen -G "$srcdir/patched/*.ttf" >/dev/null; then
884 install -m644 "$srcdir/patched/"*.ttf "$pkgdir/usr/share/fonts/apple"
885 fi
886 if compgen -G "$srcdir/patched/*.otf" >/dev/null; then
887 install -m644 "$srcdir/patched/"*.otf "$pkgdir/usr/share/fonts/apple"
888 fi
889 if ! compgen -G "$pkgdir/usr/share/fonts/apple/*" >/dev/null; then
890 echo "Error: patching was requested but produced no fonts."; exit 1
891 fi
892
893 else
894 # No patching, no hinting: install selected fonts directly.
895 if compgen -G "$srcdir/selected/*.otf" >/dev/null; then
896 install -m644 "$srcdir/selected/"*.otf "$pkgdir/usr/share/fonts/apple"
897 fi
898 if [[ "$WANT_VARIABLE" == true ]] && compgen -G "$srcdir/selected/*.ttf" >/dev/null; then
899 install -m644 "$srcdir/selected/"*.ttf "$pkgdir/usr/share/fonts/apple"
900 fi
901 if ! compgen -G "$pkgdir/usr/share/fonts/apple/*" >/dev/null; then
902 echo "Error: no fonts were installed."; exit 1
903 fi
904 fi
905
906 install -d "$pkgdir/usr/share/licenses/$pkgname"
907 cat > "$pkgdir/usr/share/licenses/$pkgname/LICENSE" <<'EOF'
908Apple San Francisco, SF Compact, SF Mono, SF Arabic, and New York fonts
909are proprietary fonts owned by Apple Inc. They are provided for use on
910Apple platforms and non-Apple platforms for personal, non-commercial use.
911See https://developer.apple.com/fonts/ for the full license agreement.
912
913Nerd Fonts glyphs are licensed under their respective open-source licenses.
914See https://github.com/ryanoasis/nerd-fonts for details.
915EOF
916}
917

Changes since previous scan

--- PKGBUILD @ 2026-06-19 19:07
+++ PKGBUILD @ 2026-09-22 11:37
@@ -1,13 +1,18 @@
# Maintainer: gaou-piou <i.am.piou@gmail.com>
pkgname=nerd-fonts-apple-hinted
-_nfver=3.4.0
-pkgver=3.4.0
+_nfver=3.5.1
+# pkgver is derived at build time by pkgver() below: the highest font version
+# found inside the selected Apple fonts, plus the Nerd Fonts patcher version
+# (e.g. 22.0.5.4+nf3.5.1). The literal here is only a placeholder for the first
+# run and for tooling that parses the PKGBUILD without executing it.
+pkgver=22.0.5.4+nf3.5.1
pkgrel=1
-pkgdesc="Apple fonts (SF Pro, SF Compact, SF Mono, SF Arabic, NY), optionally Nerd Fonts patched and/or ttfautohint hinted"
+pkgdesc="Apple fonts (SF Pro, SF Compact, SF Mono, SF Arabic, NY), optionally Nerd Fonts patched and/or autohinted (ttfautohint / CFF) with gasp tuning"
arch=(any)
url="https://developer.apple.com/fonts/"
license=("custom")
-makedepends=("p7zip" "python" "python-fonttools" "parallel" "fontforge" "ttfautohint" "libarchive")
+makedepends=("p7zip" "python" "python-fonttools" "python-skia-pathops" "parallel" "fontforge" "ttfautohint" "libarchive")
+optdepends=("afdko: better CFF/OTF hinting via otfautohint (HINT_ENGINE=cff)")
options=(!strip !zipman)
conflicts=("nerd-fonts-apple" "apple-fonts" "nerd-fonts-sf-mono")
provides=("nerd-fonts-apple" "ttf-font-nerd")
@@ -18,13 +23,42 @@
"SF-Arabic.dmg::https://devimages-cdn.apple.com/design/resources/download/SF-Arabic.dmg"
"NY.dmg::https://devimages-cdn.apple.com/design/resources/download/NY.dmg"
"font-patcher-${_nfver}::https://github.com/ryanoasis/nerd-fonts/releases/download/v${_nfver}/FontPatcher.zip"
- "otf2ttf.py::https://raw.githubusercontent.com/fonttools/fonttools/main/Snippets/otf2ttf.py"
+ "otf2ttf.py"
+ "set_gasp.py"
+ "cff_hint.py"
+ "dedup_blues.py"
+ "font_meta.py"
)
-sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP')
-
+# Apple re-spins the DMGs without versioning, so those legitimately stay SKIP —
+# the real identity check is pkgver(), which reads the version out of the font
+# files themselves. FontPatcher is a tagged GitHub release — pin it once you have
+# a known-good hash (replace the 6th SKIP). The *.py helpers are vendored locally
+# (no network fetch), so they are covered by makepkg's local-file hashing —
+# fill these in with `updpkgsums`.
+sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP'
+ 'SKIP'
+ 'SKIP'
+ 'SKIP'
+ 'SKIP'
+ 'SKIP'
+ 'SKIP')
+
+# ── Versioning ──────────────────────────────────────────────────────────────
+#
+# pkgver() composes the version from the fonts themselves plus the patcher:
+#
+# <highest Apple font version>+nf<nerd fonts version> e.g. 22.0.5.4+nf3.5.1
+#
+# The Apple part is read out of the OpenType `name` table (nameID 5, e.g.
+# "Version 22.0d5e4" → 22.0.5.4) of the *selected* families, taking the highest.
+# Apple versions each family independently (SF Pro 22.0d5e4, SF Mono 18.0d1e1,
+# New York 16.0d2e2), so taking the max keeps pkgver monotonic: any family Apple
+# bumps raises the package version, while a re-spin that changes nothing leaves
+# it untouched. Note that pkgver therefore depends on which families you select.
+#
# ── Build-time options (all settable via env vars) ──────────────────────────
#
-# NERD_PATCH true/false Patch fonts with Nerd Fonts glyphs (default: true)
+# NERD_PATCH true/false Patch fonts with Nerd Fonts glyphs (default: false)
# false → install fonts as-is (static OTF or variable TTF)
#
# WANT_VARIABLE true/false Include variable TTF fonts when NERD_PATCH=false.
@@ -32,16 +66,52 @@
# destroys fvar/gvar axes during generation.)
# Ignored when NERD_PATCH=true.
#
-# HINTING true/false Apply ttfautohint (default: false)
+# HINTING true/false Apply autohinting (default: true)
# When NERD_PATCH=true: hints the patched TTF output.
-# When NERD_PATCH=false: converts static OTF→TTF first,
-# then hints. Variable TTFs are hinted directly (axes
-# are destroyed by ttfautohint — you get a static TTF).
-#
-# HINT_MODE nnn|qqq|qsq|sss 3-char stem-width mode string for ttfautohint:
-# pos1=grayscale pos2=GDI ClearType pos3=DW ClearType
-# n=natural q=quantized s=strong
-# nnn=HiDPI qqq=balanced sss=aggressive low-DPI
+# When NERD_PATCH=false + ttfautohint engine: converts
+# static OTF→TTF first, then hints. Variable TTFs are
+# hinted directly (axes are flattened to a static TTF).
+# When NERD_PATCH=false + cff engine: hints OTFs in place,
+# output stays OTF (no TTF conversion).
+#
+# HINT_ENGINE ttfautohint|cff Hinting backend (default: ttfautohint)
+# ttfautohint: TrueType bytecode hints (best for screen/
+# terminal/Latin; requires TTF, converts OTF→TTF).
+# cff: PostScript/CFF hints, OTF output preserved. Uses
+# otfautohint (afdko) if present, else falls back
+# to FontForge AutoHint. Forced to ttfautohint when
+# NERD_PATCH=true (patched fonts are always TTF).
+#
+# ── ttfautohint tuning ──
+# HINT_PRESET name Named bundle of ttfautohint flags (default: balanced)
+# balanced stem=qsq range 8-50 x-height 14 (upstream defaults)
+# terminal stem=qss range 6-50 x-height 10 +gasp (Hack-style, small mono)
+# ui stem=qsq range 8-60 x-height 14 +gasp (proportional screen text)
+# print stem=nnn range 8-50 x-height 0 (natural stems, no x-snap)
+# light stem=nnn range 12-50 x-height 0 (HiDPI, minimal interference)
+# strong stem=sss range 6-50 x-height 14 +gasp (aggressive low-DPI)
+# custom use the individual HINT_* vars below verbatim
+#
+# Individual ttfautohint knobs (override the preset; empty = use preset value):
+# HINT_MODE nnn|qqq|qsq|sss 3-char --stem-width-mode (grayscale/GDI/DW; n/q/s)
+# HINT_RANGE_MIN N --hinting-range-min (default 8)
+# HINT_RANGE_MAX N --hinting-range-max (default 50)
+# HINT_LIMIT N --hinting-limit (default 200; 0 = no limit)
+# HINT_XHEIGHT N --increase-x-height (default 14; 0 = disable x-height snapping bump)
+# HINT_XSNAP_EXC STR --x-height-snapping-exceptions (e.g. "13-17", "" = none)
+# HINT_WINCOMPAT true/false --windows-compatibility (artificial blue zones)
+# HINT_TTFA_TABLE true/false -t/--ttfa-table (embed a TTFA table with all params)
+# HINT_FAMILY_SUFFIX STR -F/--family-suffix (distinguish differently-hinted builds)
+# (PUA/Nerd icon glyphs are always handled with --fallback-script=none --fallback-scaling.)
+#
+# ── gasp grid-fitting / anti-aliasing tuning (TTF output only) ──
+# GASP_MODE keep|sized|smooth|gridfit (default: keep)
+# keep leave the gasp table as ttfautohint/font-patcher wrote it
+# sized MS strategy: <=8 grayscale, 9-16 gridfit, 17+ both
+# smooth all sizes anti-aliased ({0xFFFF:15}; gftools style)
+# gridfit all sizes gridfit+grayscale ({0xFFFF:3})
+# Some presets enable a sensible GASP_MODE automatically (see above);
+# an explicit GASP_MODE always wins.
#
# Font family selection:
# WANT_SF_PRO true/false (default: true)
@@ -56,8 +126,9 @@
# NY_SUBS e.g. "Small Medium Large ExtraLarge"
#
# Non-interactive examples:
-# NERD_PATCH=true HINTING=true HINT_MODE=qqq SF_PRO_SUBS="Text" makepkg -si
-# NERD_PATCH=false WANT_VARIABLE=true HINTING=false makepkg -si
+# HINTING=true HINT_PRESET=terminal WANT_SF_MONO=true WANT_SF_PRO=false makepkg -si
+# NERD_PATCH=false HINTING=true HINT_ENGINE=cff makepkg -si
+# HINTING=true HINT_PRESET=custom HINT_MODE=qss HINT_RANGE_MIN=6 GASP_MODE=sized makepkg -si
# ────────────────────────────────────────────────────────────────────────────
_validate_hint_mode() {
@@ -67,6 +138,132 @@
echo " Examples: nnn qqq qsq sss"
exit 1
fi
+}
+
+# Map a named preset into the individual HINT_* / GASP_MODE vars.
+# Individual vars already set in the environment are preserved (preset never
+# overrides an explicit user value).
+_apply_preset() {
+ local _p="$1"
+ local _m _lo _hi _lim _xh _gasp
+ case "$_p" in
+ balanced) _m=qsq; _lo=8; _hi=50; _lim=200; _xh=14; _gasp=keep ;;
+ terminal) _m=qss; _lo=6; _hi=50; _lim=200; _xh=10; _gasp=sized ;;
+ ui) _m=qsq; _lo=8; _hi=60; _lim=200; _xh=14; _gasp=sized ;;
+ print) _m=nnn; _lo=8; _hi=50; _lim=200; _xh=0; _gasp=keep ;;
+ light) _m=nnn; _lo=12; _hi=50; _lim=200; _xh=0; _gasp=keep ;;
+ strong) _m=sss; _lo=6; _hi=50; _lim=200; _xh=14; _gasp=smooth ;;
+ custom) _m=qsq; _lo=8; _hi=50; _lim=200; _xh=14; _gasp=keep ;;
+ *)
+ echo "Error: unknown HINT_PRESET='$_p'."
+ echo " Valid: balanced terminal ui print light strong custom"
+ exit 1 ;;
+ esac
+ HINT_MODE="${HINT_MODE:-$_m}"
+ HINT_RANGE_MIN="${HINT_RANGE_MIN:-$_lo}"
+ HINT_RANGE_MAX="${HINT_RANGE_MAX:-$_hi}"
+ HINT_LIMIT="${HINT_LIMIT:-$_lim}"
+ HINT_XHEIGHT="${HINT_XHEIGHT:-$_xh}"
+ GASP_MODE="${GASP_MODE:-$_gasp}"
+}
+
+# ── DMG extraction ──────────────────────────────────────────────────────────
+# Apple ships two different DMG layouts and re-spins the images without bumping
+# any version, so both must be handled (verified by 7z l):
+#
+# "folder" layout (SF-Mono, SF-Arabic, NY — and SF-Pro/SF-Compact before
+# Sept 2026): the DMG holds a <Family>Fonts/ directory containing a
+# "<Family> Fonts.pkg" metapackage plus a <Family>Fonts.pkg/Payload:
+# NY.dmg → NYFonts/ "NY Fonts.pkg" → NYFonts.pkg/Payload
+#
+# "flat" layout (SF-Pro, SF-Compact as of Sept 2026): the DMG holds a single
+# <Family>Fonts.pkg *file* at the root, itself an xar archive whose inner
+# component package carries the payload:
+# SF-Pro.dmg → SFProFonts.pkg → SFProFontsPackage.pkg/Payload
+#
+# Rather than hard-coding either shape, we extract the DMG and then locate the
+# Payload(s) by search, which should survive the next re-spin too. Extraction
+# happens in prepare() so that pkgver() can read the fonts' own version strings.
+_extract_dmg() {
+ local _archive="$1" _dest="$2"
+ local _stem _work _pass _pkg _payload _found=0
+ _stem="$(basename "$_archive" .dmg)"
+ _work="$srcdir/dmg/$_stem"
+
+ rm -rf "$_work"; mkdir -p "$_work" "$_dest"
+ (
+ set -e
+ cd "$_work"
+ 7z x "$_archive" >/dev/null
+
+ # Unwrap every xar/pkg we can find, repeatedly: the "folder" layout nests a
+ # metapackage one level deep, the "flat" layout wraps the component package
+ # in an outer one. Three passes cover both with room to spare; each pass only
+ # touches .pkg entries that are regular files (the *.pkg/ payload directories
+ # are left alone).
+ for _pass in 1 2 3; do
+ while IFS= read -r -d '' _pkg; do
+ # No -P: do not preserve absolute paths from Apple's installer payload.
+ bsdtar xf "$_pkg" -C "$(dirname "$_pkg")" 2>/dev/null || true
+ rm -f "$_pkg"
+ done < <(find . -type f -name '*.pkg' ! -name '._*' -print0)
+ done
+
+ # Payload is a gzip'd cpio; there may be more than one (e.g. a family whose
+ # installer splits Display/Text into separate component packages).
+ while IFS= read -r -d '' _payload; do
+ bsdtar xf "$_payload" -C "$(dirname "$_payload")" 2>/dev/null || continue
+ _found=1
+ done < <(find . -type f -name 'Payload' ! -path '*/._*' -print0)
+
+ if (( _found == 0 )); then
+ echo "Error: no installer Payload found in $(basename "$_archive")." >&2
+ echo " Apple may have changed the DMG layout again. Inspect with:" >&2
+ echo " 7z l '$_archive'" >&2
+ exit 1
+ fi
+
+ find . \( -iname '*.ttf' -o -iname '*.otf' -o -iname '*.ttc' \) \
+ ! -path '*/._*' ! -name '._*' -exec cp -n {} "$_dest/" \;
+ ) || return 1
+
+ rm -rf "$_work"
+ return 0
+}
+
+# Extract every selected DMG into $srcdir/extracted. Used by prepare(); build()
+# reuses the result rather than extracting twice.
+_extract_selected() {
+ local _archive _stem _before _after
+ mkdir -p "$srcdir/extracted" "$srcdir/dmg"
+
+ declare -A _dmg_want=(
+ [SF-Pro]="$WANT_SF_PRO"
+ [SF-Compact]="$WANT_SF_COMPACT"
+ [SF-Mono]="$WANT_SF_MONO"
+ [SF-Arabic]="$WANT_SF_ARABIC"
+ [NY]="$WANT_NY"
+ )
+
+ for _archive in "$srcdir"/*.dmg; do
+ _stem="$(basename "$_archive" .dmg)"
+ if [[ "${_dmg_want[$_stem]:-false}" != true ]]; then
+ echo "==> Skipping: $(basename "$_archive") (not selected)"
+ continue
+ fi
+ echo "==> Extracting: $(basename "$_archive")"
+ _before=$(find "$srcdir/extracted" -type f | wc -l)
+ _extract_dmg "$_archive" "$srcdir/extracted" \
+ || { echo "Error: extraction failed for $_stem."; exit 1; }
+ # Fail loud per family rather than letting an empty extraction surface much
+ # later as a confusing "no fonts matched the selection".
+ _after=$(find "$srcdir/extracted" -type f | wc -l)
+ if (( _after == _before )); then
+ echo "Error: $_stem extracted no font files."; exit 1
+ fi
+ echo " -> $(( _after - _before )) font file(s)"
+ done
+ rm -rf "$srcdir/dmg"
}
_show() { printf " [%s] %s\n" "$1" "$2"; }
@@ -87,64 +284,136 @@
fi
}
-build() {
- # ══════════════════════════════════════════════════════════════════════════
- # Prompt section — all questions, only when env vars are not pre-set
- # ══════════════════════════════════════════════════════════════════════════
- echo ""
- echo "╔══════════════════════════════════════════════════════╗"
- echo "║ nerd-fonts-apple-hinted — build options ║"
- echo "╚══════════════════════════════════════════════════════╝"
+# Resolve every build option: prompt when interactive, otherwise fall back to
+# environment variables and defaults. Called from prepare() (so the answers are
+# known before pkgver() needs them) and re-read from disk by build().
+_resolve_options() {
+ # ══════════════════════════════════════════════════════════════════════════
+ # Interactivity guard: only prompt when connected to a terminal. In a clean
+ # chroot / CI / --noconfirm build stdin is not a TTY; prompting there would
+ # read EOF and silently fall through, so we go straight to env vars/defaults.
+ # ══════════════════════════════════════════════════════════════════════════
+ local _interactive=false
+ if [[ -t 0 ]]; then _interactive=true; fi
+
+ if [[ "$_interactive" == true ]]; then
+ echo ""
+ echo "╔══════════════════════════════════════════════════════╗"
+ echo "║ nerd-fonts-apple-hinted — build options ║"
+ echo "╚══════════════════════════════════════════════════════╝"
+ else
+ echo "==> Non-interactive build (no TTY): using environment variables / defaults."
+ fi
# ── Nerd Fonts patching ──────────────────────────────────────────────────
- if [[ -z "${NERD_PATCH:-}" ]]; then
- _ask_yn "Patch fonts with Nerd Fonts glyphs" y
+ if [[ "$_interactive" == true && -z "${NERD_PATCH:-}" ]]; then
+ _ask_yn "Patch fonts with Nerd Fonts glyphs" n
[[ "$_yn" =~ ^[yY]$ ]] && NERD_PATCH=true || NERD_PATCH=false
fi
- NERD_PATCH="${NERD_PATCH:-true}"
+ NERD_PATCH="${NERD_PATCH:-false}"
# ── Variable fonts (only relevant when not patching) ─────────────────────
- if [[ "$NERD_PATCH" == false && -z "${WANT_VARIABLE:-}" ]]; then
+ if [[ "$_interactive" == true && "$NERD_PATCH" == false && -z "${WANT_VARIABLE:-}" ]]; then
echo ""
echo " Variable fonts (SF Pro, SF Compact, New York) contain all weights"
echo " in a single file with fvar/gvar axes. They are excluded when"
echo " patching because font-patcher destroys variable axes."
- _ask_yn "Include variable TTF fonts" y
+ _ask_yn "Include variable TTF fonts" n
[[ "$_yn" =~ ^[yY]$ ]] && WANT_VARIABLE=true || WANT_VARIABLE=false
fi
# Variable fonts are never compatible with Nerd Fonts patching
- [[ "$NERD_PATCH" == true ]] && WANT_VARIABLE=false
+ if [[ "$NERD_PATCH" == true ]]; then WANT_VARIABLE=false; fi
WANT_VARIABLE="${WANT_VARIABLE:-false}"
# ── Hinting ──────────────────────────────────────────────────────────────
- if [[ -z "${HINTING:-}" ]]; then
+ if [[ "$_interactive" == true && -z "${HINTING:-}" ]]; then
echo ""
if [[ "$NERD_PATCH" == false ]]; then
- echo " Note: hinting static OTFs requires OTF→TTF conversion (via fonttools)."
- echo " Hinting variable TTFs flattens their axes to a static instance."
- fi
- _ask_yn "Apply ttfautohint hinting" n
+ echo " Note: ttfautohint engine hints TrueType, so static OTFs are"
+ echo " converted OTF→TTF first; variable TTFs are flattened to static."
+ echo " The 'cff' engine hints OTFs in place and keeps them as OTF."
+ fi
+ _ask_yn "Apply autohinting" y
[[ "$_yn" =~ ^[yY]$ ]] && HINTING=true || HINTING=false
fi
- HINTING="${HINTING:-false}"
-
- if [[ "$HINTING" == true && -z "${HINT_MODE:-}" ]]; then
+ HINTING="${HINTING:-true}"
+
+ # ── Hinting engine ─────────────────────────────────────────────────────────
+ if [[ "$HINTING" == true && "$NERD_PATCH" == false && "$_interactive" == true && -z "${HINT_ENGINE:-}" ]]; then
echo ""
- echo " Hinting stem-width mode:"
- select _choice in "Balanced (qqq)" "HiDPI (nnn)" "Aggressive (sss)" "Custom"; do
- case "$_choice" in
- "Balanced (qqq)") HINT_MODE=qqq; break ;;
- "HiDPI (nnn)") HINT_MODE=nnn; break ;;
- "Aggressive (sss)") HINT_MODE=sss; break ;;
- "Custom") read -p " Enter 3-char mode (e.g. qsq): " HINT_MODE; break ;;
+ echo " Hinting engine:"
+ PS3=" > "
+ select _choice in "ttfautohint (TrueType bytecode, converts OTF→TTF)" \
+ "cff (PostScript/CFF hints, keeps OTF)"; do
+ case "$REPLY" in
+ 1) HINT_ENGINE=ttfautohint; break ;;
+ 2) HINT_ENGINE=cff; break ;;
+ *) echo " Please enter 1 or 2." ;;
esac
+ [[ -z "$REPLY" ]] && { echo " No selection; defaulting to ttfautohint."; HINT_ENGINE=ttfautohint; break; }
done
fi
- HINT_MODE="${HINT_MODE:-qqq}"
- [[ "$HINTING" == true ]] && _validate_hint_mode
+ HINT_ENGINE="${HINT_ENGINE:-ttfautohint}"
+ # Patched fonts are always TTF → ttfautohint only.
+ if [[ "$NERD_PATCH" == true ]]; then HINT_ENGINE=ttfautohint; fi
+ if [[ "$HINT_ENGINE" != ttfautohint && "$HINT_ENGINE" != cff ]]; then
+ echo "Error: HINT_ENGINE='$HINT_ENGINE' invalid (use 'ttfautohint' or 'cff')."; exit 1
+ fi
+
+ # ── Hinting preset (ttfautohint only) ──────────────────────────────────────
+ if [[ "$HINTING" == true && "$HINT_ENGINE" == ttfautohint ]]; then
+ if [[ "$_interactive" == true && -z "${HINT_PRESET:-}" && -z "${HINT_MODE:-}" ]]; then
+ echo ""
+ echo " Hinting preset:"
+ PS3=" > "
+ select _choice in "balanced (upstream defaults, qsq 8-50 x14)" \
+ "terminal (mono/small, qss 6-50 x10 +gasp)" \
+ "ui (proportional screen, qsq 8-60 x14 +gasp)" \
+ "print (natural stems, nnn, no x-snap)" \
+ "light (HiDPI minimal, nnn 12-50)" \
+ "strong (aggressive low-DPI, sss +smooth gasp)" \
+ "custom (set HINT_* vars yourself)"; do
+ case "$REPLY" in
+ 1) HINT_PRESET=balanced; break ;;
+ 2) HINT_PRESET=terminal; break ;;
+ 3) HINT_PRESET=ui; break ;;
+ 4) HINT_PRESET=print; break ;;
+ 5) HINT_PRESET=light; break ;;
+ 6) HINT_PRESET=strong; break ;;
+ 7) HINT_PRESET=custom; break ;;
+ *) echo " Please enter 1-7." ;;
+ esac
+ [[ -z "$REPLY" ]] && { echo " No selection; defaulting to balanced."; HINT_PRESET=balanced; break; }
+ done
+ fi
+ HINT_PRESET="${HINT_PRESET:-balanced}"
+ _apply_preset "$HINT_PRESET"
+ _validate_hint_mode
+
+ # Validate numeric knobs.
+ for _v in HINT_RANGE_MIN HINT_RANGE_MAX HINT_LIMIT HINT_XHEIGHT; do
+ if [[ ! "${!_v}" =~ ^[0-9]+$ ]]; then
+ echo "Error: $_v='${!_v}' must be a non-negative integer."; exit 1
+ fi
+ done
+ if (( HINT_RANGE_MIN > HINT_RANGE_MAX )); then
+ echo "Error: HINT_RANGE_MIN ($HINT_RANGE_MIN) > HINT_RANGE_MAX ($HINT_RANGE_MAX)."; exit 1
+ fi
+ fi
+
+ # gasp default + validation (used by both engines, but only meaningful for TTF)
+ GASP_MODE="${GASP_MODE:-keep}"
+ case "$GASP_MODE" in
+ keep|sized|smooth|gridfit) ;;
+ *) echo "Error: GASP_MODE='$GASP_MODE' invalid (keep|sized|smooth|gridfit)."; exit 1 ;;
+ esac
+
+ HINT_WINCOMPAT="${HINT_WINCOMPAT:-false}"
+ HINT_TTFA_TABLE="${HINT_TTFA_TABLE:-false}"
# ── Font family & subfamily selection ────────────────────────────────────
- if [[ -z "${WANT_SF_PRO:-}${WANT_SF_COMPACT:-}${WANT_SF_MONO:-}${WANT_SF_ARABIC:-}${WANT_NY:-}" ]]; then
+ if [[ "$_interactive" == true && \
+ -z "${WANT_SF_PRO:-}${WANT_SF_COMPACT:-}${WANT_SF_MONO:-}${WANT_SF_ARABIC:-}${WANT_NY:-}" ]]; then
echo ""
echo " ── Font families ────────────────────────────────────────"
@@ -206,7 +475,19 @@
echo " ── Build summary ───────────────────────────────────────"
printf " NERD_PATCH: %s\n" "$NERD_PATCH"
printf " WANT_VARIABLE: %s\n" "$WANT_VARIABLE"
- printf " HINTING: %s%s\n" "$HINTING" "$( [[ "$HINTING" == true ]] && echo " (mode=$HINT_MODE)" )"
+ if [[ "$HINTING" == true ]]; then
+ printf " HINTING: true (engine=%s)\n" "$HINT_ENGINE"
+ if [[ "$HINT_ENGINE" == ttfautohint ]]; then
+ printf " preset=%s stem=%s range=%s-%s limit=%s x-height=%s\n" \
+ "$HINT_PRESET" "$HINT_MODE" "$HINT_RANGE_MIN" "$HINT_RANGE_MAX" "$HINT_LIMIT" "$HINT_XHEIGHT"
+ printf " x-snap-exc='%s' win-compat=%s ttfa-table=%s%s\n" \
+ "${HINT_XSNAP_EXC-<default>}" "$HINT_WINCOMPAT" "$HINT_TTFA_TABLE" \
+ "${HINT_FAMILY_SUFFIX:+ family-suffix='$HINT_FAMILY_SUFFIX'}"
+ fi
+ printf " gasp=%s\n" "$GASP_MODE"
+ else
+ printf " HINTING: false\n"
+ fi
echo ""
_show "$( [[ "$WANT_SF_PRO" == true ]] && echo "x" || echo " " )" "SF Pro ${SF_PRO_SUBS:+(subfamilies: $SF_PRO_SUBS)}"
_show "$( [[ "$WANT_SF_COMPACT" == true ]] && echo "x" || echo " " )" "SF Compact ${SF_COMPACT_SUBS:+(subfamilies: $SF_COMPACT_SUBS)}"
@@ -216,240 +497,409 @@
echo " ────────────────────────────────────────────────────────"
echo ""
+ # Persist the resolved options so build() and package() reuse the same answers
+ # instead of re-prompting (makepkg runs each function in a fresh shell).
+ {
+ echo "NERD_PATCH=$NERD_PATCH"
+ echo "WANT_VARIABLE=$WANT_VARIABLE"
+ echo "HINTING=$HINTING"
+ echo "HINT_ENGINE=$HINT_ENGINE"
+ echo "HINT_PRESET=${HINT_PRESET:-}"
+ echo "HINT_MODE=${HINT_MODE:-}"
+ echo "HINT_RANGE_MIN=${HINT_RANGE_MIN:-}"
+ echo "HINT_RANGE_MAX=${HINT_RANGE_MAX:-}"
+ echo "HINT_LIMIT=${HINT_LIMIT:-}"
+ echo "HINT_XHEIGHT=${HINT_XHEIGHT:-}"
+ echo "HINT_WINCOMPAT=$HINT_WINCOMPAT"
+ echo "HINT_TTFA_TABLE=$HINT_TTFA_TABLE"
+ echo "GASP_MODE=$GASP_MODE"
+ echo "WANT_SF_PRO=$WANT_SF_PRO"
+ echo "WANT_SF_COMPACT=$WANT_SF_COMPACT"
+ echo "WANT_SF_MONO=$WANT_SF_MONO"
+ echo "WANT_SF_ARABIC=$WANT_SF_ARABIC"
+ echo "WANT_NY=$WANT_NY"
+ echo "SF_PRO_SUBS=${SF_PRO_SUBS:-}"
+ echo "SF_COMPACT_SUBS=${SF_COMPACT_SUBS:-}"
+ echo "NY_SUBS=${NY_SUBS:-}"
+ # These two are optional and must distinguish "unset" from "set to empty".
+ # `if` rather than `&&` so an unset variable is not a non-zero exit status
+ # for the whole group under makepkg's `set -e`.
+ if [[ "${HINT_XSNAP_EXC+set}" == set ]]; then
+ echo "HINT_XSNAP_EXC=$HINT_XSNAP_EXC"
+ fi
+ if [[ -n "${HINT_FAMILY_SUFFIX:-}" ]]; then
+ echo "HINT_FAMILY_SUFFIX=$HINT_FAMILY_SUFFIX"
+ fi
+ } > "$srcdir/.build_opts"
+}
+
+# Load the options persisted by _resolve_options(). Keys are whitelisted so a
+# stray line in the file can never inject an arbitrary variable.
+_load_options() {
+ local _k _v
+ [[ -f "$srcdir/.build_opts" ]] || { echo "Error: missing .build_opts; run prepare() first."; exit 1; }
+ while IFS='=' read -r _k _v; do
+ case "$_k" in
+ NERD_PATCH|WANT_VARIABLE|HINTING|HINT_ENGINE|HINT_PRESET|HINT_MODE| \
+ HINT_RANGE_MIN|HINT_RANGE_MAX|HINT_LIMIT|HINT_XHEIGHT|HINT_WINCOMPAT| \
+ HINT_TTFA_TABLE|HINT_XSNAP_EXC|HINT_FAMILY_SUFFIX|GASP_MODE| \
+ WANT_SF_PRO|WANT_SF_COMPACT|WANT_SF_MONO|WANT_SF_ARABIC|WANT_NY| \
+ SF_PRO_SUBS|SF_COMPACT_SUBS|NY_SUBS)
+ printf -v "$_k" '%s' "$_v" ;;
+ esac
+ done < "$srcdir/.build_opts"
+}
+
+prepare() {
+ _resolve_options
+
# ══════════════════════════════════════════════════════════════════════════
# Step 1 — Extract fonts from Apple DMG archives (selected families only)
# ══════════════════════════════════════════════════════════════════════════
- # DMG layout (verified by 7z l):
- # SF-Pro.dmg → SFProFonts/ "SF Pro Fonts.pkg" → SFProFonts.pkg/Payload
- # SF-Compact.dmg → SFCompactFonts/ "SF Compact Fonts.pkg" → SFCompactFonts.pkg/Payload
- # SF-Mono.dmg → SFMonoFonts/ "SF Mono Fonts.pkg" → SFMonoFonts.pkg/Payload
- # SF-Arabic.dmg → SFArabicFonts/ "SF Arabic Fonts.pkg" → SFArabicFonts.pkg/Payload
- # NY.dmg → NYFonts/ "NY Fonts.pkg" → NYFonts.pkg/Payload
- rm -rf "$srcdir/extracted" "$srcdir/converted" "$srcdir/patched" "$srcdir/selected"
- mkdir -p "$srcdir/extracted" "$srcdir/converted" "$srcdir/patched" "$srcdir/selected"
-
- declare -A _dmg_want=(
- [SF-Pro]="$WANT_SF_PRO"
- [SF-Compact]="$WANT_SF_COMPACT"
- [SF-Mono]="$WANT_SF_MONO"
- [SF-Arabic]="$WANT_SF_ARABIC"
- [NY]="$WANT_NY"
- )
-
- local _pwd; _pwd="$(pwd)"
- cd "$srcdir"
- for _archive in *.dmg; do
- local _stem; _stem="$(basename "$_archive" .dmg)"
- if [[ "${_dmg_want[$_stem]}" != true ]]; then
- echo "==> Skipping: $_archive (not selected)"
- continue
- fi
- local _fontDir _metapkg _payload
- _fontDir="$(echo "$_stem" | tr -d '-')Fonts"
- _metapkg="$(echo "$_stem" | sed 's/-/ /g') Fonts.pkg"
- _payload="${_fontDir}.pkg/Payload"
- echo "==> Extracting: $_archive"
- 7z x "$_archive" &>/dev/null || true
- if [[ -d "$srcdir/$_fontDir" ]]; then
- cd "$srcdir/$_fontDir"
- bsdtar xvPf "$_metapkg" &>/dev/null || true
- bsdtar xvPf "$_payload" &>/dev/null || true
- find . \( -name '*.ttf' -o -name '*.otf' \) ! -path '*/._*' \
- -exec cp -n {} "$srcdir/extracted/" \;
- cd "$srcdir"
- fi
- done
- cd "$_pwd"
-
- # ══════════════════════════════════════════════════════════════════════════
- # Step 2 — Filter by subfamily and variable-font preference
- # ══════════════════════════════════════════════════════════════════════════
- # Variable TTFs (fvar present): SF-Pro.ttf, SF-Pro-Italic.ttf,
- # SF-Compact.ttf, SF-Compact-Italic.ttf, NewYork.ttf, NewYorkItalic.ttf
- # These are included only when NERD_PATCH=false AND WANT_VARIABLE=true.
-
- _copy_otf_subfamily() {
- local _want="$1" _subs="$2" _prefix="$3"
- [[ "$_want" != true ]] && return
- if [[ -z "$_subs" ]]; then
- find "$srcdir/extracted" \( -name "${_prefix}-*.otf" \) ! -path '*/._*' \
- -exec cp -n {} "$srcdir/selected/" \;
- else
- for _sub in $_subs; do
- find "$srcdir/extracted" \
- \( -name "${_prefix}-${_sub}-*.otf" -o -name "${_prefix}-${_sub}.otf" \) \
- ! -path '*/._*' \
- -exec cp -n {} "$srcdir/selected/" \;
- done
- fi
+ # Extraction lives in prepare() rather than build() so pkgver() — which runs
+ # in between — can read the version out of the extracted font files.
+ rm -rf "$srcdir/extracted" "$srcdir/converted" "$srcdir/patched" \
+ "$srcdir/selected" "$srcdir/dmg"
+ mkdir -p "$srcdir/extracted" "$srcdir/converted" "$srcdir/patched" \
+ "$srcdir/selected"
+ _extract_selected
+}
+
+pkgver() {
+ # Compose the version from what the fonts themselves declare, combined with
+ # the Nerd Fonts patcher version:
+ #
+ # <highest Apple font version>+nf<nerd fonts version> e.g. 22.0.5.4+nf3.5.1
+ #
+ # Apple's per-family versions differ (SF Pro 22.0d5e4, SF Mono 18.0d1e1,
+ # NY 16.0d2e2), so we take the highest across the *selected* families. That
+ # keeps pkgver monotonic: any family Apple bumps raises the package version,
+ # and re-spins that change nothing leave it alone.
+ local _apple
+ _apple="$(python "$srcdir/font_meta.py" version "$srcdir/extracted/"* 2>/dev/null)" || _apple=""
+ if [[ -z "$_apple" ]]; then
+ # Never fail the build over a version string: fall back to the previous
+ # pkgver so the package still builds if Apple ships unparseable metadata.
+ echo "Warning: could not read a font version; keeping $pkgver" >&2
+ echo "$pkgver"
+ return 0
+ fi
+ # '-' is not allowed in pkgver; '+' and '.' are.
+ printf '%s+nf%s\n' "${_apple//-/.}" "${_nfver//-/.}"
+}
+
+build() {
+ _load_options
+
+
+ # ══════════════════════════════════════════════════════════════════════════
+ # Step 2 — Filter by family/subfamily and variable-font preference
+ # ══════════════════════════════════════════════════════════════════════════
+ # Selection is driven by each font's OpenType `name` table (typographic family,
+ # nameID 16, falling back to nameID 1) rather than by filename patterns. Apple
+ # has already changed both the installer layout and file naming between
+ # re-spins, but the internal family names ("SF Pro Text", "New York Small",
+ # "SF Mono") have been stable across every release we have seen — and they are
+ # what fontconfig ends up showing the user anyway.
+ #
+ # The family map is built once with a single python call; the shell then only
+ # does string matching, so a new weight or a renamed file is picked up for free.
+ declare -A _fam_of=()
+ local _file _fam
+ while IFS=$'\t' read -r _file _fam; do
+ if [[ -n "$_file" ]]; then _fam_of["$_file"]="$_fam"; fi
+ done < <(python "$srcdir/font_meta.py" families "$srcdir/extracted/"* 2>/dev/null)
+
+ if (( ${#_fam_of[@]} == 0 )); then
+ echo "Error: could not read family names from any extracted font." >&2
+ echo " Is python-fonttools installed and are the fonts intact?" >&2
+ exit 1
+ fi
+
+ # Does family string $1 belong to base family $2 with one of subfamilies $3?
+ # Empty $3 means "any subfamily of this base family".
+ _family_matches() {
+ local _fam="$1" _base="$2" _subs="$3" _sub
+ # Exact base family (e.g. "SF Mono", or the variable "SF Pro") …
+ [[ "$_fam" == "$_base" ]] && { [[ -z "$_subs" ]] && return 0 || return 1; }
+ # … or "<base> <subfamily>" (e.g. "SF Pro Text", "New York Small").
+ [[ "$_fam" == "$_base "* ]] || return 1
+ [[ -z "$_subs" ]] && return 0
+ local _rest="${_fam#"$_base" }"
+ for _sub in $_subs; do
+ # Compare ignoring spaces so "ExtraLarge" matches "Extra Large".
+ [[ "${_rest// /}" == "${_sub// /}" ]] && return 0
+ done
+ return 1
}
- _copy_ny_otf() {
- local _want="$1" _subs="$2"
- [[ "$_want" != true ]] && return
- if [[ -z "$_subs" ]]; then
- find "$srcdir/extracted" -name 'NewYork*.otf' ! -path '*/._*' \
- -exec cp -n {} "$srcdir/selected/" \;
- else
- for _sub in $_subs; do
- find "$srcdir/extracted" -name "NewYork${_sub}-*.otf" ! -path '*/._*' \
- -exec cp -n {} "$srcdir/selected/" \;
- done
- fi
+ # Copy every extracted font whose family matches, honouring the static/variable
+ # split: variable fonts are TTFs with an fvar axis and are only wanted when
+ # NERD_PATCH=false and WANT_VARIABLE=true (font-patcher destroys fvar/gvar).
+ _select_family() {
+ local _want="$1" _base="$2" _subs="$3"
+ [[ "$_want" != true ]] && return 0
+ local _f _bn _fam _n=0
+ for _f in "$srcdir/extracted"/*; do
+ [[ -f "$_f" ]] || continue
+ _bn="$(basename "$_f")"
+ [[ "$_bn" == ._* ]] && continue
+ _fam="${_fam_of[$_bn]:-}"
+ [[ -n "$_fam" ]] || continue
+ _family_matches "$_fam" "$_base" "$_subs" || continue
+ # Apple ships the variable faces as .ttf and the static faces as .otf.
+ if [[ "$_bn" == *.ttf || "$_bn" == *.TTF ]]; then
+ [[ "$NERD_PATCH" == true || "$WANT_VARIABLE" != true ]] && continue
+ fi
+ cp -n "$_f" "$srcdir/selected/" && _n=$(( _n + 1 ))
+ done
+ if (( _n == 0 )); then
+ echo "Warning: no fonts matched '$_base'${_subs:+ (subfamilies: $_subs)}." >&2
+ # List what was actually available so a renamed subfamily is obvious.
+ local _avail
+ _avail="$(printf '%s\n' "${_fam_of[@]}" | grep -F "$_base" | sort -u | paste -sd', ')"
+ if [[ -n "$_avail" ]]; then echo " Available: $_avail" >&2; fi
+ fi
+ return 0
}
- _maybe_variable() {
- # Copy a variable TTF only when NERD_PATCH=false and WANT_VARIABLE=true
- local _want_family="$1" _file="$2"
- [[ "$_want_family" != true ]] && return
- [[ "$NERD_PATCH" == true || "$WANT_VARIABLE" != true ]] && return
- cp -n "$srcdir/extracted/$_file" "$srcdir/selected/" 2>/dev/null || true
- }
-
- # SF Mono has no variable font and no subfamilies — always OTF
- [[ "$WANT_SF_MONO" == true ]] && \
- find "$srcdir/extracted" -name 'SF-Mono-*.otf' ! -path '*/._*' \
- -exec cp -n {} "$srcdir/selected/" \;
-
- # SF Arabic has no variable font — always OTF (TTF)
- [[ "$WANT_SF_ARABIC" == true ]] && \
- find "$srcdir/extracted" -name 'SF-Arabic*' ! -path '*/._*' \
- -exec cp -n {} "$srcdir/selected/" \;
-
- # SF Pro: static OTFs by subfamily + optional variable TTFs
- _copy_otf_subfamily "$WANT_SF_PRO" "$SF_PRO_SUBS" "SF-Pro"
- _maybe_variable "$WANT_SF_PRO" "SF-Pro.ttf"
- _maybe_variable "$WANT_SF_PRO" "SF-Pro-Italic.ttf"
-
- # SF Compact: static OTFs by subfamily + optional variable TTFs
- _copy_otf_subfamily "$WANT_SF_COMPACT" "$SF_COMPACT_SUBS" "SF-Compact"
- _maybe_variable "$WANT_SF_COMPACT" "SF-Compact.ttf"
- _maybe_variable "$WANT_SF_COMPACT" "SF-Compact-Italic.ttf"
-
- # New York: static OTFs by subfamily + optional variable TTFs
- _copy_ny_otf "$WANT_NY" "$NY_SUBS"
- _maybe_variable "$WANT_NY" "NewYork.ttf"
- _maybe_variable "$WANT_NY" "NewYorkItalic.ttf"
-
- local _sel_count; _sel_count=$(ls "$srcdir/selected/" | wc -l)
+ # SF Mono and SF Arabic have no subfamily split; the rest are filtered by the
+ # *_SUBS variables (empty = every subfamily).
+ _select_family "$WANT_SF_MONO" "SF Mono" ""
+ _select_family "$WANT_SF_ARABIC" "SF Arabic" ""
+ _select_family "$WANT_SF_PRO" "SF Pro" "$SF_PRO_SUBS"
+ _select_family "$WANT_SF_COMPACT" "SF Compact" "$SF_COMPACT_SUBS"
+ _select_family "$WANT_NY" "New York" "$NY_SUBS"
+
+ local _sel_count; _sel_count=$(find "$srcdir/selected" -type f | wc -l)
echo "==> Selected $_sel_count font files for processing."
if (( _sel_count == 0 )); then
echo "Error: no fonts matched the selection. Check subfamily names."; exit 1
fi
# ══════════════════════════════════════════════════════════════════════════
- # Step 3 — OTF → TTF conversion (only when hinting is requested)
- # ══════════════════════════════════════════════════════════════════════════
- # ttfautohint requires TrueType glyf outlines.
- # We use fonttools' otf2ttf.py (cu2qu) which converts CFF cubic beziers to
- # TrueType quadratic splines without touching the name table — the name table
- # is preserved exactly from the original OTF, Apple private name IDs intact.
- # Variable TTFs already have glyf outlines and pass through unchanged.
- # When NERD_PATCH=true, font-patcher handles OTF→TTF implicitly.
+ # Step 3 — OTF → TTF conversion (only for the ttfautohint engine, no patch)
+ # ══════════════════════════════════════════════════════════════════════════
+ # ttfautohint requires TrueType glyf outlines. otf2ttf.py (cu2qu) converts CFF
+ # cubics to quadratic splines while preserving the name table. The 'cff' engine
+ # keeps OTFs intact and is handled in step 5.
local _patchsrc
- if [[ "$NERD_PATCH" == false && "$HINTING" == true ]]; then
+ if [[ "$NERD_PATCH" == false && "$HINTING" == true && "$HINT_ENGINE" == ttfautohint ]]; then
printf "%b" "\e[1;34m==> INFO:\e[0m Converting static OTF→TTF for ttfautohint...\n"
- find "$srcdir/selected" -name '*.otf' ! -path '*/._*' | sort | \
- parallel -j"$(nproc)" \
+ find "$srcdir/selected" -name '*.otf' ! -name '._*' | sort | \
+ parallel --will-cite -j"$(nproc)" \
'python "'"$srcdir/otf2ttf.py"'" --post-format 2.0 --max-error 1.0 \
-o "'"$srcdir/converted"'/{/.}.ttf" "{}"'
# Variable TTFs and native TTFs pass through unchanged
- find "$srcdir/selected" -name '*.ttf' ! -path '*/._*' \
+ find "$srcdir/selected" -name '*.ttf' ! -name '._*' \
-exec cp -n {} "$srcdir/converted/" \;
_patchsrc="$srcdir/converted"
- elif [[ "$NERD_PATCH" == true && "$HINTING" == true ]]; then
- # font-patcher will convert OTF→TTF; conversion happens in step 4
+ else
+ # NERD_PATCH=true → font-patcher converts implicitly.
+ # cff engine / no-hint → operate on selected fonts directly.
_patchsrc="$srcdir/selected"
- elif [[ "$NERD_PATCH" == true && "$HINTING" == false ]]; then
- _patchsrc="$srcdir/selected"
- else
- # NERD_PATCH=false, HINTING=false — install selected fonts directly
- _patchsrc="$srcdir/selected"
fi
# ══════════════════════════════════════════════════════════════════════════
# Step 4 — Nerd Fonts patching (skipped when NERD_PATCH=false)
# ══════════════════════════════════════════════════════════════════════════
+ local _hintsrc
if [[ "$NERD_PATCH" == true ]]; then
printf "%b" "\e[1;33m==> WARNING:\e[0m Patching with Nerd Fonts glyphs — this takes a while...\n"
- # SF Mono: -s (--mono) preserves monospace cell-width metrics
+ # SF Mono: -s (--mono) preserves monospace cell-width metrics.
+ # stderr is kept (only stdout silenced) so patch failures are visible, and we
+ # assert non-empty output afterwards rather than masking with `|| true`.
find "$_patchsrc" \( -name 'SF-Mono-*.otf' -o -name 'SF-Mono-*.ttf' \) | sort | \
- parallel -j4 python "$srcdir/font-patcher" \
+ parallel --will-cite -j4 python "$srcdir/font-patcher" \
--variable-width-glyphs -q -c -s {} \
- -out "$srcdir/patched" &>/dev/null || true
+ -out "$srcdir/patched" '>/dev/null'
find "$_patchsrc" \( -name '*.otf' -o -name '*.ttf' \) ! -name 'SF-Mono-*' | sort | \
- parallel -j4 python "$srcdir/font-patcher" \
+ parallel --will-cite -j4 python "$srcdir/font-patcher" \
--variable-width-glyphs -q -c {} \
- -out "$srcdir/patched" &>/dev/null || true
-
- local _hintsrc="$srcdir/patched"
+ -out "$srcdir/patched" '>/dev/null'
+
+ # font-patcher preserves the input outline format: OTF/CFF inputs (all the
+ # Apple static fonts, including SF Mono) yield patched *.otf; only TrueType
+ # inputs yield *.ttf. Count both, and fail loud if nothing came out.
+ local _patched_count
+ _patched_count=$(find "$srcdir/patched" -maxdepth 1 \( -name '*.ttf' -o -name '*.otf' \) | wc -l)
+ if (( _patched_count == 0 )); then
+ echo "Error: font-patcher produced no output. See messages above."; exit 1
+ fi
+ echo "==> Patched $_patched_count font(s)."
+
+ # The ttfautohint engine needs TrueType glyf outlines, so convert any patched
+ # OTFs to TTF in place. (The cff engine only runs when NERD_PATCH=false, so a
+ # patched tree never feeds it.)
+ if [[ "$HINTING" == true && "$HINT_ENGINE" == ttfautohint ]] && \
+ compgen -G "$srcdir/patched/*.otf" >/dev/null; then
+ printf "%b" "\e[1;34m==> INFO:\e[0m Converting patched OTF→TTF for ttfautohint...\n"
+ find "$srcdir/patched" -maxdepth 1 -name '*.otf' ! -name '._*' | sort | \
+ parallel --will-cite -j"$(nproc)" \
+ 'python "'"$srcdir/otf2ttf.py"'" --post-format 2.0 --max-error 1.0 \
+ -o "'"$srcdir/patched"'/{/.}.ttf" "{}" && rm -f "{}"'
+ fi
+ _hintsrc="$srcdir/patched"
else
- # No patching — hint or install directly from the conversion output
- local _hintsrc="$_patchsrc"
- fi
-
- # ══════════════════════════════════════════════════════════════════════════
- # Step 5 — ttfautohint hinting (only TTF files)
- # ══════════════════════════════════════════════════════════════════════════
- # When NERD_PATCH=true: fonts in $_hintsrc are already TTF (font-patcher output)
- # When NERD_PATCH=false: OTFs were converted in step 3; variable TTFs pass through.
- # --fallback-script=none + --fallback-scaling: PUA glyphs (SF Symbols) are scaled
- # proportionally instead of receiving incorrect Latin hinting.
- if [[ "$HINTING" == true ]]; then
- local _xheight
- case "$HINT_MODE" in
- nnn) _xheight=12 ;;
- *) _xheight=14 ;;
- esac
- printf "%b" "\e[1;32m==> INFO:\e[0m Applying ttfautohint (mode=$HINT_MODE, x-height=${_xheight}px)...\n"
+ _hintsrc="$_patchsrc"
+ fi
+
+ # ══════════════════════════════════════════════════════════════════════════
+ # Step 5 — Hinting
+ # ══════════════════════════════════════════════════════════════════════════
+ if [[ "$HINTING" == true && "$HINT_ENGINE" == cff && "$NERD_PATCH" == false ]]; then
+ # ── CFF/OTF hinting: keep OTF outlines, no TTF conversion ──
mkdir -p "$srcdir/patched/hinted"
+ local _have_otfautohint=false
+ command -v otfautohint >/dev/null 2>&1 && _have_otfautohint=true
+ if [[ "$_have_otfautohint" == true ]]; then
+ printf "%b" "\e[1;32m==> INFO:\e[0m CFF hinting via otfautohint (AFDKO)...\n"
+ else
+ printf "%b" "\e[1;32m==> INFO:\e[0m CFF hinting via FontForge AutoHint (install afdko for otfautohint)...\n"
+ fi
+ # Two Apple-specific quirks make stock otfautohint abort on these fonts:
+ #
+ # 1. Overlapping alignment zones. otfautohint reads the CFF Private-dict
+ # zones and FATALLY rejects any that overlap or sit within 1+2*BlueFuzz
+ # units. Apple's SF Pro/Compact ship DUPLICATE/OVERLAPPING zones (e.g.
+ # SF Pro Text BlueValues lists (1443,1477) twice; OtherBlues has
+ # (-34,0)⊃(-19,0)) → "top of zone X overlaps zone Y". dedup_blues.py
+ # collapses those redundant pairs into a spec-conformant, non-overlapping
+ # set (outlines untouched).
+ #
+ # 2. closeSegs assertion on overlapping contours. otfautohint has an open
+ # bug (afdko #1795, unfixed through 5.0.1) where `assert n and p` in
+ # hinter.py crashes on glyphs with overlapping/degenerate contours — the
+ # heavy SF *-Black weights hit this (e.g. uni10094E.large). Passing
+ # --force-overlap routes glyphs through otfautohint's overlap-correction
+ # path during analysis and avoids the crash WITHOUT altering the emitted
+ # outlines (verified bit-identical on normal glyphs).
+ #
+ # otfautohint preserves the name table; the FontForge AutoHint path
+ # (cff_hint.py) is kept only as a last-resort fallback (it repairs the names
+ # FontForge mangles) so a font is never silently dropped from the package.
+ for _f in "$_hintsrc"/*.otf; do
+ [[ -f "$_f" ]] || continue
+ local _out="$srcdir/patched/hinted/$(basename "$_f")"
+ if [[ "$_have_otfautohint" == true ]]; then
+ python "$srcdir/dedup_blues.py" "$_f"
+ if ! otfautohint --force-overlap -o "$_out" "$_f" 2>/dev/null; then
+ printf "%b" "\e[1;33m==> WARNING:\e[0m otfautohint failed on $(basename "$_f"); using FontForge AutoHint instead.\n"
+ rm -f "$_out"
+ python "$srcdir/cff_hint.py" "$_f" "$_out"
+ fi
+ else
+ python "$srcdir/cff_hint.py" "$_f" "$_out"
+ fi
+ done
+ # Variable / native TTFs cannot get CFF hints — pass them through untouched.
for _f in "$_hintsrc"/*.ttf; do
[[ -f "$_f" ]] || continue
- ttfautohint \
- --hinting-range-min=8 \
- --hinting-range-max=50 \
- --hinting-limit=200 \
- --increase-x-height="$_xheight" \
- --x-height-snapping-exceptions="" \
- --fallback-script=none \
- --fallback-scaling \
- --stem-width-mode="$HINT_MODE" \
- --no-info \
- --ignore-restrictions \
- "$_f" "$srcdir/patched/hinted/$(basename "$_f")"
+ cp -n "$_f" "$srcdir/patched/hinted/"
done
- fi
-
- # Persist state for package() — makepkg functions don't share shell vars
- printf "%s\n%s\n%s\n" "$HINTING" "$NERD_PATCH" "$WANT_VARIABLE" \
- > "$srcdir/.build_state"
+
+ elif [[ "$HINTING" == true ]]; then
+ # ── ttfautohint engine (TTF) ──
+ # PUA glyphs (SF Symbols / Nerd icons): --fallback-script=none +
+ # --fallback-scaling scale them proportionally instead of mis-hinting as Latin.
+ printf "%b" "\e[1;32m==> INFO:\e[0m ttfautohint (preset=$HINT_PRESET, stem=$HINT_MODE, range=$HINT_RANGE_MIN-$HINT_RANGE_MAX, x-height=$HINT_XHEIGHT)...\n"
+ mkdir -p "$srcdir/patched/hinted"
+
+ # Assemble optional flags.
+ local -a _ta_opts=(
+ --hinting-range-min="$HINT_RANGE_MIN"
+ --hinting-range-max="$HINT_RANGE_MAX"
+ --hinting-limit="$HINT_LIMIT"
+ --increase-x-height="$HINT_XHEIGHT"
+ --fallback-script=none
+ --fallback-scaling
+ --stem-width-mode="$HINT_MODE"
+ --ignore-restrictions
+ )
+ # x-height-snapping-exceptions: honour an explicit value (including empty
+ # string = no exceptions); omit entirely if unset to use ttfautohint's default.
+ if [[ "${HINT_XSNAP_EXC+set}" == set ]]; then
+ _ta_opts+=(--x-height-snapping-exceptions="$HINT_XSNAP_EXC")
+ fi
+ if [[ "$HINT_WINCOMPAT" == true ]]; then _ta_opts+=(--windows-compatibility); fi
+ if [[ -n "${HINT_FAMILY_SUFFIX:-}" ]]; then _ta_opts+=(--family-suffix="$HINT_FAMILY_SUFFIX"); fi
+ if [[ "$HINT_TTFA_TABLE" == true ]]; then
+ _ta_opts+=(--ttfa-table)
+ else
+ _ta_opts+=(--no-info)
+ fi
+
+ for _f in "$_hintsrc"/*.ttf; do
+ [[ -f "$_f" ]] || continue
+ ttfautohint "${_ta_opts[@]}" "$_f" "$srcdir/patched/hinted/$(basename "$_f")"
+ done
+ fi
+
+ # ══════════════════════════════════════════════════════════════════════════
+ # Step 6 — gasp grid-fitting / anti-aliasing tuning (TTF output only)
+ # ══════════════════════════════════════════════════════════════════════════
+ if [[ "$HINTING" == true && "$GASP_MODE" != keep ]]; then
+ local _gasp_dir
+ if [[ "$HINT_ENGINE" == cff && "$NERD_PATCH" == false ]]; then
+ _gasp_dir="$srcdir/patched/hinted" # only the .ttf passthroughs here have a gasp
+ else
+ _gasp_dir="$srcdir/patched/hinted"
+ fi
+ if compgen -G "$_gasp_dir/*.ttf" >/dev/null; then
+ printf "%b" "\e[1;36m==> INFO:\e[0m Applying gasp mode '$GASP_MODE' to TTF output...\n"
+ python "$srcdir/set_gasp.py" "$GASP_MODE" "$_gasp_dir"/*.ttf
+ fi
+ fi
+
}
package() {
- # Restore state written by build()
- local HINTING NERD_PATCH WANT_VARIABLE
- { read -r HINTING; read -r NERD_PATCH; read -r WANT_VARIABLE; } \
- < "$srcdir/.build_state" 2>/dev/null || { HINTING=false; NERD_PATCH=true; WANT_VARIABLE=false; }
+ # Reuse the single source of truth written by prepare(); makepkg runs each
+ # function in a fresh shell, so nothing is inherited.
+ local HINTING NERD_PATCH WANT_VARIABLE HINT_ENGINE
+ _load_options
install -d "$pkgdir/usr/share/fonts/apple"
if [[ "$HINTING" == true ]]; then
- # Hinted TTFs are always in patched/hinted/ regardless of NERD_PATCH path
- install -m644 "$srcdir/patched/hinted/"*.ttf "$pkgdir/usr/share/fonts/apple"
+ # Hinted output (TTF and/or OTF for the cff engine) lives in patched/hinted/.
+ # Fail loud if it is empty — never silently ship a fontless package.
+ if compgen -G "$srcdir/patched/hinted/*.ttf" >/dev/null; then
+ install -m644 "$srcdir/patched/hinted/"*.ttf "$pkgdir/usr/share/fonts/apple"
+ fi
+ if compgen -G "$srcdir/patched/hinted/*.otf" >/dev/null; then
+ install -m644 "$srcdir/patched/hinted/"*.otf "$pkgdir/usr/share/fonts/apple"
+ fi
+ if ! compgen -G "$pkgdir/usr/share/fonts/apple/*" >/dev/null; then
+ echo "Error: hinting was requested but produced no fonts."; exit 1
+ fi
elif [[ "$NERD_PATCH" == true ]]; then
- # Patched but not hinted: font-patcher always outputs TTF
- install -m644 "$srcdir/patched/"*.ttf "$pkgdir/usr/share/fonts/apple"
+ # Patched but not hinted: font-patcher emits TTF for TTF inputs and OTF for
+ # OTF inputs (e.g. SF Mono), so install both. patched/hinted/ doesn't exist
+ # on this path, so the shallow globs won't pick up hinted output.
+ if compgen -G "$srcdir/patched/*.ttf" >/dev/null; then
+ install -m644 "$srcdir/patched/"*.ttf "$pkgdir/usr/share/fonts/apple"
+ fi
+ if compgen -G "$srcdir/patched/*.otf" >/dev/null; then
+ install -m644 "$srcdir/patched/"*.otf "$pkgdir/usr/share/fonts/apple"
+ fi
+ if ! compgen -G "$pkgdir/usr/share/fonts/apple/*" >/dev/null; then
+ echo "Error: patching was requested but produced no fonts."; exit 1
+ fi
else
- # No patching, no hinting: install selected fonts directly (OTF + optional variable TTF)
- if compgen -G "$srcdir/selected/*.otf" &>/dev/null; then
+ # No patching, no hinting: install selected fonts directly.
+ if compgen -G "$srcdir/selected/*.otf" >/dev/null; then
install -m644 "$srcdir/selected/"*.otf "$pkgdir/usr/share/fonts/apple"
fi
- if [[ "$WANT_VARIABLE" == true ]] && compgen -G "$srcdir/selected/*.ttf" &>/dev/null; then
+ if [[ "$WANT_VARIABLE" == true ]] && compgen -G "$srcdir/selected/*.ttf" >/dev/null; then
install -m644 "$srcdir/selected/"*.ttf "$pkgdir/usr/share/fonts/apple"
+ fi
+ if ! compgen -G "$pkgdir/usr/share/fonts/apple/*" >/dev/null; then
+ echo "Error: no fonts were installed."; exit 1
fi
fi

Scan history

Scanned at (UTC)SeverityRules
2026-09-22 11:37:52 Medium 1
2026-06-19 19:07:35 Clean 2
2026-06-18 16:11:54 Medium 1

Report a package

Reports go to the AURWatch maintainer (one person) and are read by hand. No login required.

0 / 4000
Your suggestion