atuq: dos cosas que el README daba por ciertas eran falsas, y las dos se midieron

El README de la receta afirmaba (a) que las extensiones las instala la POLÍTICA porque el
sideloading desde `distribution/extensions/` «ya no funciona», y (b) que el chrome de verdad
—split view— exige entrar a `omni.ja` y escribirlo nosotros. Las dos son falsas, y ninguna
observación del artefacto las distinguía: hubo que romper un mecanismo por vez.

scripts/test-atuq-instalacion.py — tres escenarios:
  as-is        nada roto                             ⇒ las dos extensiones puestas
  no-policy    `policies.json` sin ExtensionSettings ⇒ SIGUEN puestas
  policy-only  los XPI fuera de la carpeta           ⇒ NINGUNA, y la home vuelve a about:home
⇒ instala el ESCANEO de la carpeta; `install_url` con `file://` no instala nada. El §7.bis del
SDD 26 tenía razón. El control es por construcción: la sonda vive en la carpeta y ninguna política
la nombra, así que una corrida muda se declara ROTA en vez de leerse como «no instaló». Y la
segunda señal que probé NO discrimina, queda dicho: el `location` de `extensions.json` sale
`app-profile` también cuando instala la carpeta.

scripts/test-atuq-chrome.py — el chrome se programa desde `atuq.cfg`, sin abrir el zip: observando
`browser-delayed-startup-finished` se toca el `gBrowser` de cada ventana. Y la vista dividida ya la
trae el motor (fx 154) PRENDIDA de fábrica, ejercitada de verdad —`addTabSplitView` ⇒
`activeSplitView` + 2 navegadores—, no «el fichero está». Dos pendientes que eran de upstream.
Control negativo del propio motor: con las pestañas fijadas devuelve null (WRAPPER null, ACTIVA no).

Corolario para lo que venga: cada función del chrome que NO entre a `omni.ja` es una que no pelea
con el orden del `jarlog` del PGO — que es justo lo que tiene al jarlog aparcado.

Queda escrito lo que NO está: ninguno de los `test-atuq-*` corre en el latido (sólo lo hace
`vigia-sonames.py`), y meterlos cuesta ~3 min por ciclo más el rootfs hidratado en el hub.

Medido sobre atuq b3:fab2fbfb → b3:8f6d09c2 (el README entra en el hash: documentar la medición
re-hashea el artefacto medido, y los guardianes se niegan a medir uno que no sea el vigente).
This commit is contained in:
Sergio
2026-09-09 23:15:09 +00:00
parent 894bfaf0f5
commit 97c35d67b3
4 changed files with 658 additions and 14 deletions
+232
View File
@@ -0,0 +1,232 @@
#!/usr/bin/env python3
"""¿Corre JS NUESTRO en el chrome de `atuq`, y funciona la vista dividida? (SDD 26 §4)
python3 scripts/test-atuq-chrome.py # exige las dos cosas
python3 scripts/test-atuq-chrome.py --negative-control # con las pestañas FIJADAS: no debe partir
── LAS DOS PREGUNTAS, Y POR QUÉ VAN JUNTAS ───────────────────────────────────────────────────
El README de atuq decía que «el chrome de verdad» —split view, dientes propios— exige entrar a
`omni.ja` y poner contenido nuestro adentro. Las dos mitades de esa frase resultaron falsas, y esta
sonda mide cada una:
1. **El gancho.** `atuq.cfg` ya corre con privilegios (de ahí sale `nsIStyleSheetService`), así que
desde ahí se puede observar `browser-delayed-startup-finished` y tocar el `gBrowser` de CADA
ventana. O sea: **el chrome se programa sin abrir el zip**, que además es lo que hay que hacer,
porque re-empacar `omni.ja` pelea con el orden del `jarlog` del PGO (por eso `jarlog` está
aparcado).
2. **La vista dividida.** Firefox 154 la trae HECHA y PRENDIDA de fábrica:
`browser/omni.ja` lleva `tabbrowser/tabsplitview.js` (492 líneas, el custom element),
`opentabs-splitview.mjs` y `split-view-footer.js`, y `defaults/preferences/firefox.js` dice
`pref("browser.tabs.splitView.enabled", true)`. Eso no es «lo que falta escribir»: es una
función que ya tenemos y que nadie había ejercitado.
Lo que la sonda hace en la ventana real: abre dos pestañas, llama a `gBrowser.addTabSplitView`, y
después pregunta al motor por `gBrowser.activeSplitView` y `gBrowser.splitViewBrowsers`. Son hechos
del DOM del chrome, no píxeles — corre headless.
── EL CONTROL NEGATIVO SALE DEL PROPIO MOTOR ─────────────────────────────────────────────────
`addTabSplitView` tiene un `bail out` documentado: si TODAS las pestañas están fijadas, borra el
wrapper y devuelve `null` (no se pueden agrupar). `--negative-control` fija las dos pestañas y exige
justamente eso. Sin él, una sonda que dijera «partió» pase lo que pase se vería idéntica a una que
funciona.
REQUISITOS: el rootfs de `scripts/atuq-nested.sh` (que lo hidrata) y `atuq` sellado.
"""
import os
import shutil
import subprocess
import sys
import tempfile
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs")
ALPINE = os.path.join(ROOT, ".dev-fs/alpine")
HAMMER = os.path.join(ROOT, "target/release/takana")
STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store"))
APPDIR = "usr/lib/atuq"
# El bloque se APENDA al `atuq.cfg` del artefacto (no lo reemplaza): así se mide el fichero real de
# atuq con la sonda encima, y no una copia que podría haber divergido. La capa de overlay pisa el
# fichero entero, por eso hay que reescribirlo completo con el original adentro.
SONDA = """
// ── SONDA DE PRUEBA — NO VIAJA EN EL ARTEFACTO (scripts/test-atuq-chrome.py) ───────────────────
try {
const S = (typeof Services !== "undefined")
? Services
: ChromeUtils.importESModule("resource://gre/modules/Services.sys.mjs").Services;
const lineas = [];
const di = function (s) {
lineas.push(s);
try { dump("CHROME-TEST " + s + "\\n"); } catch (e) {}
// El fichero es la vía FIABLE: `dump()` desde el proceso padre depende de prefs y de cómo se
// compiló. Se usan las dos y el guión lee la que haya.
try { IOUtils.writeUTF8("/salida/chrome.txt", lineas.join("\\n") + "\\n"); } catch (e) {}
};
di("VIVO el JS de atuq.cfg corre");
S.obs.addObserver(function observador(win) {
S.obs.removeObserver(observador, "browser-delayed-startup-finished");
try {
di("VENTANA " + (win.gBrowser ? "con gBrowser" : "SIN gBrowser"));
di("PREF splitView.enabled=" +
S.prefs.getBoolPref("browser.tabs.splitView.enabled", false));
const principal = S.scriptSecurityManager.getSystemPrincipal();
const t1 = win.gBrowser.addTab("about:blank", { triggeringPrincipal: principal });
const t2 = win.gBrowser.addTab("about:blank", { triggeringPrincipal: principal });
if (FIJADAS) { win.gBrowser.pinTab(t1); win.gBrowser.pinTab(t2); }
const sv = win.gBrowser.addTabSplitView([t1, t2]);
di("WRAPPER " + (sv ? ("tabs=" + sv.tabs.length) : "null"));
if (sv && sv.tabs.length) { win.gBrowser.selectedTab = sv.tabs[0]; }
win.setTimeout(function () {
di("ACTIVA " + (win.gBrowser.activeSplitView ? "si" : "no"));
di("BROWSERS " + win.gBrowser.splitViewBrowsers.length);
di("FIN");
}, 2000);
} catch (e) {
di("EXCEPCIÓN " + e);
}
}, "browser-delayed-startup-finished");
} catch (e) {
try { dump("CHROME-TEST EXCEPCIÓN-EXTERIOR " + e + "\\n"); } catch (e2) {}
}
"""
def fatal(msg):
print(f"{msg}", file=sys.stderr)
raise SystemExit(1)
def artefacto():
"""El artefacto VIGENTE de atuq, por `takana hash` y nunca por glob.
⚠ ESCOTILLA `ATUQ_DIR`, que grita: el corpus es compartido y otro frente puede re-hashear
`firefox` dejando el atuq vigente sin sellar durante las horas del build. Un resultado obtenido
con la escotilla NO se cita como «el atuq de hoy pasa».
"""
forzado = os.environ.get("ATUQ_DIR")
if forzado:
if not os.path.isdir(forzado):
fatal(f"ATUQ_DIR={forzado} no existe")
print(f"⚠ ATUQ_DIR: se mide {os.path.basename(forzado)}, que NO es necesariamente el vigente")
return forzado
r = subprocess.run([HAMMER, "--store", STORE, "hash", "recipes/atuq.toml"],
cwd=ROOT, capture_output=True, text=True)
if r.returncode != 0:
fatal(f"no pude hashear atuq: {r.stderr.strip()}")
h = r.stdout.strip().splitlines()[-1].removeprefix("b3:")
d = os.path.join(STORE, f"{h}-atuq")
if not os.path.isdir(d):
fatal(f"atuq b3:{h[:8]} no está sellado — construilo antes")
return d
def main():
negativo = "--negative-control" in sys.argv[1:]
if negativo:
print("== MODO CONTROL NEGATIVO: las dos pestañas van FIJADAS; el motor tiene que negarse")
if not os.path.isdir(RFS):
fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`, que lo hidrata")
atuq = artefacto()
print(f"artefacto: {os.path.basename(atuq)}")
tmp = tempfile.mkdtemp(prefix="atuq-chrome-")
salida = os.path.join(tmp, "salida")
capa_app = os.path.join(tmp, "capa", APPDIR)
os.makedirs(os.path.join(salida, "perfil"))
os.makedirs(capa_app)
try:
original = open(os.path.join(atuq, APPDIR, "atuq.cfg"), encoding="utf8").read()
with open(os.path.join(capa_app, "atuq.cfg"), "w", encoding="utf8") as fh:
fh.write(original)
fh.write(SONDA.replace("FIJADAS", "true" if negativo else "false"))
with open(os.path.join(salida, "perfil", "user.js"), "w") as fh:
fh.write('user_pref("browser.dom.window.dump.enabled", true);\n')
fh.write('user_pref("browser.shell.checkDefaultBrowser", false);\n')
guion = """
mkdir -p /root
[ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
timeout 60 /usr/bin/atuq --profile /salida/perfil --no-remote --headless about:blank \
> /salida/consola.log 2>&1
exit 0
"""
subprocess.run([
"bwrap",
"--overlay-src", ALPINE, "--overlay-src", RFS, "--overlay-src", atuq,
"--overlay-src", os.path.join(tmp, "capa"), "--tmp-overlay", "/",
"--dev-bind", "/dev", "/dev", "--proc", "/proc", "--bind", "/sys", "/sys",
"--tmpfs", "/tmp", "--tmpfs", "/run", "--unshare-pid",
"--bind", salida, "/salida",
"--uid", "0", "--gid", "0",
"--setenv", "PATH", "/usr/bin:/bin:/usr/sbin:/sbin",
"--setenv", "XDG_RUNTIME_DIR", "/run", "--setenv", "HOME", "/root",
"--setenv", "XDG_DATA_DIRS", "/usr/share", "--setenv", "MOZ_HEADLESS", "1",
"--setenv", "MOZ_DISABLE_CONTENT_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_GMP_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_RDD_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_UTILITY_SANDBOX", "1",
"sh", "-c", guion,
], check=False)
log = open(os.path.join(salida, "consola.log"), errors="replace").read()
lineas = [l[len("CHROME-TEST "):] for l in log.splitlines() if l.startswith("CHROME-TEST ")]
fichero = os.path.join(salida, "chrome.txt")
if os.path.exists(fichero):
del_fichero = open(fichero, errors="replace").read().splitlines()
# El fichero es la fuente autorizada si dice MÁS que el stdout.
if len(del_fichero) > len(lineas):
lineas = del_fichero
for l in lineas:
print(" " + l)
def valor(clave):
for l in lineas:
if l.startswith(clave + " "):
return l.split(" ", 1)[1]
return None
if not lineas:
destino = os.path.join(ROOT, "work", "atuq-chrome.log")
os.makedirs(os.path.dirname(destino), exist_ok=True)
open(destino, "w").write(log)
fatal("la sonda no dijo NADA: el JS de `atuq.cfg` no corrió, o no llegó a la ventana."
f" Log en {destino}")
if valor("VIVO") is None:
fatal("no salió la línea VIVO — el bloque de la sonda no se ejecutó")
if "FIN" not in lineas and valor("EXCEPCIÓN"):
fatal(f"el JS del chrome se rompió: {valor('EXCEPCIÓN')}")
wrapper, activa, browsers = valor("WRAPPER"), valor("ACTIVA"), valor("BROWSERS")
if negativo:
if wrapper != "null":
fatal(f"con las dos pestañas FIJADAS el motor partió igual (WRAPPER={wrapper})"
" — esta prueba no distingue nada")
if activa != "no":
fatal(f"control negativo: ACTIVA={activa}, se esperaba «no»")
print("✓ control negativo: con las pestañas fijadas NO hay vista dividida"
" (la sonda sí lo vería)")
return 0
if valor("VENTANA") != "con gBrowser":
fatal("el observador corrió pero la ventana no traía `gBrowser`")
if valor("PREF") != "splitView.enabled=true":
fatal(f"la vista dividida viene APAGADA en este build ({valor('PREF')})"
" — habría que prenderla en atuq.cfg")
if wrapper != "tabs=2":
fatal(f"`addTabSplitView` no armó la vista de dos pestañas (WRAPPER={wrapper})")
if activa != "si":
fatal(f"la vista se creó pero el motor no la reporta ACTIVA (ACTIVA={activa})")
if browsers != "2":
fatal(f"el motor no reporta dos navegadores en la vista (BROWSERS={browsers})")
print("✓ el JS de `atuq.cfg` llega al `gBrowser` de la ventana:"
" el chrome se programa SIN abrir `omni.ja`")
print("✓ la vista dividida del motor funciona en nuestro build:"
" dos pestañas, vista ACTIVA y dos navegadores")
return 0
finally:
shutil.rmtree(tmp, ignore_errors=True)
sys.exit(main())
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""¿QUIÉN instala de verdad las extensiones de `atuq`: la política o la carpeta? (SDD 26 §7.bis)
python3 scripts/test-atuq-instalacion.py # los tres escenarios y el veredicto
python3 scripts/test-atuq-instalacion.py --scenario as-is # uno solo, para diagnosticar
── POR QUÉ EXISTE ESTA PRUEBA ────────────────────────────────────────────────────────────────
Dos documentos nuestros se contradecían sobre el mecanismo del que dependen TODAS las extensiones
de atuq —hoy `inicio` y `proxy`, mañana la de `sct`—:
· `recipes/atuq/README.md` decía que el *sideloading* desde `distribution/extensions/` «ya no
funciona» y que lo que instala es `ExtensionSettings.install_url` de la política;
· el §7.bis del SDD 26 midió lo contrario: que `install_url` con `file://` no instala nada y que
quien instala es el escaneo de la carpeta.
Los dos mecanismos APUNTAN A LOS MISMOS FICHEROS, así que ninguna observación del artefacto los
distingue: hay que romper uno por vez y mirar qué pasa. Eso es lo que hace este guión.
── LOS TRES ESCENARIOS ───────────────────────────────────────────────────────────────────────
as-is el artefacto tal cual ⇒ las extensiones tienen que estar
no-policy `policies.json` SIN `ExtensionSettings` ⇒ si siguen: instala LA CARPETA
policy-only los XPI mudados fuera de la carpeta y la
política apuntando a la ruta nueva ⇒ si no están: la política NO instala
En los tres se mira lo mismo, y por dos vías independientes:
el MOTOR una sonda con permiso `browserSettings` pregunta el `homepageOverride`; si el
override de `inicio` agarró, es `moz-extension://…/inicio.html`
el PERFIL `extensions.json` dice si el complemento ESTÁ y si está `active`
⚠ El `location` de `extensions.json` NO sirve para distinguir el mecanismo, y esto es medido: los
dos XPI de atuq salen `app-profile` **también cuando los instala el escaneo de la carpeta** y no hay
ninguna política que los nombre. Se lo imprime igual porque es barato, pero la única señal que
discrimina es la de arriba: romper un mecanismo por vez y ver si la extensión sigue.
La sonda es además el CONTROL de que la corrida sirvió: vive en `distribution/extensions/` y NO está
declarada en ninguna política, así que si habla, el escaneo de la carpeta está vivo en esa corrida.
Una corrida muda no se interpreta como «no instaló»: se declara rota.
REQUISITOS: el rootfs de `scripts/atuq-nested.sh` (que lo hidrata) y `atuq` sellado.
"""
import json
import os
import shutil
import subprocess
import sys
import tempfile
import zipfile
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs")
ALPINE = os.path.join(ROOT, ".dev-fs/alpine")
HAMMER = os.path.join(ROOT, "target/release/takana")
STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store"))
APPDIR = "/usr/lib/atuq"
EXTDIR = f"{APPDIR}/distribution/extensions"
ALTDIR = f"{APPDIR}/xpi-politica"
IDS = ("inicio@atuq.tawasuyu", "proxy@atuq.tawasuyu")
SONDA = "sonda-instalacion@atuq.tawasuyu"
MANIFIESTO = {
"manifest_version": 2,
"name": "sonda-instalacion",
"version": "1.0",
"browser_specific_settings": {"gecko": {"id": SONDA}},
"permissions": ["browserSettings"],
"background": {"scripts": ["fondo.js"]},
}
FONDO = """
function di(s) { try { dump("INSTALACION-TEST " + s + "\\n"); } catch (e) {} }
(async () => {
try {
const h = await browser.browserSettings.homepageOverride.get({});
di("HOMEPAGE " + h.value);
} catch (e) {
di("EXCEPCIÓN " + e.message);
}
})();
"""
ESCENARIOS = ("as-is", "no-policy", "policy-only")
def fatal(msg):
print(f"{msg}", file=sys.stderr)
raise SystemExit(1)
def artefacto():
"""El artefacto VIGENTE de atuq, por `takana hash` y nunca por glob.
⚠ ESCOTILLA `ATUQ_DIR` con su aviso puesto: el corpus es compartido y otro frente puede
re-hashear `firefox` dejando el atuq vigente sin sellar durante las horas del build. Con la
escotilla se puede medir contra uno viejo A SABIENDAS, y por eso grita.
"""
forzado = os.environ.get("ATUQ_DIR")
if forzado:
if not os.path.isdir(forzado):
fatal(f"ATUQ_DIR={forzado} no existe")
print(f"⚠ ATUQ_DIR: se mide {os.path.basename(forzado)}, que NO es necesariamente el vigente")
return forzado
r = subprocess.run([HAMMER, "--store", STORE, "hash", "recipes/atuq.toml"],
cwd=ROOT, capture_output=True, text=True)
if r.returncode != 0:
fatal(f"no pude hashear atuq: {r.stderr.strip()}")
h = r.stdout.strip().splitlines()[-1].removeprefix("b3:")
d = os.path.join(STORE, f"{h}-atuq")
if not os.path.isdir(d):
fatal(f"atuq b3:{h[:8]} no está sellado — construilo antes")
return d
def politica_sin_extensionsettings(base):
p = json.load(open(base))
p["policies"].pop("ExtensionSettings", None)
return p
def politica_apuntando_a(base, directorio):
p = json.load(open(base))
for ident, cfg in p["policies"]["ExtensionSettings"].items():
cfg["install_url"] = f"file://{directorio}/{ident}.xpi"
return p
def correr(escenario, atuq):
"""Una corrida de atuq headless con perfil nuevo. Devuelve (homepage, locations, log)."""
tmp = tempfile.mkdtemp(prefix=f"atuq-inst-{escenario}-")
salida = os.path.join(tmp, "salida")
capa_ext = os.path.join(tmp, "capa", APPDIR.lstrip("/"), "distribution", "extensions")
perfil = os.path.join(salida, "perfil")
os.makedirs(perfil)
os.makedirs(capa_ext)
try:
with zipfile.ZipFile(os.path.join(capa_ext, f"{SONDA}.xpi"), "w") as z:
z.writestr("manifest.json", json.dumps(MANIFIESTO, indent=1))
z.writestr("fondo.js", FONDO)
with open(os.path.join(perfil, "user.js"), "w") as fh:
fh.write('user_pref("browser.dom.window.dump.enabled", true);\n')
fh.write('user_pref("browser.shell.checkDefaultBrowser", false);\n')
base = os.path.join(atuq, APPDIR.lstrip("/"), "distribution", "policies.json")
preparar = "true"
if escenario == "no-policy":
nueva = politica_sin_extensionsettings(base)
elif escenario == "policy-only":
nueva = politica_apuntando_a(base, ALTDIR)
preparar = (f"mkdir -p {ALTDIR} && "
+ " && ".join(f"mv {EXTDIR}/{i}.xpi {ALTDIR}/{i}.xpi" for i in IDS))
else:
nueva = None
if nueva is not None:
capa_pol = os.path.join(tmp, "capa", APPDIR.lstrip("/"), "distribution")
with open(os.path.join(capa_pol, "policies.json"), "w") as fh:
json.dump(nueva, fh, indent=2)
guion = f"""
mkdir -p /root
[ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
{preparar}
timeout 60 /usr/bin/atuq --profile /salida/perfil --no-remote --headless about:blank \
> /salida/consola.log 2>&1
cp /salida/perfil/extensions.json /salida/extensions.json 2>/dev/null || true
exit 0
"""
subprocess.run([
"bwrap",
"--overlay-src", ALPINE, "--overlay-src", RFS, "--overlay-src", atuq,
"--overlay-src", os.path.join(tmp, "capa"), "--tmp-overlay", "/",
"--dev-bind", "/dev", "/dev", "--proc", "/proc", "--bind", "/sys", "/sys",
"--tmpfs", "/tmp", "--tmpfs", "/run", "--unshare-pid",
"--bind", salida, "/salida",
"--uid", "0", "--gid", "0",
"--setenv", "PATH", "/usr/bin:/bin:/usr/sbin:/sbin",
"--setenv", "XDG_RUNTIME_DIR", "/run", "--setenv", "HOME", "/root",
"--setenv", "XDG_DATA_DIRS", "/usr/share", "--setenv", "MOZ_HEADLESS", "1",
"--setenv", "MOZ_DISABLE_CONTENT_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_GMP_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_RDD_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_UTILITY_SANDBOX", "1",
"sh", "-c", guion,
], check=False)
log = open(os.path.join(salida, "consola.log"), errors="replace").read()
homepage = None
for l in log.splitlines():
if l.startswith("INSTALACION-TEST HOMEPAGE "):
homepage = l.split(" ", 2)[2]
locations = {}
ej = os.path.join(salida, "extensions.json")
if os.path.exists(ej):
try:
for a in json.load(open(ej)).get("addons", []):
if a.get("id") in IDS or a.get("id") == SONDA:
locations[a["id"]] = (a.get("location"), bool(a.get("active")))
except (ValueError, KeyError) as e:
print(f" ⚠ no pude leer extensions.json: {e}")
return homepage, locations, log
finally:
shutil.rmtree(tmp, ignore_errors=True)
def main():
argv = sys.argv[1:]
pedido = None
if "--scenario" in argv:
pedido = argv[argv.index("--scenario") + 1]
if pedido not in ESCENARIOS:
fatal(f"escenario desconocido: {pedido} (hay {', '.join(ESCENARIOS)})")
if not os.path.isdir(RFS):
fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`, que lo hidrata")
atuq = artefacto()
print(f"artefacto: {os.path.basename(atuq)}")
res = {}
for e in (ESCENARIOS if pedido is None else (pedido,)):
print(f"\n== escenario {e}")
homepage, locations, log = correr(e, atuq)
nuestra = bool(homepage and "inicio.html" in homepage)
print(f" MOTOR homepage = {homepage}"
f"{'la de atuq' if nuestra else 'la de Firefox'}")
if not homepage:
destino = os.path.join(ROOT, "work", f"atuq-instalacion-{e}.log")
os.makedirs(os.path.dirname(destino), exist_ok=True)
open(destino, "w").write(log)
fatal(f"la sonda no dijo NADA en «{e}» — corrida rota, no resultado. Log en {destino}")
for ident, (loc, activo) in sorted(locations.items()):
print(f" PERFIL {ident:34s} location={loc} activo={activo}")
for ident in IDS:
if ident not in locations:
print(f" PERFIL {ident:34s} NO ESTÁ en extensions.json")
res[e] = (nuestra, locations)
if pedido is not None:
return 0
print()
fallos = []
if not res["as-is"][0]:
fallos.append("tal cual, la página de inicio NO es la de atuq — atuq está roto, "
"no es un problema del mecanismo")
if not res["no-policy"][0]:
fallos.append("sin `ExtensionSettings` las extensiones NO se instalan ⇒ el escaneo de "
"`distribution/extensions/` YA NO alcanza: hay que revisar el §7.bis y el "
"README, porque todas las extensiones de atuq cuelgan de ahí")
if res["policy-only"][0]:
fallos.append("con los XPI FUERA de la carpeta y sólo la política apuntándolos, las "
"extensiones SÍ se instalaron ⇒ `install_url` con `file://` ahora funciona "
"y el §7.bis quedó viejo")
if fallos:
for f in fallos:
print(f"{f}", file=sys.stderr)
return 1
print("✓ quien instala es el ESCANEO de `distribution/extensions/`: sin la política siguen "
"puestas, y con la política sola no aparecen.")
print(" La política las FIJA y las CONFIGURA; instalarlas, no. (SDD 26 §7.bis)")
return 0
sys.exit(main())