Files
Termind/Scripts/update-host-icons.py
T
Kaede Akino dca37ca72f fix(icons): use the Simple Icons macOS logo
Replace the Apple silhouette with the unmodified macOS wordmark from
Simple Icons 16.29.0. Update generator and provenance, retain the stable
HostOS-macos asset/detection IDs, and add a real-bundle source regression.
Other 45 assets and all license files remain byte-identical.

Validation: 19 macOS and 20 iOS focused tests passed; icon integrity,
formatting, AppStrings, localization, and diff checks passed.

Full integration before this isolated resource change: all 1206 shared
iOS tests passed. Two inherited hosted-iOS identity/artwork tests remain
failing. Two unchanged macOS Keychain/Secure Enclave tests return -25308
(errSecInteractionNotAllowed), including a narrow retry. Neither default
full scheme is claimed completely green; no security checks were relaxed.
2026-10-17 09:51:04 +08:00

126 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Regenerate bundled host SVGs from explicit npm releases, never moving tags."""
import argparse
import base64
import hashlib
import io
import json
from pathlib import Path
import re
import shutil
import tarfile
import tempfile
import urllib.request
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[1]
RESOURCES = ROOT / "Frameworks/Resources"
def package(name, version):
if not re.fullmatch(r"\d+\.\d+\.\d+(?:-[\w.-]+)?", version):
raise ValueError("An explicit release version is required")
with urllib.request.urlopen(f"https://registry.npmjs.org/{name}/{version}", timeout=30) as response:
metadata = json.load(response)
with urllib.request.urlopen(metadata["dist"]["tarball"], timeout=60) as response:
data = response.read()
integrity = "sha512-" + base64.b64encode(hashlib.sha512(data).digest()).decode()
if metadata["version"] != version or integrity != metadata["dist"]["integrity"]:
raise ValueError(f"Package integrity mismatch: {name}")
archive = tarfile.open(fileobj=io.BytesIO(data), mode="r:gz")
return archive, {"package": name, "version": version, "integrity": integrity}
def read(archive, path):
return archive.extractfile("package/" + path).read()
def write_json(path, data):
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
def write_icon(directory, asset, svg):
text = svg.decode()
root = ET.fromstring(text)
if root.tag != "{http://www.w3.org/2000/svg}svg":
raise ValueError(f"Expected an SVG document: {asset}")
if re.search(r"<script|<foreignObject|<image|javascript:|(?:xlink:)?href\s*=", text, re.I):
raise ValueError(f"Unsafe SVG: {asset}")
# Asset catalogs rasterize standalone SVGs without a CSS currentColor context.
text = text.replace("currentColor", "#000000")
imageset = directory / (asset + ".imageset")
imageset.mkdir()
(imageset / (asset + ".svg")).write_text(text)
write_json(imageset / "Contents.json", {
"images": [{"filename": asset + ".svg", "idiom": "universal"}],
"info": {"author": "xcode", "version": 1},
"properties": {"preserves-vector-representation": True, "template-rendering-intent": "template"},
})
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--simple-icons-version", default="16.29.0")
parser.add_argument("--tabler-version", default="3.49.0")
args = parser.parse_args()
simple, simple_source = package("simple-icons", args.simple_icons_version)
tabler, tabler_source = package("@tabler/icons", args.tabler_version)
simple_source["source"] = "https://github.com/simple-icons/simple-icons"
tabler_source["source"] = "https://github.com/tabler/tabler-icons"
metadata = {entry["title"]: entry for entry in json.loads(read(simple, "data/simple-icons.json"))}
requested = json.loads((ROOT / "Scripts/linux-distribution-icons.json").read_text())
requested.append({"title": "macOS", "slug": "macos", "ids": ["macos"], "id": "macos"})
for title, slug, ids in [
("BSD", "bsd", ["bsd", "dragonfly"]),
("FreeBSD", "freebsd", ["freebsd"]),
("OpenBSD", "openbsd", ["openbsd"]),
("NetBSD", "netbsd", ["netbsd"]),
("Linux", "linux", ["linux"]),
]:
requested.append({"title": title, "slug": slug, "ids": ids, "id": slug})
with tempfile.TemporaryDirectory(prefix="termind-host-icons-") as temporary:
stage = Path(temporary)
assets = stage / "assets"
licenses = stage / "licenses"
assets.mkdir()
licenses.mkdir()
write_json(assets / "Contents.json", {"info": {"author": "xcode", "version": 1}})
entries = []
for item in requested:
info = metadata[item["title"]]
asset = "HostOS-" + item["id"] if "id" in item else "LinuxDistro-" + item["slug"]
write_icon(assets, asset, read(simple, "icons/" + item["slug"] + ".svg"))
entries.append({
"id": item.get("id", item["slug"]), "title": item["title"],
"asset": asset, "ids": item["ids"], "hex": info["hex"], "slug": item["slug"],
"sourceURL": info["source"], "license": info.get("license", {}).get("type"),
"package": "simple-icons",
})
write_icon(assets, "HostOS-windows", read(tabler, "icons/filled/brand-windows.svg"))
entries.append({
"id": "windows", "title": "Windows", "asset": "HostOS-windows", "ids": ["windows"],
"hex": "0078D4", "colorSource": "Termind presentation choice",
"sourceURL": f"https://github.com/tabler/tabler-icons/blob/v{args.tabler_version}/icons/filled/brand-windows.svg",
"license": "MIT", "package": "@tabler/icons",
})
for filename in ["LICENSE.md", "DISCLAIMER.md"]:
(licenses / ("SimpleIcons." + filename)).write_bytes(read(simple, filename))
(licenses / "Tabler.LICENSE").write_bytes(read(tabler, "LICENSE"))
write_json(stage / "HostIcons.json", {"sources": [simple_source, tabler_source], "entries": entries})
# Validate every input before replacing any generated output.
destination = RESOURCES / "Assets.xcassets/LinuxDistributions"
shutil.rmtree(destination)
shutil.copytree(assets, destination)
license_destination = RESOURCES / "HostIcons"
if license_destination.exists():
shutil.rmtree(license_destination)
shutil.copytree(licenses, license_destination)
shutil.copyfile(stage / "HostIcons.json", RESOURCES / "HostIcons.json")
print(f"Generated {len(entries)} host icons.")
if __name__ == "__main__":
main()