Buscando si los cambios de atuq habían llegado a la caja apareció esto: el hub y el worker calculaban `b3:e556024b` y un clon limpio `b3:5156cb8f`, con los 52 ficheros versionados de `recipes/atuq/` byte a byte IGUALES en las tres máquinas y la receta con el mismo md5. La diferencia era un fichero de más: `tools/__pycache__/rebrand.cpython-314.pyc`, bytecode que dejó alguien al correr `rebrand.py` dentro del repo. Comprobado en los dos sentidos — metiendo y sacando ese .pyc en un clon limpio, el hash salta de `5156cb8f` a `e556024b` y vuelve. Lo traicionero es que `__pycache__/` está en `.gitignore`: el árbol se ve limpio en `git status` mientras el hash ya es otro. Y el artefacto sellado en el store era el de `e556024b` — o sea uno que un clon limpio no puede reproducir jamás. Es la regla 3 al revés: no un vacío que pasa por lleno, sino basura invisible que pasa por fuente. `ArtifactHash::of_tree` no sabe de `.gitignore`, y son dos cosas legítimamente distintas; pero hoy divergen por descuido y no por decisión. Queda escrito en la receta, con la comprobación barata al lado, para las cuatro que usan `source.dir`. El `.pyc` se barrió del hub y del worker: las tres máquinas ya convergen en `5156cb8f`.
214 lines
16 KiB
TOML
214 lines
16 KiB
TOML
# atuq 0.1.0 — el navegador de la distro, como ARTEFACTO DERIVADO de firefox.
|
|
#
|
|
# ══ POR QUÉ DERIVADO Y NO UN FORK DE FUENTE ════════════════════════════════════════════════════
|
|
# Zen —el fork de Firefox al que esto reemplaza en nuestro catálogo— parchea el árbol de Gecko y
|
|
# recompila. Nosotros no distribuimos un binario: sellamos artefactos. Entonces atuq puede DEPENDER
|
|
# de `firefox` y poner su capa encima, y eso cambia el precio de todo:
|
|
# · una iteración de chrome cuesta SEGUNDOS, no las cuatro horas de un build de Gecko;
|
|
# · no mueve el `ArtifactHash` de `firefox` ⇒ el corpus no se invalida;
|
|
# · las CVE se heredan gratis: cuando sube `firefox`, atuq se reconstruye solo.
|
|
# El razonamiento completo, con lo que se promete y lo que NO (spoiler: no habrá «modo Tor»), está
|
|
# en `docs/26-atuq-envoltorio-gecko.md`.
|
|
#
|
|
# ══ SU FUENTE ES ESTE REPO ═════════════════════════════════════════════════════════════════════
|
|
# `source.dir` es el modo de fuente que se agregó para esto (takana-core/src/recipe.rs): el árbol
|
|
# 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.
|
|
#
|
|
# ⚠ **Y MUEVE EL HASH CUALQUIER FICHERO DEL ÁRBOL, INCLUIDOS LOS QUE `git` NO VE.** Medido el
|
|
# 2026-09-18: el hub y el worker calculaban `b3:e556024b` para esta receta y un clon limpio
|
|
# `b3:5156cb8f`, con los 52 ficheros versionados BYTE A BYTE IGUALES en las tres máquinas. La
|
|
# diferencia era **un solo fichero de más**: `tools/__pycache__/rebrand.cpython-314.pyc`, bytecode
|
|
# que dejó alguien al correr `rebrand.py` dentro del repo. Comprobado en los dos sentidos —metiendo
|
|
# y sacando ese `.pyc` el hash salta de uno a otro—.
|
|
#
|
|
# Lo que lo hace traicionero es que `__pycache__/` **está en `.gitignore`**: git no lo muestra, así
|
|
# que el árbol se ve limpio en `git status` mientras el hash ya es otro. Y el artefacto que estaba
|
|
# sellado en el store era el de `e556024b`, o sea **uno que un clon limpio no puede reproducir
|
|
# jamás**. Es la regla 3 de CLAUDE.md invertida: no un vacío que pasa por lleno, sino basura
|
|
# invisible que pasa por fuente.
|
|
#
|
|
# `ArtifactHash::of_tree` no sabe nada de `.gitignore` — y son dos cosas distintas a propósito: lo
|
|
# que no se versiona y lo que no entra al hash no tienen por qué coincidir. Pero hoy no coinciden
|
|
# **por descuido**, no por decisión. Hasta que eso se decida (ver el ADR que toque), las cuatro
|
|
# recetas con `source.dir` —`atuq`, `firmware-tigerlake`, `os-release`, `wasi-sdk`— se hashean con
|
|
# lo que haya en el directorio, y conviene comprobarlo antes de creerle a un sello:
|
|
#
|
|
# find recipes/atuq -type f | wc -l # 52 · si da más, algo se coló
|
|
#
|
|
# ══ 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.
|
|
# ══ v0.5: PROXY POR CONTENEDOR (SDD 26 §6.8) ═══════════════════════════════════════════════════
|
|
# El primer DIFERENCIADOR de la lista del §6 que se paga entero acá, sin depender del host de
|
|
# native messaging (§7) ni de nada de tawasuyu: es API de Firefox y nada más. Cada contenedor
|
|
# —Personal, Trabajo, Banco, Compras— puede salir por su propio proxy.
|
|
#
|
|
# Son tres piezas y ninguna alcanza sola:
|
|
# · `atuq.cfg` prende los contenedores (`privacy.userContext.enabled`), que vienen apagados;
|
|
# · `distribution/policies.json` los CREA con `Containers.Default` —el único mecanismo que los
|
|
# pone en un perfil nuevo— y deja la config de fábrica del proxy en `3rdparty.Extensions`;
|
|
# · `extensions/proxy/` los enruta con `proxy.onRequest`, que es lo único que ve el
|
|
# `cookieStoreId` de la petición.
|
|
#
|
|
# Las tres se comprobaron DENTRO del artefacto de firefox antes de escribirlas, no en la
|
|
# documentación de Mozilla: `Containers` y `3rdparty` están en el `policies-schema.json` de
|
|
# `browser/omni.ja`, y `cookieStoreId` en el `schemas/proxy.json` de `omni.ja`. Es la regla que dejó
|
|
# el §2.sexies: cuando la duda es «¿existe esto en NUESTRO build?», la respuesta está adentro.
|
|
#
|
|
# ⚠ LO QUE NO ES, Y VA ESCRITO EN LA PROPIA PÁGINA DE OPCIONES: separación de tráfico, NO anonimato.
|
|
# No toca la huella del navegador. El §4 del SDD ya se había comprometido a decirlo primero.
|
|
name = "atuq"
|
|
# El campo se había quedado en 0.2.0 mientras los commits ya iban por v0.4; se pone al día acá.
|
|
version = "0.5.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"
|
|
|
|
[source]
|
|
dir = "atuq"
|
|
|
|
[build]
|
|
# No se compila nada: esta receta copia y edita ficheros. `compiler` y `link` entran igual en el
|
|
# hash, así que se dejan en el default del corpus para no inventar una identidad que no significa
|
|
# nada.
|
|
target = "x86_64-linux-musl"
|
|
|
|
[build.phases]
|
|
configure = "echo 'atuq: nada que configurar — es un artefacto derivado'"
|
|
compile = "echo 'atuq: nada que compilar — el motor lo pone la dep firefox'"
|
|
install = '''
|
|
set -e
|
|
SRC=/usr/lib/firefox
|
|
DST=/out/usr/lib/atuq
|
|
|
|
# ── LAS ASERCIONES VAN PRIMERO, Y FALLAN RUIDOSAS ─────────────────────────────────────────────
|
|
# Un derivado que no encuentra su base no debe producir un artefacto flaco: produciría un
|
|
# directorio con cuatro ficheros de configuración, `Store::has` lo daría por presente y el fallo
|
|
# aparecería el día que alguien intente abrir el navegador. Un ausente falla fuerte; un vacío llega
|
|
# 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; }
|
|
[ -f "$SRC/browser/omni.ja" ] || { echo "atuq: falta $SRC/browser/omni.ja" >&2; exit 1; }
|
|
|
|
mkdir -p "$DST"
|
|
cp -a "$SRC"/. "$DST"/
|
|
# ── EL ÁRBOL COPIADO LLEGA DE SÓLO LECTURA ────────────────────────────────────────────────────
|
|
# `cp -a` preserva los modos, y los ficheros del store están sellados sin permiso de escritura. Todo
|
|
# lo que viene después —el `.ini`, el `omni.ja`, los iconos, el parche del app_id— los MODIFICA.
|
|
# En el worker esto pasaba inadvertido porque corre como root, que ignora los bits de permiso; el
|
|
# primer build fuera de root murió con `PermissionError: /out/usr/lib/atuq/application.ini`.
|
|
# Es la clase de bug que sólo aparece en la máquina de otro, así que se arregla acá y no en el
|
|
# entorno: un artefacto no debe depender de con qué uid lo construiste.
|
|
chmod -R u+w "$DST"
|
|
|
|
# ── 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/distribution/policies.json "$DST/distribution/policies.json"
|
|
|
|
# ── EL HOST NATIVO (SDD 26 §7) ────────────────────────────────────────────────────────────────
|
|
# El lanzador va ANTES de `rebrand.py` a propósito: la herramienta comprueba que el `path` de cada
|
|
# manifiesto de native messaging exista y sea ejecutable DENTRO del artefacto, y esa comprobación no
|
|
# mide nada si el fichero se copia después. Un manifiesto que apunta a un ejecutable ausente no
|
|
# falla: el navegador arranca, la extensión conecta, el puerto se cierra «sin error y sin mensaje» y
|
|
# la función no existe. Es el peor informe posible, y ya nos costó una tarde (§7.bis).
|
|
cp /src/bin/puriy-costura-host "$DST/puriy-costura-host"
|
|
chmod 0755 "$DST/puriy-costura-host"
|
|
|
|
# ── 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
|
|
# El lanzador es un script y no un symlink: ni el binario ni sus `.so` traen RPATH (comprobado con
|
|
# `readelf -d`), así que sin `LD_LIBRARY_PATH` el motor no encuentra su propio libnspr4 y muere en
|
|
# XPCOMGlueLoad antes de pintar nada. El porqué completo está en el propio fichero.
|
|
cp /src/bin/atuq /out/usr/bin/atuq
|
|
chmod 0755 /out/usr/bin/atuq
|
|
|
|
# ── 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
|
|
# Las extensiones de sistema: que el XPI EXISTA y tenga bytes. `rebrand.py` ya cruzó cada id contra
|
|
# `ExtensionSettings` y contra la ruta desde la que la política lo va a pedir; esto es el otro lado,
|
|
# el del fichero. Un `install_url` correcto apuntando a un XPI de cero bytes falla en el arranque
|
|
# del navegador, lejos de acá y con un mensaje que no nombra a esta receta.
|
|
for x in "$DST"/distribution/extensions/*.xpi; do
|
|
[ -s "$x" ] || { echo "atuq: el XPI $x quedó vacío o no existe" >&2; exit 1; }
|
|
echo " $(basename "$x") ($(wc -c < "$x") bytes)"
|
|
done
|
|
|
|
# El manifiesto de native messaging: que exista y tenga bytes. `rebrand.py` ya cruzó su `path`
|
|
# contra el árbol y sus `allowed_extensions` contra los XPI empaquetados; esto es el otro lado —que
|
|
# el fichero llegara al artefacto—. Va en /usr/lib/mozilla y NO en el appdir porque esa ruta está
|
|
# COMPILADA dentro de Gecko (`XRESysNativeManifests`) y el rebranding no la mueve.
|
|
for m in /out/usr/lib/mozilla/native-messaging-hosts/*.json; do
|
|
[ -s "$m" ] || { echo "atuq: el manifiesto nativo $m quedó vacío o no existe" >&2; exit 1; }
|
|
echo " $(basename "$m") ($(wc -c < "$m") bytes)"
|
|
done
|
|
[ -x "$DST/puriy-costura-host" ] || { echo "atuq: el lanzador del host nativo no quedó ejecutable" >&2; exit 1; }
|
|
|
|
[ -x "$DST/atuq" ] || { echo "atuq: el binario renombrado no quedó ejecutable" >&2; exit 1; }
|
|
[ -x /out/usr/bin/atuq ] || { echo "atuq: el lanzador no quedó ejecutable" >&2; exit 1; }
|
|
grep -q '^Name=atuq' "$DST/application.ini" || { echo "atuq: application.ini no quedó rebrandeado" >&2; exit 1; }
|
|
'''
|
|
|
|
[deps]
|
|
# ⚠ DEP DE EJECUCIÓN, NO DE BUILD — Y NO ES DECORACIÓN (2026-09-06).
|
|
# Este artefacto declara `NEEDED libstdc++.so.6` / `libgcc_s.so.1`, y hasta hoy NADIE en el corpus
|
|
# las proveía: sólo existían en `.dev-fs/alpine`, o sea en el LAB. El binario sellaba, REPRODUCÍA
|
|
# bit a bit, pasaba todos los guardianes… y no arrancaba fuera del lab. Se descubrió corriendo
|
|
# `atuq` bajo un sway headless, no leyendo el grafo.
|
|
# `runtime` NO entra en el ArtifactHash (igual que `license`), así que declararlo NO re-hashea:
|
|
# lo que cambia es la CLAUSURA, que es lo que se hidrata en la imagen. Lo vigila
|
|
# `scripts/vigia-sonames.py`, que entró en el latido en `02facebe` — y NO `scripts/audit-needed.sh`,
|
|
# que ese mismo commit BORRÓ. Quedan otras siete recetas apuntando al script muerto; no se tocan
|
|
# desde acá porque son de otro frente.
|
|
#
|
|
# ⚠ Y hubo un segundo agujero, en el INSTRUMENTO y no en el artefacto (2026-09-07): la clausura
|
|
# declaraba bien `gcc-libs`, pero `scripts/atuq-nested.sh` no lo hidrataba, y como la jaula monta
|
|
# `.dev-fs/alpine` como capa de abajo, cada «atuq corre» de estos días resolvió `libstdc++.so.6`
|
|
# contra el LAB. Corregido allá. Los dos ficheros ni se parecen —20.850.968 bytes el del corpus,
|
|
# 2.804.104 el del lab—, así que ante la duda de cuál se cargó, se mira el tamaño.
|
|
# Y `puriy-costura` (SDD 26 §7): el lanzador del appdir hace `exec /usr/bin/puriy-costura`, así que
|
|
# sin esta línea la imagen llevaría el manifiesto y la extensión, y el host NO estaría — la función
|
|
# se apagaría sola sin una línea de error. `runtime` no entra en el ArtifactHash: lo que cambia es la
|
|
# CLAUSURA, que es lo que se hidrata.
|
|
# Y `puriy-costura-torrent` (§6.9): el brazo LARGO de la costura. Va como dep de RUNTIME y no como
|
|
# servicio — es PEREZOSO a propósito: el binario está en la imagen y no corre nada hasta que alguien
|
|
# hace clic en un `magnet:`. Un daemon que arranca con la sesión es un daemon que nadie pidió.
|
|
runtime = ["gcc-libs", "puriy-costura", "puriy-costura-torrent"]
|
|
# `firefox` como dep de BUILD y no de runtime: su árbol se materializa en el sandbox, se copia
|
|
# 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", "python3"]
|