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 shutil
import subprocess import subprocess
import sys import sys
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
MACH_MAGIC = {bytes.fromhex(h) for h in ("feedface", "cefaedfe", "feedfacf", "cffaedfe", "cafebabe", "bebafeca", "cafebabf", "bfbafeca")} 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) 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]: def audit(app: Path) -> dict[str, object]:
binary = app / "Contents/MacOS/zoitechat" binary = app / "Contents/MacOS/zoitechat"
architectures = set(run("lipo", "-archs", binary).split()) 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)) seeds.append((source, resources / "lib/zoitechat/plugins" / source.name))
prefixes = {formula: Path(run("brew", "--prefix", formula)) for formula in ( 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. # Runtime-loaded code is not visible in the main executable's otool output.
module_groups = [ module_groups = [
(prefixes["gdk-pixbuf"] / "lib/gdk-pixbuf-2.0/2.10.0/loaders", "lib/gdk-pixbuf-2.0/2.10.0/loaders"), (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))) data = data.replace(str(source), "@ZOITECHAT_RESOURCES@/" + str(target.relative_to(resources)))
(resources / cache).write_text(data + "\n") (resources / cache).write_text(data + "\n")
# The private font map loads the bundled Noto font; ordinary GTK fonts stay local. # The private font map loads the bundled Noto font; ordinary GTK fonts stay local.
(resources / "fonts.conf").write_text('''<?xml version="1.0"?> write_fontconfig(resources, prefixes["fontconfig"])
<!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>
''')
(resources / "zoitechat-bundle").write_text("1\n") (resources / "zoitechat-bundle").write_text("1\n")
licenses = resources / "licenses" licenses = resources / "licenses"
licenses.mkdir() licenses.mkdir()
+47
View File
@@ -1,16 +1,63 @@
"""Dependency parsing, relocation guards and release audit tests without a Mac SDK.""" """Dependency parsing, relocation guards and release audit tests without a Mac SDK."""
from pathlib import Path from pathlib import Path
import os
import shutil import shutil
import subprocess
import sys import sys
import tempfile import tempfile
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from xml.sax.saxutils import escape
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import bundle import bundle
class BundleTests(unittest.TestCase): 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") @unittest.skipUnless(sys.platform == "darwin", "requires Apple's Mach-O tools")
def test_signed_binary_relocation_and_resigning(self): def test_signed_binary_relocation_and_resigning(self):
with tempfile.TemporaryDirectory(prefix="zoitechat relocation ") as temporary: with tempfile.TemporaryDirectory(prefix="zoitechat relocation ") as temporary: