kde/Plasma: hito H-QML PASADO — hidratación FHS del cierre runtime + reproducer QML
- repo-selfcontain.py: work/repo-kde auto-contenido (alias qt-corto→qt6-* + cierre base desde dist/repo), re-firmado 884 pkgs, repo verify trusted. - hydrate-fhs.sh: hidrata el cierre runtime instalando cada pkg del cierre en el mismo prefix (install --prefix sólo baja el pkg pedido); 32 pkgs → work/kde-rootfs, 455 .so (libQt6Core/Gui/Qml/Quick + QPA offscreen + mesa/xcb/wayland/openssl). - h-qml-run.sh + h-qml-test.cpp: QQmlApplicationEngine carga QtQuick Window+Text, compila zig-cc/musl contra el FHS, corre offscreen rc=0 (engine instancia el árbol QML). - ADR 0011: sección resultado H-QML + deudas (target_bin espurio en libs, cache-miss por re-resolución de deps → qtbase más completo con SSL, fuentes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Semantic delta
No definition changed: this change is text, documentation or data.
4 files · +206 −1
diff --git a/docs/adr/0011-escritorio-kde-plasma.md b/docs/adr/0011-escritorio-kde-plasma.md
index 82be06ea..992365fc 100644
--- a/docs/adr/0011-escritorio-kde-plasma.md
+++ b/docs/adr/0011-escritorio-kde-plasma.md
@@ -1,6 +1,6 @@
# 0011 — Etapa Escritorio: campaña KDE Plasma 6
-Estado: aceptado · **gate H-Qt PASADO (2026-07-11)**
+Estado: aceptado · **gate H-Qt PASADO (2026-07-11)** · **hito H-QML PASADO (2026-07-14)**
Fecha: 2026-07-11
Relacionado: [[etapa-g-importar-nix]], [[etapa-g-gui-chain-boundary]], [[granja-efimera-hcloud]],
0003-zig-cc-builder, 0008-bootstrap-stages, 0010-arranque-grafo-mirada
@@ -203,3 +203,48 @@ pero KDE usa `qtwayland` extendido). En paralelo, **saldar la deuda de Capa 0**:
shared-PIC y glib `.a` completo, para reactivar SSL/SQL/glib en un qtbase "real" (hoy es el qtbase del
gate, reducido; se rebuildea con features plenas antes de promover a canónico/firmado). Arrancar las
`farm-up` desde el snapshot `hammer-kde-qtbase-2026-07-11`.
+
+## Resultado H-QML (2026-07-14) — HITO PASADO ✅
+
+`qtdeclarative` (QML/QtQuick) cosechado en el repo firmado del escritorio (`work/repo-kde`, 161 pkgs
+KDE). El test `scripts/kde/h-qml-test.cpp` (`QQmlApplicationEngine` que carga QML inline `Window` +
+`Text` vía `import QtQuick`) **compila** con zig-cc/musl contra el FHS hidratado y **corre a rc=0** con
+`QT_QPA_PLATFORM=offscreen`: el engine instancia el árbol de objetos QtQuick y el event loop sale
+limpio. **El motor QML — el corazón de Plasma — carga e instancia una escena bajo el régimen
+dinámico/musl/zig-cc.** Reproducible con `scripts/kde/h-qml-run.sh` en el worker.
+
+### La HIDRATACIÓN FHS (salda la deuda de la lección 3 del gate H-Qt)
+El gate dejó pendiente "una hidratación FHS que capture el cierre runtime". Ahora existe y está
+probada, en tres piezas nuevas:
+1. **`scripts/kde/repo-selfcontain.py`** — vuelve `work/repo-kde` AUTO-CONTENIDO para `install`:
+ `install` lee los `depends` del recipe embebido en cada `.swm` y resuelve cada nombre como CLAVE
+ del índice. El repo cosechado no cerraba por dos causas: (a) deps a nombres de receta-archivo
+ qt-cortos (`qtbase`) cuando el paquete es `qt6-qtbase` (fuga de de-Alpinización, 79 recetas), y
+ (b) faltaban libs base del cierre. El script trae de `dist/repo` (repo base firmado, cierre
+ completo) todo paquete ausente (colisión → gana la versión KDE) y agrega entradas ALIAS
+ qt-corto→qt6-* (mismo `.swm`, otra clave). Índice resultante 884 entradas, re-firmado con
+ `hammer repo sign` → `repo verify` trusted.
+2. **`scripts/kde/hydrate-fhs.sh`** — hidrata el cierre runtime instalando **cada** paquete del
+ cierre en el MISMO prefix (clave: `install <pkg> --prefix P` sólo baja los ficheros de `<pkg>`, NO
+ de su cierre, igual que `product-userland-from-repo.sh`). Con el store del worker caliente los
+ 32 pkgs del cierre de `qt6-qtdeclarative` cache-hitean; produce `work/kde-rootfs` con 455 `.so`
+ (libQt6Core/Gui/Qml/Quick + el QPA `libqoffscreen.so` + mesa/EGL/xcb/wayland/openssl/...).
+3. **`scripts/kde/h-qml-run.sh`** — compila+corre el reproducer H-QML contra ese FHS.
+
+### Deudas/lecciones nuevas (para KDE real)
+- **`target_bin` espurio en recetas de LIBS**: las recetas KDE declaran `target_bin=/usr/bin/<pkg>`
+ que no existe (son libs). `install` hidrata los ficheros y LUEGO falla la aserción con exit 1;
+ `hydrate-fhs.sh` lo tolera (cuenta éxito si dejó ficheros). **Fix pendiente**: quitar/corregir
+ `target_bin` en las recetas de librería (requiere re-pack del `.swm`).
+- **Cache-miss al re-resolver deps**: agregar libpng/openssl/etc. al cierre cambió el input-hash de
+ qtbase → `install` rebuildeó un qtbase MÁS COMPLETO (con módulo Network/SSL, antes apagado en el
+ gate). Es avance hacia el "qtbase real", pero rompe la reutilización del sellado `7c7c747d`. Para
+ reproducibilidad estricta hay que fijar el cierre de build-deps exacto del sellado.
+- **Fuentes**: Qt ya no trae fuentes (`QFontDatabase: Cannot find font directory`). Cosmético para
+ H-QML (no bloquea la instanciación), pero desplegar dejavu/fontconfig antes de apps reales.
+
+### Próximo paso concreto
+Con H-QML cerrado, **Capa 2 (KDE Frameworks 6)**: `extra-cmake-modules` ya está; atacar tier 1
+(`kcoreaddons`, `kconfig`, `ki18n`, `kirigami`…) hacia el hito **H-KF6** (una app KDE de ejemplo
+corre). En paralelo saldar la deuda Capa 0 (sqlite/openssl/glib shared-PIC) para promover un qtbase
+"real" a canónico/firmado. Mantener la hidratación FHS como el patrón de validación por capa.
diff --git a/scripts/kde/h-qml-run.sh b/scripts/kde/h-qml-run.sh
new file mode 100644
index 00000000..f7b09010
--- /dev/null
+++ b/scripts/kde/h-qml-run.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# h-qml-run.sh — VALIDA el hito H-QML (ADR 0011, Capa 1) contra el FHS hidratado desde el repo firmado.
+# Compila scripts/kde/h-qml-test.cpp (QQmlApplicationEngine + QtQuick Window/Text) con zig-cc/musl
+# contra work/kde-rootfs y lo corre con QT_QPA_PLATFORM=offscreen vía el loader musl del .dev-fs.
+# Pensado para correr EN EL WORKER (host glibc + rootfs musl). Prerequisito: hydrate-fhs.sh ya hidrató
+# el cierre runtime de qt6-qtdeclarative en work/kde-rootfs.
+#
+# Éxito = "H-QML: QML instanciado" + "event loop terminó rc=0".
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
+R="${RFS:-$ROOT/work/kde-rootfs}"
+ZIG="${ZIG:-$ROOT/.dev-fs/tools/zig-x86_64-linux-0.13.0/zig}"
+LOADER="${LOADER:-$ROOT/.dev-fs/alpine/lib/ld-musl-x86_64.so.1}"
+A="$ROOT/.dev-fs/alpine"
+BIN="${BIN:-/tmp/h-qml}"
+I="$R/usr/include/qt6"
+
+echo "==> compilando h-qml-test.cpp (zig-cc/musl) contra $R"
+"$ZIG" c++ -std=c++17 -target x86_64-linux-musl -mcpu=baseline -fPIC \
+ scripts/kde/h-qml-test.cpp -o "$BIN" \
+ -I"$I" -I"$I/QtCore" -I"$I/QtGui" -I"$I/QtQml" \
+ -L"$R/usr/lib" -lQt6Qml -lQt6Gui -lQt6Core
+
+echo "==> corriendo H-QML (offscreen)"
+LIBS="$R/usr/lib:$R/usr/lib/qt6:$A/usr/lib:$A/lib"
+QT_QPA_PLATFORM=offscreen QT_QUICK_BACKEND=software \
+QT_PLUGIN_PATH="$R/usr/lib/qt6/plugins" \
+QML_IMPORT_PATH="$R/usr/lib/qt6/qml" QML2_IMPORT_PATH="$R/usr/lib/qt6/qml" \
+ "$LOADER" --library-path "$LIBS" "$BIN"
diff --git a/scripts/kde/hydrate-fhs.sh b/scripts/kde/hydrate-fhs.sh
new file mode 100644
index 00000000..40caf449
--- /dev/null
+++ b/scripts/kde/hydrate-fhs.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+# hydrate-fhs.sh — hidrata un FHS runtime KDE desde el repo firmado, instalando el CIERRE completo.
+#
+# `hammer install <pkg> --prefix P` sólo hidrata los archivos de <pkg>, NO de su cierre. Para un
+# rootfs runtime usable (que capture todas las `.so`), hay que instalar cada paquete del cierre en el
+# MISMO prefix (igual que product-userland-from-repo.sh). El store del worker está caliente ⇒ cache-hit.
+#
+# Las recetas de LIBS KDE declaran `target_bin=/usr/bin/<pkg>` que no existe (son libs, no binarios):
+# `install` hidrata los ficheros y LUEGO falla la aserción con exit 1. Lo toleramos: contamos éxito si
+# el paquete dejó ficheros en el prefix.
+#
+# Uso: TARGET=qt6-qtdeclarative ./hydrate-fhs.sh (default)
+set -uo pipefail
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
+REPO="${REPO:-work/repo-kde}"; TRUST="${TRUST:-dist/keys}"; STORE="${STORE:-./store}"
+RFS="${RFS:-work/kde-rootfs}"; TARGET="${TARGET:-qt6-qtdeclarative}"
+HAMMER="${HAMMER:-./target/release/hammer}"
+DB="$RFS/var/lib/hammer/installed.json"
+
+# cierre transitivo del TARGET, leído del índice (deps + aliases ya resuelven). Excluimos herramientas
+# puramente de build (no aportan runtime) para acelerar; si algo falta lo delata ldd.
+CLOSURE=$(python3 - "$REPO/index.json" "$TARGET" <<'PY'
+import json,sys
+idx=json.load(open(sys.argv[1])); tgt=sys.argv[2]
+by={p['name']:p for p in idx['packages']}
+BUILD_ONLY={'cmake','samurai','meson','ninja','bison','flex','gperf','util-macros','xorgproto',
+ 'xcb-proto','wayland-protocols','pkgconf','mako','markupsafe','packaging','python3','extra-cmake-modules',
+ 'qttools','qt6-qttools'}
+# alias qt-corto -> qt6-*
+def canon(n):
+ return n if n in by else ('qt6-'+n if 'qt6-'+n in by else n)
+seen=set(); order=[]
+def visit(n):
+ n=canon(n)
+ if n in seen or n not in by: return
+ seen.add(n)
+ for d in by[n].get('deps',[]): visit(d)
+ order.append(n)
+visit(tgt)
+for n in order:
+ if n not in BUILD_ONLY: print(n)
+PY
+)
+
+mkdir -p "$(dirname "$DB")"
+echo "==> hidratando cierre runtime de $TARGET → $RFS ($(echo "$CLOSURE"|wc -w) pkgs)"
+ok=0; fail=0
+for pkg in $CLOSURE; do
+ before=$(find "$RFS" -type f 2>/dev/null | wc -l)
+ NO_PROXY=localhost NIX_STORE="${NIX_STORE:-$HOME/.nixstore}" \
+ "$HAMMER" --store "$STORE" install "$pkg" --repo "$REPO" --prefix "$RFS" \
+ --trust "$TRUST" --require-signed --db "$DB" >"$RFS/.h-$pkg.log" 2>&1
+ rc=$?
+ after=$(find "$RFS" -type f 2>/dev/null | wc -l)
+ if [ "$after" -gt "$before" ] || grep -q "hidratados" "$RFS/.h-$pkg.log"; then
+ printf " ✓ %-22s (+%s ficheros%s)\n" "$pkg" "$((after-before))" \
+ "$([ $rc -ne 0 ] && echo ', target_bin tolerado')"
+ ok=$((ok+1))
+ else
+ printf " ✗ %-22s rc=%s (ver %s)\n" "$pkg" "$rc" "$RFS/.h-$pkg.log"; fail=$((fail+1))
+ fi
+done
+echo "==> $ok hidratados, $fail fallaron; .so totales: $(find "$RFS" -name '*.so*'|wc -l)"
diff --git a/scripts/kde/repo-selfcontain.py b/scripts/kde/repo-selfcontain.py
new file mode 100644
index 00000000..06aa9186
--- /dev/null
+++ b/scripts/kde/repo-selfcontain.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+"""repo-selfcontain.py — vuelve el repo KDE AUTO-CONTENIDO para hidratar por `install`.
+
+`install` lee los `depends` del recipe embebido en cada `.swm` y resuelve cada nombre como
+CLAVE del índice. El repo KDE cosechado (work/repo-kde) no cierra:
+ 1. deps a nombres de receta-archivo qt-cortos (`qtbase`) cuando el paquete es `qt6-qtbase`.
+ 2. faltan libs base del cierre (libffi/libpng/gperf/mako/markupsafe/packaging y su cierre).
+
+Este script, SIN re-buildear nada:
+ - trae de dist/repo (repo base firmado, cierre completo) todo paquete cuyo nombre no esté ya
+ en el repo KDE (colisión → gana la versión KDE), copiando su `.swm`.
+ - agrega entradas ALIAS qt-corto→qt6-* (mismo `.swm`, otra clave de índice).
+El índice resultante se re-firma aparte con `hammer repo sign`.
+"""
+import json, shutil, sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+KDE = ROOT / "work/repo-kde"
+BASE = ROOT / "dist/repo"
+
+kde_idx = json.loads((KDE / "index.json").read_text())
+base_idx = json.loads((BASE / "index.json").read_text())
+
+kde_pkgs = kde_idx["packages"]
+kde_names = {p["name"] for p in kde_pkgs}
+base_by_name = {p["name"]: p for p in base_idx["packages"]}
+
+added_base, copied, aliases = [], 0, []
+
+# 1) traer del repo base todo lo que el KDE no tenga (colisión → gana KDE)
+for name, entry in base_by_name.items():
+ if name in kde_names:
+ continue
+ src = BASE / entry["file"]
+ if not src.exists():
+ continue
+ dst = KDE / entry["file"]
+ if not dst.exists():
+ shutil.copy2(src, dst); copied += 1
+ kde_pkgs.append(entry)
+ kde_names.add(name)
+ added_base.append(name)
+
+# 2) aliases qt-corto → qt6-* (misma .swm, clave de índice distinta)
+by_name = {p["name"]: p for p in kde_pkgs}
+for p in list(kde_pkgs):
+ n = p["name"]
+ if n.startswith("qt6-qt"):
+ short = n[len("qt6-"):] # qt6-qtbase -> qtbase
+ if short not in kde_names:
+ alias = dict(p); alias["name"] = short
+ kde_pkgs.append(alias); kde_names.add(short); aliases.append(short)
+
+kde_idx["packages"] = kde_pkgs
+(KDE / "index.json").write_text(json.dumps(kde_idx, indent=None))
+
+print(f"base traídos: {len(added_base)} ({copied} .swm copiados)")
+print(f"aliases qt: {len(aliases)} -> {' '.join(sorted(aliases))}")
+print(f"total índice: {len(kde_pkgs)} entradas")
+
+# 3) chequeo de cierre: cada dep declarado en el índice ahora resuelve
+missing = set()
+for p in kde_pkgs:
+ for d in p.get("deps", []):
+ if d not in kde_names:
+ missing.add(d)
+print(f"deps del índice sin resolver: {len(missing)} {sorted(missing) if missing else ''}")