embed the standard scaling rules

This commit is contained in:
2026-10-17 12:23:09 -06:00
parent 00cfdb4c55
commit 9febbb175d
2 changed files with 73 additions and 10 deletions
+26 -10
View File
@@ -12,6 +12,7 @@ import re
import shutil
import subprocess
import sys
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[2]
MACH_MAGIC = {bytes.fromhex(h) for h in ("feedface", "cefaedfe", "feedfacf", "cffaedfe", "cafebabe", "bebafeca", "cafebabf", "bfbafeca")}
@@ -123,6 +124,29 @@ def copy_tree(source: Path, target: Path) -> None:
shutil.copytree(source, target, dirs_exist_ok=True, symlinks=False)
def write_fontconfig(resources: Path, fontconfig_prefix: Path) -> None:
# Noto Color Emoji has fixed-size color bitmaps. Fontconfig supplies the
# requested-size/strike-size matrix; without it glyphs render at 109px.
rule = fontconfig_prefix / "share/fontconfig/conf.avail/10-scale-bitmap-fonts.conf"
if not rule.is_file():
raise RuntimeError(f"Required Fontconfig bitmap scaling rules not found: {rule}")
resources.mkdir(parents=True, exist_ok=True)
config = ET.fromstring('''<fontconfig>
<dir>/System/Library/Fonts</dir><dir>/Library/Fonts</dir><dir>~/Library/Fonts</dir>
<cachedir prefix="xdg">fontconfig</cachedir>
<alias><family>sans-serif</family><prefer><family>Helvetica</family></prefer></alias>
<alias><family>monospace</family><prefer><family>Menlo</family></prefer></alias>
</fontconfig>
''')
# Embed the standard rules so they work after relocation without depending
# on Homebrew or Fontconfig's host configuration search paths.
for element in reversed(list(ET.parse(rule).getroot())):
config.insert(0, element)
(resources / "fonts.conf").write_text(
'<?xml version="1.0"?>\n<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">\n' +
ET.tostring(config, encoding="unicode") + "\n", encoding="utf-8")
def audit(app: Path) -> dict[str, object]:
binary = app / "Contents/MacOS/zoitechat"
architectures = set(run("lipo", "-archs", binary).split())
@@ -180,7 +204,7 @@ def package(stage: Path, app: Path, build_number: str) -> None:
seeds.append((source, resources / "lib/zoitechat/plugins" / source.name))
prefixes = {formula: Path(run("brew", "--prefix", formula)) for formula in (
"gtk+3", "gdk-pixbuf", "glib", "glib-networking", "librsvg", "enchant", "adwaita-icon-theme", "hicolor-icon-theme", "gsettings-desktop-schemas", "ca-certificates", "openssl@3", "lua", "libarchive", "gtk-mac-integration")}
"gtk+3", "gdk-pixbuf", "glib", "glib-networking", "librsvg", "enchant", "adwaita-icon-theme", "hicolor-icon-theme", "gsettings-desktop-schemas", "ca-certificates", "openssl@3", "lua", "libarchive", "gtk-mac-integration", "fontconfig")}
# Runtime-loaded code is not visible in the main executable's otool output.
module_groups = [
(prefixes["gdk-pixbuf"] / "lib/gdk-pixbuf-2.0/2.10.0/loaders", "lib/gdk-pixbuf-2.0/2.10.0/loaders"),
@@ -241,15 +265,7 @@ def package(stage: Path, app: Path, build_number: str) -> None:
data = data.replace(str(source), "@ZOITECHAT_RESOURCES@/" + str(target.relative_to(resources)))
(resources / cache).write_text(data + "\n")
# The private font map loads the bundled Noto font; ordinary GTK fonts stay local.
(resources / "fonts.conf").write_text('''<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<fontconfig>
<dir>/System/Library/Fonts</dir><dir>/Library/Fonts</dir><dir>~/Library/Fonts</dir>
<cachedir prefix="xdg">fontconfig</cachedir>
<alias><family>sans-serif</family><prefer><family>Helvetica</family></prefer></alias>
<alias><family>monospace</family><prefer><family>Menlo</family></prefer></alias>
</fontconfig>
''')
write_fontconfig(resources, prefixes["fontconfig"])
(resources / "zoitechat-bundle").write_text("1\n")
licenses = resources / "licenses"
licenses.mkdir()
+47
View File
@@ -1,16 +1,63 @@
"""Dependency parsing, relocation guards and release audit tests without a Mac SDK."""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from xml.sax.saxutils import escape
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import bundle
class BundleTests(unittest.TestCase):
def test_missing_bitmap_scaling_rules_fail_packaging(self):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
with self.assertRaisesRegex(RuntimeError, "bitmap scaling rules not found"):
bundle.write_fontconfig(directory / "Resources", directory / "missing-prefix")
@unittest.skipUnless(shutil.which("fc-match"), "requires Fontconfig's matching tool")
def test_bundled_bitmap_scaling_survives_relocation(self):
prefix = Path(bundle.run("brew", "--prefix", "fontconfig")) if sys.platform == "darwin" else Path("/usr")
rule = prefix / "share/fontconfig/conf.avail/10-scale-bitmap-fonts.conf"
if not rule.is_file():
self.skipTest("Fontconfig bitmap scaling configuration unavailable")
font = bundle.ROOT / "data/fonts/NotoColorEmoji.ttf"
with tempfile.TemporaryDirectory(prefix="zoitechat font scaling ") as temporary:
directory = Path(temporary)
source = directory / "build-prefix/share/fontconfig/conf.avail" / rule.name
source.parent.mkdir(parents=True)
shutil.copy2(rule, source)
resources = directory / "Resources"
bundle.write_fontconfig(resources, directory / "build-prefix")
relocated = directory / "Relocated Resources"
shutil.move(resources, relocated)
source.unlink()
# Exercise the shipped configuration without consulting host font
# rules. Only the bundled font is visible after reset-dirs.
wrapper = directory / "probe.conf"
wrapper.write_text(
'<fontconfig><include>' + escape(str(relocated / "fonts.conf")) +
'</include><reset-dirs/><dir>' + escape(str(font.parent)) + '</dir></fontconfig>')
env = os.environ.copy()
env["FONTCONFIG_FILE"] = str(wrapper)
env["XDG_CACHE_HOME"] = str(directory / "cache")
for requested in (12, 16, 24):
output = subprocess.check_output(
["fc-match", "-f", "%{pixelsize}\\n%{matrix}\\n%{file}\\n",
f"Noto Color Emoji:pixelsize={requested}"], env=env, cwd=directory, text=True)
pixels, matrix, matched = output.splitlines()
self.assertEqual(Path(matched).resolve(), font.resolve())
values = [float(value) for value in matrix.split()]
self.assertEqual(len(values), 4, "bitmap scaling matrix missing")
self.assertAlmostEqual(float(pixels) * values[0], requested, places=3)
self.assertAlmostEqual(float(pixels) * values[3], requested, places=3)
self.assertEqual(values[1:3], [0.0, 0.0])
@unittest.skipUnless(sys.platform == "darwin", "requires Apple's Mach-O tools")
def test_signed_binary_relocation_and_resigning(self):
with tempfile.TemporaryDirectory(prefix="zoitechat relocation ") as temporary: