atuq: los .xpi sin construir el navegador, y qué se pierde con una extensión «temporal»
`scripts/atuq-xpi.py` emite los diez .xpi en work/atuq-xpi/ sin tocar firefox. NO repite el empaquetador: ejecuta `empaquetar_extensiones()` de `recipes/atuq/tools/rebrand.py`, que es el que corre en el build. Un segundo empaquetador derivaría sin avisar — el .xpi de mano dejaría de ser el de la imagen y se instalaría igual. Comprobado: los diez salen byte a byte idénticos a los de los DOS artefactos atuq sellados del store (5156cb8f… y e556024b…, 10/10 en cada uno), y dos corridas seguidas dan los mismos bytes. No se le agrega un `if __name__` a rebrand.py —que sería lo limpio— porque ese fichero está dentro del overlay cuya huella es el BuildID (`huella_overlay`): tocarlo invalida el hash de atuq y obliga a reconstruir firefox con PGO. El script ejecuta la fuente con el `main()` final quitado, y falla ruidosamente si algún día ese `main()` ya no está ahí. README: una temporal de about:debugging se desinstala al reiniciar y se lleva su storage.local, y no vuelve con el mismo id porque el UUID moz-extension se regenera. De las diez, la única con datos de usuario que perder es `proxy` (medido: storage.local sólo aparece ahí); sus cuatro contenedores de fábrica vienen de storage.managed por política y ésos no se pierden. Tabla de cómo dejarla fija en atuq / Developer Edition / release —en release NO se puede— y el aviso de que ocho de las diez se instalan MUDAS fuera de la imagen por falta del host puriy_costura. ⚠ El comportamiento de Gecko no está medido acá: en esta caja no corre ningún navegador (el firefox del corpus muere en `arc4random` de libstdc++, y en el anfitrión no hay ninguno de glibc). Queda dicho como tal en el README.
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
2 archivos · +115 −0
- recipes/atuq/README.md +51 −0
- scripts/atuq-xpi.py +64 −0
diff --git a/recipes/atuq/README.md b/recipes/atuq/README.md
index 154bb71f..bc272d64 100644
--- a/recipes/atuq/README.md
+++ b/recipes/atuq/README.md
@@ -397,3 +397,54 @@ pestañas están fijadas borra el wrapper y devuelve `null`. `--negative-control
*(El §6.8 ya no tiene pendientes: que una petición hecha en «Banco» sale por el proxy de «Banco»
está probado por `scripts/test-atuq-ruteo.py`, que abre las dos pestañas por sessionstore y mira a
qué puerto llama el navegador. Trae control negativo.)*
+
+## Instalar una extensión A MANO — y qué pasa con los datos de una «temporal»
+
+El `.xpi` de las diez sale sin construir el navegador:
+
+```sh
+python3 scripts/atuq-xpi.py # ⇒ work/atuq-xpi/<id>.xpi
+```
+
+**No es un empaquetador nuevo**: ese script ejecuta `empaquetar_extensiones()` de `tools/rebrand.py`,
+que es el mismo que corre en el build. Comprobado, y es la única comprobación que vale acá: los diez
+`.xpi` generados salen **byte a byte idénticos** a los de los dos artefactos `atuq` sellados del store
+(`5156cb8f…` y `e556024b…`, 10/10 iguales en los dos), y dos corridas seguidas dan los mismos bytes.
+Así que el que te instalás a mano ES el de la imagen, no una variante parecida.
+
+### Lo «temporal» de `about:debugging` se va, y se lleva sus datos
+
+Un complemento cargado con **«Cargar complemento temporal»** vive hasta que cerrás el navegador: al
+reiniciar queda *desinstalado*, y desinstalar un complemento borra su `storage.local` y su entrada en
+`extensions.webextensions.uuids`. O sea que **sí, contá con que lo que configuraste ahí se pierde** —
+y no vuelve instalándolo después con el mismo id, porque el `moz-extension://` UUID se regenera.
+
+⚠ Esto es comportamiento documentado de Gecko, **no medido en esta caja**: acá no corre ningún
+navegador (el firefox del corpus muere con `Error relocating /lib/libstdc++.so.6: arc4random`, y en el
+anfitrión no hay ninguno de glibc). Si hace falta certeza, se mide en la imagen, no acá.
+
+**De las diez, la única que tiene datos de usuario que perder es `proxy`** (medido: `storage.local`
+sólo aparece en `extensions/proxy/`). Las otras nueve son sin estado o lo guardan del lado nativo
+(`puriy_costura`), así que para ellas la pregunta es indiferente.
+
+Y `proxy` tiene una capa que la temporal **no** se lleva: sus cuatro contenedores de fábrica vienen de
+`storage.managed` por `3rdparty.Extensions` de la política, que es de sólo lectura y no vive en el
+perfil. Lo que se pierde es lo que pisaste vos desde la página de opciones.
+
+### Que quede fija
+
+| dónde | qué hace falta |
+|---|---|
+| **atuq** | nada más: `atuq.cfg:67` trae `lockPref("xpinstall.signatures.required", false)`, así que un `.xpi` sin firmar se instala permanente desde `about:addons` → *Instalar complemento desde archivo* |
+| **Firefox Developer Edition / Nightly / ESR** | poner `xpinstall.signatures.required = false` en `about:config` y después instalar desde archivo |
+| **Firefox release o beta** | **no se puede.** El requisito de firma está clavado en esos canales y el pref no lo levanta; la salida es usar otro canal o firmar el `.xpi` en AMO |
+
+Si en vez de a mano lo querés como lo hace la imagen —`distribution/extensions/` + entrada en
+`ExtensionSettings`— eso es la sección de arriba, y ahí el `installation_mode: normal_installed` es lo
+que además le entrega su `storage.managed`.
+
+⚠ **Ocho de las diez no sirven de nada fuera de la imagen.** `boveda`, `archivo`, `ia`, `descargas`,
+`torrent`, `sct`, `medios` y `foco` piden `nativeMessaging` contra `puriy_costura`, cuyo manifiesto
+apunta a `/usr/lib/atuq/puriy-costura-host`; sin ese binario **se instalan y se quedan mudas** — no
+fallan al instalar, que es justo la forma que no se nota. Las dos que andan solas son `proxy` e
+`inicio`.
diff --git a/scripts/atuq-xpi.py b/scripts/atuq-xpi.py
new file mode 100755
index 00000000..ab6b0d3a
--- /dev/null
+++ b/scripts/atuq-xpi.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+"""Empaqueta las extensiones de atuq en `.xpi` SIN construir el navegador.
+
+Uso: atuq-xpi.py [--out <dir>] (por defecto: work/atuq-xpi)
+
+POR QUÉ NO REPITE EL EMPAQUETADOR
+---------------------------------
+El zip determinista vive en `recipes/atuq/tools/rebrand.py` (`empaquetar_extensiones`) y es el que
+entra en el artefacto sellado. Copiarlo acá daría dos empaquetadores que derivan: el día que uno
+cambie el `date_time` o el orden de las entradas, el `.xpi` que te instalás a mano deja de ser el
+mismo que trae la imagen, y eso NO se nota — se instala igual. Así que este script IMPORTA ese
+módulo y llama a su función; lo único que aporta es el andamio de directorios que `rebrand.py`
+espera (que normalmente es el árbol de firefox a medio construir) y que acá es un temporal.
+
+⚠ `rebrand.py` no tiene `if __name__`: lee `sys.argv` y **llama a `main()`** al importarse, y
+`main()` exige el árbol de firefox entero (se muere en `application.ini`). Así que no se importa —
+se ejecuta su fuente con esa última llamada quitada, y se comprueba que estaba antes de quitarla:
+si alguien le pone un `if __name__` algún día, este script falla RUIDOSAMENTE en vez de empaquetar
+con un módulo que ya no es el que creía.
+
+Y no se le agrega el `if __name__` a `rebrand.py` a propósito: ese fichero está DENTRO del overlay
+cuya huella es el `BuildID` (`huella_overlay`), así que tocarlo invalida el hash de atuq y obliga a
+reconstruir firefox con PGO. Un `if __name__` no vale ese precio.
+"""
+import os
+import shutil
+import sys
+import tempfile
+
+RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+OVERLAY = os.path.join(RAIZ, "recipes", "atuq")
+
+destino = os.path.join(RAIZ, "work", "atuq-xpi")
+if len(sys.argv) > 2 and sys.argv[1] == "--out":
+ destino = os.path.abspath(sys.argv[2])
+elif len(sys.argv) > 1:
+ print(__doc__, file=sys.stderr)
+ raise SystemExit(2)
+
+tmp = tempfile.mkdtemp(prefix="atuq-xpi.")
+try:
+ appdir = os.path.join(tmp, "usr", "lib", "atuq")
+ os.makedirs(appdir)
+ sys.argv = ["rebrand.py", appdir, OVERLAY, tmp]
+ fuente = os.path.join(OVERLAY, "tools", "rebrand.py")
+ with open(fuente, encoding="utf-8") as fh:
+ codigo = fh.read()
+ if not codigo.rstrip().endswith("main()"):
+ print(f"atuq-xpi: {fuente} ya no termina en main() — revisá este script antes de confiar en él",
+ file=sys.stderr)
+ raise SystemExit(1)
+ codigo = codigo.rstrip()[: -len("main()")]
+ mod = {"__name__": "rebrand", "__file__": fuente}
+ exec(compile(codigo, fuente, "exec"), mod)
+
+ resultados = mod["empaquetar_extensiones"]()
+ os.makedirs(destino, exist_ok=True)
+ for nombre, ext_id, n_ficheros, tamano, _url in resultados:
+ origen = os.path.join(appdir, "distribution", "extensions", f"{ext_id}.xpi")
+ shutil.copy2(origen, os.path.join(destino, f"{ext_id}.xpi"))
+ print(f"{ext_id}.xpi {tamano:>7} B {n_ficheros} ficheros ({nombre})")
+ print(f"\n{len(resultados)} extensiones en {destino}")
+finally:
+ shutil.rmtree(tmp, ignore_errors=True)