> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
atuq 0.2.0: branding real — el zip, el .ini, los binarios y un icono que no es de otro
Hash b3:7eff4ba4. La v0.1 dejó el branding fuera porque había que MIRAR el árbol antes de adivinar.
Se miró, y lo que apareció decidió la forma de esta versión:
- `application.ini` del appdir arranca diciendo «This file is not used». Es herencia del árbol de
desarrollo: en un build empaquetado el launcher SÍ lo lee. `rebrand.py` no le cree al comentario:
comprueba que las claves estén y falla si no.
- Las cadenas que la gente VE están DENTRO de `browser/omni.ja` ⇒ sin re-empacar el zip no hay
branding. Y el original no dice «Firefox», dice **Nightly**, porque construimos con
`--with-branding=unofficial`. Sin tocarlo, atuq se presentaría como lo peor de los dos mundos.
- Los ICONOS, en cambio, viven FUERA del zip ⇒ se reemplazan sin abrirlo.
EL RE-EMPAQUE PRESERVA TRES PROPIEDADES DEL ORIGINAL, Y CADA UNA POR UNA RAZÓN MEDIDA:
orden de las 5306 entradas (es el que Gecko lee al arrancar, y es lo que el jarlog del PGO va a
refinar), `compress_type=0` (Mozilla lo deja SIN COMPRIMIR para poder mapearlo) y `date_time`
2010-01-01 (ya venía normalizado: Mozilla también persigue reproducibilidad).
VERIFICADO CONTRA EL ARTEFACTO, NO CONTRA EL LOG:
- diff entrada por entrada entre el omni.ja de firefox y el de atuq: **5306 → 5306, mismo orden,
todo STORED, mismas fechas, y UNA SOLA entrada con contenido distinto**: brand.ftl.
- re-empacar dos veces el mismo zip da el mismo sha256 ⇒ el paso es determinista.
- application.ini: Vendor=tawasuyu, Name/RemotingName/CodeName=atuq. `ID` NO se toca: es el GUID
con el que las extensiones declaran compatibilidad, y cambiarlo dejaría a atuq fuera del
ecosistema de complementos.
EL BuildID SE DERIVA DEL CONTENIDO DEL OVERLAY, no de la fecha. Es la clave con la que Gecko
invalida su startup cache: si atuq cambia su chrome y el BuildID no se mueve, el navegador arranca
con la interfaz vieja cacheada y PARECE que el overlay no agarró. Derivarlo del contenido lo mueve
exactamente cuando hace falta y dos builds del mismo atuq dan el mismo número — con la fecha
pasaría lo contrario en los dos sentidos. Se vio funcionar: al sumar `CodeName` el BuildID cambió
solo, de 81921491609672 a 24128696592342.
El icono se DIBUJA en código (PNG en Python puro, zlib + struct) y no se commitea como binario:
un PNG en el árbol no se puede revisar en un diff, el código sí, y el resultado es idéntico en cada
build. ⚠ Es un marcador de posición declarado —una marca geométrica plana, 5 tamaños—; lo correcto
es que alguien dibuje el zorro. Su única función irrenunciable ya la cumple: que la ventana y el
lanzador NO muestren el icono de otro producto.
Binarios: firefox → atuq y firefox-bin → atuq-bin, con symlink `firefox` → `atuq` dentro del appdir
porque hay scripts de terceros que invocan por el nombre histórico y Gecko resuelve su directorio
por /proc/self/exe (entrar por el symlink resuelve al mismo sitio). Más `.desktop` con
StartupWMClass=atuq, que casa con el app_id que da RemotingName bajo Wayland.
diff --git a/recipes/atuq.toml b/recipes/atuq.toml
index 6a243c75..59ec0bb1 100644
--- a/recipes/atuq.toml
+++ b/recipes/atuq.toml
@@ -15,14 +15,24 @@
# vive en `recipes/atuq/` y se hashea por CONTENIDO con `ArtifactHash::of_tree`, igual que ya se
# hacía con los `patches`. No hay commit que pinear y no hay fetch: editar un CSS mueve el hash.
#
-# ══ LO QUE ESTA v0.1 NO HACE, A PROPÓSITO ══════════════════════════════════════════════════════
-# No renombra el binario ni toca `application.ini`. El branding necesita mirar la forma real del
-# árbol que sella `firefox` —qué es `firefox-bin`, cómo se resuelve el appdir— y adivinarlo desde
-# acá produciría un artefacto que arranca en la máquina de quien lo escribió y en ninguna otra.
-# Tampoco re-empaqueta `omni.ja`, que es donde vive el chrome de verdad; eso es la v0.2 y trae dos
-# condiciones ya sabidas (re-empacar determinista y preservar el orden del `jarlog` del PGO).
+# ══ v0.2: EL BRANDING VIVE DENTRO DEL ZIP ══════════════════════════════════════════════════════
+# La v0.1 dejó el branding fuera porque había que mirar el árbol real antes de adivinar. Ya se miró,
+# y lo que se encontró decide la forma de esta versión:
+#
+# · `application.ini` del appdir arranca con el comentario «This file is not used». Es HERENCIA
+# del árbol de desarrollo: en un build empaquetado el launcher SÍ lo lee. `rebrand.py` no le
+# cree al comentario, comprueba que las claves estén y falla si no.
+# · Las cadenas que la gente VE están dentro de `browser/omni.ja` (`brand.ftl`), así que sin
+# re-empacar el zip no hay branding posible. Y el original dice **Nightly**, no «Firefox»,
+# porque construimos con `--with-branding=browser/branding/unofficial`.
+# · Los ICONOS, en cambio, están FUERA del zip (`browser/chrome/icons/default/`) ⇒ se reemplazan
+# sin tocarlo.
+# · `browser/omni.ja` son 56 MB con TODAS sus entradas sin comprimir y con `date_time` ya
+# normalizado a 2010-01-01: Mozilla también persigue reproducibilidad. El re-empaque preserva
+# orden, método y fecha — el orden porque es el que Gecko lee al arrancar, y es lo que el
+# `jarlog` del PGO refinará cuando exista.
name = "atuq"
-version = "0.1.0"
+version = "0.2.0"
# El artefacto CONTIENE Firefox, así que hereda su licencia. El overlay de `recipes/atuq/` es
# nuestro, pero eso no cambia lo que se distribuye.
license = "MPL-2.0"
@@ -51,34 +61,42 @@ DST=/out/usr/lib/atuq
# hasta el final diciendo que todo fue bien (regla 3 del CLAUDE.md).
[ -d "$SRC" ] || { echo "atuq: no está el árbol de firefox en $SRC — ¿la dep se materializó?" >&2; exit 1; }
[ -x "$SRC/firefox" ] || { echo "atuq: $SRC/firefox no existe o no es ejecutable" >&2; exit 1; }
-[ -d "$SRC/browser" ] || { echo "atuq: falta $SRC/browser — el árbol no tiene la forma esperada" >&2; exit 1; }
+[ -f "$SRC/browser/omni.ja" ] || { echo "atuq: falta $SRC/browser/omni.ja" >&2; exit 1; }
mkdir -p "$DST"
cp -a "$SRC"/. "$DST"/
-# ── LA CAPA DE atuq ───────────────────────────────────────────────────────────────────────────
+# ── LA CAPA DE CONFIGURACIÓN ──────────────────────────────────────────────────────────────────
# `defaults/pref/` es el único sitio desde el que se puede pedir el autoconfig ANTES de que exista
# un perfil; el resto cuelga de ahí.
mkdir -p "$DST/defaults/pref" "$DST/chrome" "$DST/distribution"
-cp /src/prefs/autoconfig.js "$DST/defaults/pref/autoconfig.js"
-cp /src/atuq.cfg "$DST/atuq.cfg"
-cp /src/chrome/atuq.css "$DST/chrome/atuq.css"
+cp /src/prefs/autoconfig.js "$DST/defaults/pref/autoconfig.js"
+cp /src/atuq.cfg "$DST/atuq.cfg"
+cp /src/chrome/atuq.css "$DST/chrome/atuq.css"
cp /src/distribution/policies.json "$DST/distribution/policies.json"
-# El comando que teclea la gente. Apunta al binario SIN renombrar (ver la nota de la cabecera):
-# renombrarlo es branding y branding es la v0.2.
-mkdir -p /out/usr/bin
-ln -sf ../lib/atuq/firefox /out/usr/bin/atuq
+# ── EL BRANDING ───────────────────────────────────────────────────────────────────────────────
+# Todo lo que exige entrar al zip, editar el .ini o dibujar un PNG está en una herramienta aparte:
+# el shell no tiene `zip` en este lab, y `zipfile` además deja fijar orden, fecha y permisos, que
+# es de lo que depende que el artefacto reproduzca. La herramienta VERIFICA cada cambio y falla si
+# la entrada que iba a reemplazar no estaba.
+python3 /src/tools/rebrand.py "$DST" /src /out
+
+# ── INTEGRACIÓN CON EL ESCRITORIO ─────────────────────────────────────────────────────────────
+mkdir -p /out/usr/bin /out/usr/share/applications
+cp /src/atuq.desktop /out/usr/share/applications/atuq.desktop
+ln -sf ../lib/atuq/atuq /out/usr/bin/atuq
-# ── INVENTARIO: LO QUE SE MIRA CUANDO «ATUQ SE VE COMO FIREFOX» ───────────────────────────────
-# Los cuatro ficheros de la capa son el 100% de lo que distingue este artefacto de su base, y son
-# los que un `cp` silencioso podría no haber puesto. Se listan para que el log del build responda
-# solo la primera pregunta del diagnóstico.
+# ── INVENTARIO ────────────────────────────────────────────────────────────────────────────────
+# Lo que distingue este artefacto de su base, listado con tamaños, para que el log del build
+# responda solo la primera pregunta del diagnóstico cuando «atuq se ve como Firefox».
echo "atuq: capa aplicada —"
for f in defaults/pref/autoconfig.js atuq.cfg chrome/atuq.css distribution/policies.json; do
[ -s "$DST/$f" ] || { echo "atuq: $f quedó VACÍO o ausente" >&2; exit 1; }
echo " $f ($(wc -c < "$DST/$f") bytes)"
done
+[ -x "$DST/atuq" ] || { echo "atuq: el binario renombrado no quedó ejecutable" >&2; exit 1; }
+grep -q '^Name=atuq' "$DST/application.ini" || { echo "atuq: application.ini no quedó rebrandeado" >&2; exit 1; }
'''
[deps]
@@ -86,4 +104,4 @@ done
# dentro del artefacto de atuq y a partir de ahí atuq no lo necesita más. El precio es que el
# artefacto pesa lo que pesa Firefox otra vez; la salida barata (una granja de symlinks) rompe la
# resolución del appdir por /proc/self/exe, así que la v0.1 paga el disco y lo dice.
-build = ["firefox"]
+build = ["firefox", "python3"]
diff --git a/recipes/atuq/atuq.desktop b/recipes/atuq/atuq.desktop
new file mode 100644
index 00000000..e590a407
--- /dev/null
+++ b/recipes/atuq/atuq.desktop
@@ -0,0 +1,14 @@
+[Desktop Entry]
+Type=Application
+Name=atuq
+GenericName=Navegador web
+GenericName[en]=Web Browser
+Comment=Navegá la web con el navegador de la distro
+Comment[en]=Browse the web with the distro's browser
+Exec=atuq %u
+Icon=atuq
+Terminal=false
+Categories=Network;WebBrowser;
+StartupNotify=true
+StartupWMClass=atuq
+MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https;
diff --git a/recipes/atuq/branding/brand.ftl b/recipes/atuq/branding/brand.ftl
new file mode 100644
index 00000000..ca5511dc
--- /dev/null
+++ b/recipes/atuq/branding/brand.ftl
@@ -0,0 +1,16 @@
+# Reemplaza a `localization/en-US/branding/brand.ftl` dentro de `browser/omni.ja`.
+#
+# Éstas son las cadenas que la GENTE ve: el título de la ventana, «Acerca de», el menú. El original
+# de nuestra base dice «Nightly» —no «Firefox»— porque `recipes/firefox.toml` construye con
+# `--with-branding=browser/branding/unofficial`, cuyo nombre en clave es ése. Sin este fichero, atuq
+# se presentaría como Nightly, que es lo peor de los dos mundos: ni es Firefox ni es nuestro.
+#
+# `-brand-product-name` se mantiene distinto de `-brand-short-name` a propósito: hay cadenas de la
+# UI que hablan del PRODUCTO y otras de la MARCA, y aplanarlas produce frases raras en español.
+-brand-shorter-name = atuq
+-brand-short-name = atuq
+-brand-shortcut-name = atuq
+-brand-full-name = atuq
+-brand-product-name = atuq
+-vendor-short-name = tawasuyu
+trademarkInfo = { " " }
diff --git a/recipes/atuq/tools/rebrand.py b/recipes/atuq/tools/rebrand.py
new file mode 100644
index 00000000..04c21348
--- /dev/null
+++ b/recipes/atuq/tools/rebrand.py
@@ -0,0 +1,272 @@
+#!/usr/bin/env python3
+"""Convierte el árbol de `firefox` ya copiado en el árbol de `atuq`.
+
+Uso: rebrand.py <appdir> <overlay> <outroot>
+ appdir = /out/usr/lib/atuq (la copia del árbol de firefox)
+ overlay = /src (este directorio, `recipes/atuq/`)
+ outroot = /out
+
+POR QUÉ UNA HERRAMIENTA Y NO CUATRO LÍNEAS DE SHELL
+---------------------------------------------------
+El branding de verdad vive DENTRO de `browser/omni.ja`, que es un zip, y re-empacarlo desde shell
+exige `zip`, que el lab no trae. `zipfile` de la stdlib, además de estar disponible, da control
+sobre las tres cosas que el CLI no deja fijar y de las que depende que el artefacto REPRODUZCA:
+el orden de las entradas, su `date_time` y sus permisos.
+
+TODO CAMBIO ES VERIFICADO, NINGUNO ES SILENCIOSO
+------------------------------------------------
+Cada reemplazo declara qué entrada espera encontrar y falla si no está. Un rebranding que no
+encuentra la cadena que iba a cambiar produce un navegador que se sigue llamando Nightly y sella
+igual de bien: sería exactamente la clase de fallo que llega hasta el final diciendo que todo fue
+bien (regla 3 del CLAUDE.md).
+"""
+import hashlib
+import os
+import struct
+import sys
+import zlib
+import zipfile
+
+APPDIR, OVERLAY, OUTROOT = sys.argv[1], sys.argv[2], sys.argv[3]
+
+MARCA = "atuq"
+VENDOR = "tawasuyu"
+ACENTO = (0xB5, 0x65, 0x1D) # el mismo token que chrome/atuq.css
+CREMA = (0xF2, 0xE8, 0xD5)
+TAMANOS = (16, 32, 48, 64, 128)
+
+
+def fatal(msg):
+ print(f"rebrand: {msg}", file=sys.stderr)
+ raise SystemExit(1)
+
+
+# ── 1. La huella del overlay, que es también el BuildID ────────────────────────────────────────
+# El `BuildID` es la clave con la que Gecko invalida su startup cache. Si atuq cambia su chrome y
+# el BuildID no se mueve, el navegador arranca con la interfaz vieja cacheada y PARECE que el
+# overlay no agarró — el falso negativo que el SDD 26 §2 anota como gotcha del camino derivado.
+#
+# Se deriva del CONTENIDO del overlay y no de la fecha: cambia exactamente cuando cambia lo que
+# tiene que invalidar, y dos builds del mismo atuq dan el mismo número. La fecha haría lo contrario
+# en los dos sentidos.
+def huella_overlay():
+ h = hashlib.sha256()
+ for raiz, dirs, ficheros in os.walk(OVERLAY):
+ dirs.sort()
+ for f in sorted(ficheros):
+ p = os.path.join(raiz, f)
+ h.update(os.path.relpath(p, OVERLAY).encode())
+ h.update(b"\0")
+ with open(p, "rb") as fh:
+ h.update(fh.read())
+ h.update(b"\0")
+ return h.hexdigest()
+
+
+def build_id(huella):
+ # 14 dígitos decimales: la forma que Gecko espera (nació de un `YYYYMMDDHHMMSS`).
+ return str(int(huella[:16], 16))[:14].rjust(14, "0")
+
+
+# ── 2. application.ini — la identidad de la aplicación ─────────────────────────────────────────
+# ⚠ La cabecera de este fichero dice «This file is not used», y es un comentario HEREDADO del árbol
+# de desarrollo: en un build empaquetado como éste el launcher SÍ lo lee. Se comprueba en vez de
+# creerle al comentario — si no encontramos las claves, fallamos.
+#
+# `ID` NO se toca a propósito: es el GUID con el que las extensiones declaran compatibilidad
+# (`gecko.id` de Firefox). Cambiarlo convertiría a atuq en una aplicación desconocida para todo el
+# ecosistema de complementos, que es justo lo que un envoltorio no quiere.
+def rebrandear_application_ini(bid):
+ ruta = os.path.join(APPDIR, "application.ini")
+ if not os.path.isfile(ruta):
+ fatal(f"no está {ruta}")
+ with open(ruta, encoding="utf-8") as fh:
+ lineas = fh.read().splitlines()
+ # `CodeName` también: es el residuo de `--with-branding=unofficial` (dice «Nightly») y aparece
+ # en about:buildconfig y en los informes de fallo. Dejarlo sería exactamente el rincón por el
+ # que atuq se sigue presentando como otro producto.
+ cambios = {
+ "Vendor": VENDOR,
+ "Name": MARCA,
+ "RemotingName": MARCA,
+ "CodeName": MARCA,
+ "BuildID": bid,
+ }
+ vistas = set()
+ salida = []
+ for ln in lineas:
+ clave = ln.split("=", 1)[0] if "=" in ln else None
+ if clave in cambios:
+ salida.append(f"{clave}={cambios[clave]}")
+ vistas.add(clave)
+ else:
+ salida.append(ln)
+ faltan = set(cambios) - vistas
+ if faltan:
+ fatal(f"application.ini no tenía las claves {sorted(faltan)} — ¿cambió el formato?")
+ with open(ruta, "w", encoding="utf-8") as fh:
+ fh.write("\n".join(salida) + "\n")
+ return vistas
+
+
+# ── 3. omni.ja — el re-empaque determinista ────────────────────────────────────────────────────
+# TRES PROPIEDADES DEL ORIGINAL QUE SE PRESERVAN, Y POR QUÉ:
+#
+# · EL ORDEN de las entradas. Gecko lee este zip en el arranque y el orden es el que Mozilla
+# eligió para que las primeras lecturas caigan juntas. Reordenarlo (p.ej. alfabético, que es lo
+# que sale «gratis» al recomprimir) no rompe nada visible: sólo hace el arranque más lento, en
+# silencio. Cuando exista el PGO, el `jarlog` refina justamente este orden — tirarlo sería
+# tirar media optimización de arranque.
+# · `compress_type = 0` (STORED). El omni.ja de Mozilla va SIN COMPRIMIR a propósito, para poder
+# mapearlo en memoria. Comprimirlo achica el artefacto y encarece cada lectura.
+# · `date_time`. El original ya viene normalizado a 2010-01-01 —Mozilla también persigue
+# reproducibilidad— así que copiarlo tal cual mantiene el zip byte-idéntico salvo por lo que
+# cambiamos de verdad.
+def repack_omni(ruta_ja, reemplazos):
+ if not os.path.isfile(ruta_ja):
+ fatal(f"no está {ruta_ja}")
+ pendientes = dict(reemplazos)
+ tmp = ruta_ja + ".nuevo"
+ with zipfile.ZipFile(ruta_ja) as orig:
+ infos = orig.infolist()
+ with zipfile.ZipFile(tmp, "w") as nuevo:
+ for i in infos:
+ datos = pendientes.pop(i.filename, None)
+ if datos is None:
+ datos = orig.read(i.filename)
+ ni = zipfile.ZipInfo(i.filename, date_time=i.date_time)
+ ni.compress_type = i.compress_type
+ ni.external_attr = i.external_attr
+ ni.create_system = i.create_system
+ nuevo.writestr(ni, datos)
+ if pendientes:
+ os.unlink(tmp)
+ fatal(f"{ruta_ja}: estas entradas no existían y no se reemplazó nada: {sorted(pendientes)}")
+ os.replace(tmp, ruta_ja)
+ return len(infos)
+
+
+# ── 4. El icono ────────────────────────────────────────────────────────────────────────────────
+# Se DIBUJA acá, en vez de commitear cinco PNG, por una razón práctica: un binario en el árbol de
+# fuentes no se puede revisar en un diff. El código sí, y el resultado es idéntico en cada build.
+#
+# ⚠ ES UN MARCADOR DE POSICIÓN, y conviene que se note: una marca geométrica plana, no un intento de
+# logo. Lo correcto es que alguien dibuje el zorro; hasta entonces esto cumple la única función que
+# no puede quedar vacía —que la ventana y el lanzador NO muestren el icono de otro producto—.
+def _en_rect_redondeado(px, py, n, r):
+ cx = min(max(px, r), n - r)
+ cy = min(max(py, r), n - r)
+ dx, dy = px - cx, py - cy
+ return dx * dx + dy * dy <= r * r
+
+
+def _en_triangulo(px, py, a, b, c):
+ def signo(p, q, r):
+ return (p[0] - r[0]) * (q[1] - r[1]) - (q[0] - r[0]) * (p[1] - r[1])
+ d1, d2, d3 = signo((px, py), a, b), signo((px, py), b, c), signo((px, py), c, a)
+ neg = d1 < 0 or d2 < 0 or d3 < 0
+ pos = d1 > 0 or d2 > 0 or d3 > 0
+ return not (neg and pos)
+
+
+def dibujar(size, ss=3):
+ n = size * ss
+ r = n * 0.22
+ # La «oreja»: un triángulo centrado, con la punta arriba. A 16 px cualquier cosa con más de
+ # tres vértices se convierte en barro, así que la forma es deliberadamente una sola.
+ a = (n * 0.50, n * 0.26)
+ b = (n * 0.74, n * 0.72)
+ c = (n * 0.26, n * 0.72)
+ filas = []
+ for y in range(size):
+ fila = []
+ for x in range(size):
+ acc = [0, 0, 0, 0]
+ for sy in range(ss):
+ for sx in range(ss):
+ px, py = x * ss + sx + 0.5, y * ss + sy + 0.5
+ if not _en_rect_redondeado(px, py, n, r):
+ continue
+ col = CREMA if _en_triangulo(px, py, a, b, c) else ACENTO
+ acc[0] += col[0]
+ acc[1] += col[1]
+ acc[2] += col[2]
+ acc[3] += 255
+ m = ss * ss
+ fila.append((acc[0] // m, acc[1] // m, acc[2] // m, acc[3] // m))
+ filas.append(fila)
+ return filas
+
+
+def png(filas):
+ size = len(filas)
+ crudo = b"".join(b"\x00" + bytes(v for px in f for v in px) for f in filas)
+
+ def trozo(tipo, datos):
+ cuerpo = tipo + datos
+ return struct.pack(">I", len(datos)) + cuerpo + struct.pack(">I", zlib.crc32(cuerpo) & 0xFFFFFFFF)
+
+ ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
+ return (b"\x89PNG\r\n\x1a\n" + trozo(b"IHDR", ihdr)
+ + trozo(b"IDAT", zlib.compress(crudo, 9)) + trozo(b"IEND", b""))
+
+
+def escribir_iconos():
+ dir_app = os.path.join(APPDIR, "browser", "chrome", "icons", "default")
+ if not os.path.isdir(dir_app):
+ fatal(f"no está {dir_app} — el árbol de firefox no tiene la forma esperada")
+ escritos = []
+ for s in TAMANOS:
+ datos = png(dibujar(s))
+ # (a) el icono de la VENTANA, que Gecko lee del appdir
+ p = os.path.join(dir_app, f"default{s}.png")
+ with open(p, "wb") as fh:
+ fh.write(datos)
+ escritos.append(p)
+ # (b) el icono del LANZADOR, que lo lee el escritorio por el tema hicolor
+ q = os.path.join(OUTROOT, "usr/share/icons/hicolor", f"{s}x{s}", "apps", f"{MARCA}.png")
+ os.makedirs(os.path.dirname(q), exist_ok=True)
+ with open(q, "wb") as fh:
+ fh.write(datos)
+ escritos.append(q)
+ return escritos
+
+
+# ── 5. Los binarios ────────────────────────────────────────────────────────────────────────────
+# Se renombran los dos y se deja un symlink `firefox` → `atuq` DENTRO del appdir. El symlink no es
+# indecisión: hay código y scripts de terceros que invocan el binario por su nombre histórico, y
+# como Gecko resuelve su directorio por `/proc/self/exe`, entrar por el symlink resuelve al mismo
+# sitio y no cambia nada del comportamiento.
+def renombrar_binarios():
+ hechos = []
+ for viejo, nuevo in (("firefox", MARCA), ("firefox-bin", f"{MARCA}-bin")):
+ vp = os.path.join(APPDIR, viejo)
+ if not os.path.isfile(vp):
+ fatal(f"no está {vp}")
+ os.rename(vp, os.path.join(APPDIR, nuevo))
+ hechos.append(f"{viejo} → {nuevo}")
+ os.symlink(MARCA, os.path.join(APPDIR, "firefox"))
+ return hechos
+
+
+def main():
+ huella = huella_overlay()
+ bid = build_id(huella)
+ claves = rebrandear_application_ini(bid)
+ with open(os.path.join(OVERLAY, "branding", "brand.ftl"), "rb") as fh:
+ brand = fh.read()
+ n = repack_omni(
+ os.path.join(APPDIR, "browser", "omni.ja"),
+ {"localization/en-US/branding/brand.ftl": brand},
+ )
+ iconos = escribir_iconos()
+ binarios = renombrar_binarios()
+
+ print(f"rebrand: overlay {huella[:16]}… ⇒ BuildID {bid}")
+ print(f"rebrand: application.ini {sorted(claves)}")
+ print(f"rebrand: browser/omni.ja re-empacado, {n} entradas, orden y STORED preservados")
+ print(f"rebrand: {len(iconos)} iconos escritos ({', '.join(str(s) for s in TAMANOS)} px)")
+ print(f"rebrand: binarios {', '.join(binarios)} (+ symlink firefox → {MARCA})")
+
+
+main()