nerd-fonts-apple-hinted
MEDIUM
maintainer gaougalos
0 votes
scanned 2026-09-22 11:37:52.799854
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>
2
pkgname=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.
8
pkgver=22.0.5.4+nf3.5.1
9
pkgrel=1
10
pkgdesc="Apple fonts (SF Pro, SF Compact, SF Mono, SF Arabic, NY), optionally Nerd Fonts patched and/or autohinted (ttfautohint / CFF) with gasp tuning"
11
arch=(any)
12
url="https://developer.apple.com/fonts/"
13
license=("custom")
14
makedepends=("p7zip" "python" "python-fonttools" "python-skia-pathops" "parallel" "fontforge" "ttfautohint" "libarchive")
15
optdepends=("afdko: better CFF/OTF hinting via otfautohint (HINT_ENGINE=cff)")
16
options=(!strip !zipman)
17
conflicts=("nerd-fonts-apple" "apple-fonts" "nerd-fonts-sf-mono")
18
provides=("nerd-fonts-apple" "ttf-font-nerd")
19
source=(
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`.
38
sha256sums=('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
553
prepare() {
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
568
pkgver() {
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
591
build() {
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
858
package() {
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'
908
Apple San Francisco, SF Compact, SF Mono, SF Arabic, and New York fonts
909
are proprietary fonts owned by Apple Inc. They are provided for use on
910
Apple platforms and non-Apple platforms for personal, non-commercial use.
911
See https://developer.apple.com/fonts/ for the full license agreement.
912
913
Nerd Fonts glyphs are licensed under their respective open-source licenses.
914
See https://github.com/ryanoasis/nerd-fonts for details.
915
EOF
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) | Severity | Rules |
|---|---|---|
| 2026-09-22 11:37:52 | Medium | 1 |
| 2026-06-19 19:07:35 | Clean | 2 |
| 2026-06-18 16:11:54 | Medium | 1 |