tawasuyu

sergio/takana

> 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…

TOML 604Rust 56Shell 36Markdown 281227 Definiciones

gnome: los 7 typelibs CERRADOS — librsvg y ibus, y dos deudas viejas pagadas de paso

  Rsvg-2.0   librsvg b3:351f4658
  IBus-1.0   ibus    b3:d4d3750a

**1. PERILLA NUEVA DEL LAB: `[source] cargo_vendor`.** `detect_build_system` asume UN
sistema de build por árbol, y librsvg genuinamente tiene dos: `configure.ac` gana por
precedencia pero su Makefile llama a `cargo build`, que dentro del sandbox hermético
no tiene red. La perilla fuerza el vendoreo (que ocurre en el fetch, donde sí hay red)
sin tocar la detección. **No entra al ArtifactHash** —decide de dónde salen las deps,
no cuáles: eso lo fija el Cargo.lock, ya bajo el sha256 de la fuente— así que se puede
prender en una receta ya sellada sin re-hashear nada. Con test, y verificado en vivo:
libelogind no movió su hash tras el cambio.

**2. LA DEUDA DEL UNWINDER, PAGADA.** librsvg moría en
`undefined reference: _Unwind_DeleteException`. No es de librsvg: es de la `std` de
rustc, que trae landing pads y espera el runtime que en glibc vive en libgcc_s. Es la
deuda que el corpus arrastra desde matar-gcc —«las 12 recetas Rust son UN problema, no
12»— y el remedio estaba a mano: **zig empaqueta la libunwind de LLVM** y exporta los
`_Unwind_*` (verificado con nm). Alcanza `LIBS=-lunwind`. Va por LIBS y no por LDFLAGS
porque autotools pone LIBS al FINAL de la línea de enlace, que es donde tiene que ir
una librería que resuelve símbolos indefinidos.

librsvg va en 2.58.5 y no 2.59+: en 2.59 cambió a meson + cargo-c, y cargo-cbuild
enlaza el crate `cargo` entero (libgit2, libssh2, libcurl, openssl) — una campaña
propia por un binario que sólo corre en el constructor. 2.58.5 produce el mismo
Rsvg-2.0. Mismo criterio que gnome-desktop 44.5.

**3. `x11-compose-data`, y es una tensión que vale la pena tener escrita.** ibus COMPILA
la tabla de teclas muertas de X11 dentro de libibus (Makefile.am:310, incondicional, sin
`--disable-`). Sin datos no construye. Los datos viven en el tarball de libX11 por
historia, no por necesidad técnica: son 5192 líneas de reglas. La receta extrae SÓLO los
ficheros de locale reproduciendo la regla de upstream (`cpprules.in`: cpp crudo +
CPP_SED_MAGIC literal) y no compila una línea de X11. Un escritorio Wayland-only sigue
necesitando la tabla de composición del mundo Unix.

Gotchas de ibus, los tres medidos: su ayuda MIENTE (`--enable-gtk4`/`--enable-wayland`
sugieren default apagado; el default es `yes`); `--disable-emoji-dict` NO apaga
`--disable-unicode-dict`; y con `--disable-wayland` el build muere igual porque
`tools/main.c` llama wl_display_* sin `#ifdef` mientras WAYLAND_LIBS sólo se agrega si
la opción está prendida — bug de upstream en su propia configuración sin Wayland.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

bc4d0c8aead387176a1c09e7fa82fe981e50d140 · por sergio · (hace 2 meses) · padre 0a76ead12 · ver el árbol en este commit

Delta semántico · 3

crates/hammer-build/src/lib.rs

  • cambió por dentro fn build pub

crates/hammer-core/src/recipe.rs

6 archivos · +324 −3

diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs
index 6e615ca8..995ef5ea 100644
--- a/crates/hammer-build/src/lib.rs
+++ b/crates/hammer-build/src/lib.rs
@@ -220,7 +220,13 @@ pub fn build(
 
     // Recetas Cargo: vendoreamos las deps (red permitida en el fetch) para que el build sea
     // hermético `--offline` dentro del sandbox. Tras los patches, por si tocaron Cargo.toml/lock.
-    if matches!(detect_build_system(&src_tree), BuildSys::Cargo) {
+    // `cargo_vendor = true` fuerza esta rama en árboles POLÍGLOTAS, donde `detect_build_system`
+    // —que asume un sistema por árbol— eligió el de C. librsvg es el arquetipo: `configure.ac` gana
+    // por precedencia, pero su Makefile llama a `cargo build`, y sin vendoreo ese cargo sale a la
+    // red dentro del sandbox hermético. Ver `Source::cargo_vendor`.
+    if matches!(detect_build_system(&src_tree), BuildSys::Cargo)
+        || eff_recipe.source.cargo_vendor == Some(true)
+    {
         // El source se copia DENTRO del repo hammer (que es un workspace Cargo) ⇒ si el Cargo.toml
         // de la fuente NO declara su propio `[workspace]`, cargo cree que pertenece al workspace de
         // hammer y `cargo vendor` aborta. Inyectamos un `[workspace]` vacío para aislarla. SÓLO si
@@ -229,7 +235,13 @@ pub fn build(
         // por receta. Ver memoria 'cargo-recipe crate suelto'.
         ensure_cargo_workspace_isolation(&src_tree)?;
         // Extractor de workspaces (Etapa G): si el root es virtual, resuelve `-p <pkg>` del bin.
-        inject_cargo_package_selector(&mut eff_recipe, &src_tree);
+        // SÓLO cuando el árbol ES una receta Cargo. En un políglota (`cargo_vendor = true`) el
+        // producto lo arma el sistema de build de C —que llama a cargo con sus propios argumentos—
+        // y meterle un `-p` a los flags de la receta sería inyectar una opción de Cargo en un
+        // `./configure`. Acá el forzado pide vendoreo, nada más.
+        if matches!(detect_build_system(&src_tree), BuildSys::Cargo) {
+            inject_cargo_package_selector(&mut eff_recipe, &src_tree);
+        }
         tracing::info!("cargo: vendoreando deps para build offline");
         fetch::vendor_cargo_deps(
             &src_tree,
diff --git a/crates/hammer-core/src/recipe.rs b/crates/hammer-core/src/recipe.rs
index d565e54f..b21c8957 100644
--- a/crates/hammer-core/src/recipe.rs
+++ b/crates/hammer-core/src/recipe.rs
@@ -66,6 +66,19 @@ pub struct Source {
     /// `.cargo/config.toml` que emite `cargo vendor` ya apunta al dir que se le pasa.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub cargo_vendor_dir: Option<String>,
+    /// Fuerza el vendoreo cargo aunque el árbol NO se detecte como `BuildSys::Cargo`.
+    ///
+    /// Existe porque `detect_build_system` asume **un** sistema de build por árbol, y hay proyectos
+    /// que genuinamente tienen dos: C por fuera, Rust por dentro. El arquetipo es librsvg —trae
+    /// `configure.ac` Y `Cargo.toml`, y autotools gana por precedencia—, pero su `Makefile` invoca
+    /// `cargo build`. Sin vendoreo ese cargo intenta salir a la red DENTRO del sandbox hermético y
+    /// el build muere. Es un patrón, no una excepción: cada vez hay más GNOME así.
+    ///
+    /// No cambia qué se construye, sólo de dónde salen las deps —el `Cargo.lock` del source es el
+    /// que fija las versiones— así que **no entra al ArtifactHash**, igual que `cargo_vendor_dir`.
+    /// Es lo que permite prenderla en una receta ya sellada sin re-hashearla.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub cargo_vendor: Option<bool>,
     #[serde(default)]
     pub patches: Vec<String>,
 }
@@ -732,6 +745,34 @@ expected_output = "b3:deadbeef"
         );
     }
 
+    #[test]
+    fn cargo_vendor_roundtripea_y_no_afecta_el_hash() {
+        // `cargo_vendor` decide DE DÓNDE salen las deps de Cargo (registro vs. `vendor/` local), no
+        // CUÁLES: eso lo fija el `Cargo.lock` del source, que ya está bajo el hash de la fuente. Por
+        // eso no entra a `hash_inputs`, igual que `cargo_vendor_dir` — y por eso se puede prender en
+        // una receta YA SELLADA para destrabar su build offline sin re-hashear nada aguas abajo.
+        let sin = Recipe::from_toml(SAMPLE).unwrap();
+        let mut con = sin.clone();
+        con.source.cargo_vendor = Some(true);
+        assert_eq!(
+            sin.hash_inputs(&[]).unwrap(),
+            con.hash_inputs(&[]).unwrap(),
+            "cargo_vendor NO debe entrar en hash_inputs"
+        );
+        // Y roundtripea por TOML: si se perdiera al serializar, el forzado se apagaría solo en
+        // cualquier flujo que reescriba la receta.
+        let r = Recipe::from_toml(&con.to_toml().unwrap()).unwrap();
+        assert_eq!(r.source.cargo_vendor, Some(true));
+        // Ausente sigue siendo ausente (no `Some(false)`), que es lo que mantiene el TOML limpio.
+        assert_eq!(
+            Recipe::from_toml(&sin.to_toml().unwrap())
+                .unwrap()
+                .source
+                .cargo_vendor,
+            None
+        );
+    }
+
     #[test]
     fn slots_round_trip_y_no_afectan_el_hash() {
         // H4b: el bloque `slots` declara TOPOLOGÍA (qué toca / de qué depende), no identidad ⇒
diff --git a/recipes/incoming-gnome/ibus.toml b/recipes/incoming-gnome/ibus.toml
new file mode 100644
index 00000000..6b26ef4b
--- /dev/null
+++ b/recipes/incoming-gnome/ibus.toml
@@ -0,0 +1,107 @@
+# ibus 1.5.34 — el bus de métodos de entrada (teclados de chino/japonés/coreano, emoji, Unicode).
+# De acá sale el typelib `IBus-1.0`.
+#
+# POR QUÉ ENTRA, y es el caso más claro de la tanda: gnome-shell lo importa desde JavaScript **sin
+# condicional** (`js/misc/dependencies.js`: `import 'gi://IBus?version=1.0'`), aunque el usuario no
+# use ningún método de entrada. El panel de teclado del shell habla con ibus siempre. Dep de RUNTIME
+# invisible al grafo de build — el mismo patrón que AccountsService, Gdm, Geoclue y GWeather.
+#
+# LO QUE SE CONSTRUYE ES LA LIBRERÍA Y EL BUS, NO EL ESCRITORIO DE ibus. Casi todos los
+# `--disable-*` de abajo apagan piezas que dependen de cosas que esta distro no tiene y que el shell
+# no consulta:
+#   --disable-gtk2/3/4                los módulos de método de entrada para GTK. GTK3 no está en el
+#                                     corpus (decisión de X11-al-tacho) y el de GTK4 no hace falta:
+#                                     GTK4 habla `text-input` de Wayland directo con el compositor.
+#                                     ⚠ OJO CON EL TEXTO DE AYUDA DE UPSTREAM: dice `--enable-gtk4`
+#                                     y `--enable-wayland`, lo que sugiere que están APAGADAS por
+#                                     defecto. No lo están: `enable_gtk4=yes` y `enable_wayland=yes`
+#                                     son los defaults reales (configure.ac:277 y :299). Sin
+#                                     `--disable-gtk4` explícito el configure corta pidiendo gtk4.
+#                                     La ayuda miente; el default manda.
+#
+# ⚠ `--enable-wayland` VA PRENDIDA, y no por gusto. Empezó apagada —el módulo Wayland es para que el
+# demonio de ibus hable con un compositor, y el shell implementa `text-input` por su cuenta— pero con
+# `--disable-wayland` el build MUERE igual: `tools/main.c` incluye `<wayland-client.h>` y llama
+# `wl_display_connect`/`wl_display_disconnect` **sin ningún `#ifdef`** (:1260), mientras que el
+# `WAYLAND_LIBS` que resolvería esos símbolos sólo se agrega si la opción está encendida. Es un bug
+# de upstream en su propia configuración «sin Wayland». Prenderla es más honesto que parchear el
+# `tools/`: esta distro ES Wayland. Arrastra `libxkbcommon`, que su PKG_CHECK pide junto a
+# `wayland-client`.
+#   --disable-xim                     el servidor XIM. Es X11 puro.
+#   --disable-setup                   `ibus-setup`, una GUI en Python+GTK3.
+#   --disable-python2 / --disable-python-library / --disable-dbus-python-check
+#                                     bindings de Python 2. No hay Python 2 y no va a haber.
+#   --disable-dconf --enable-memconf  ibus guarda su configuración en dconf por defecto; dconf no
+#                                     está en el corpus (ya lo esquivó gdm por la misma razón).
+#                                     `memconf` es el backend EN MEMORIA que trae upstream para
+#                                     exactamente este caso: la config no persiste entre sesiones,
+#                                     que para un escritorio sin métodos de entrada configurados es
+#                                     la diferencia entre nada y nada.
+#   --disable-systemd-services        acá PID1 es arje-zero.
+#   --disable-appindicator            pide dbusmenu-glib/dbusmenu-gtk3 (bandeja legacy de Ubuntu).
+#   --disable-libnotify               sirve para que el DEMONIO avise «se cambió el método de
+#                                     entrada» por notificación de escritorio. libnotify no está en
+#                                     el corpus, y las notificaciones del escritorio las sirve el
+#                                     propio gnome-shell.
+#   --disable-engine                  el motor `simple` (mapas de teclado de teclas muertas). Va
+#                                     junto con emoji-dict: son datos y lógica de método de entrada,
+#                                     no la librería.
+#   --disable-emoji-dict              el diccionario de emoji se compila desde los XML de CLDR y
+#                                     `/usr/share/unicode/emoji`, datos que no están en el corpus.
+#                                     Se pierde el selector de emoji de ibus, no el typelib.
+#   --disable-unicode-dict            su hermano: el índice de nombres de caracteres se compila desde
+#                                     los ficheros UCD (`NamesList.txt`, `Blocks.txt` de
+#                                     `/usr/share/unicode/ucd`). Apagar emoji-dict NO lo apaga —son
+#                                     dos opciones distintas, y el configure aborta por la segunda
+#                                     después de pasar la primera.
+#   --disable-tests --disable-gtk-doc --disable-vala
+#
+# Las comprobaciones de X11/XFixes y de libevdev/libudev son BLANDAS en el configure.ac
+# (:778 y :785 no abortan, sólo definen un condicional), así que no hace falta apagarlas ni
+# satisfacerlas: se resuelven en "no" solas.
+#
+# ══ LA DEP QUE NO SE VE VENIR: `x11-compose-data` ══════════════════════════════════════════════
+# ibus COMPILA la tabla de teclas muertas de X11 dentro de libibus. `gen-internal-compose-table`
+# (src/gencomposetable.c:70) lee un fichero `Compose` en tiempo de build y lo convierte en un
+# GResource que entra en `ibusresources.c`; ese paso es INCONDICIONAL en su Makefile.am:310 y no hay
+# `--disable-` que lo evite. Sin datos el build muere con
+# `Failed to open file "/X11/locale/en/Compose"`.
+#
+# Los datos viven en el tarball de libX11 —por historia, no por necesidad técnica— así que hay una
+# receta aparte, `x11-compose-data`, que extrae SÓLO los ficheros de locale y no compila una línea
+# de X11. Es la tensión de un escritorio Wayland-only con un mundo Unix cuya tabla de composición
+# sigue publicándose dentro de X11, y queda escrita ahí.
+#
+# También pide `iso-codes` (nombres de idioma para el selector de método de entrada), que ya estaba.
+#
+# ISLA DINÁMICA: el typelib nombra `libibus-1.0.so.5` y gjs la dlopea.
+name    = "ibus"
+version = "1.5.34"
+
+[source]
+tarball = "https://github.com/ibus/ibus/releases/download/1.5.34/ibus-1.5.34.tar.gz"
+sha256  = "12a72210ce5250f8a66df562a75e034e67b2bce74c4df2983dc86d1154894943"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "dynamic"
+
+[build.phases]
+configure = '''
+set -e
+PKG_CONFIG_PATH=/usr/lib/pkgconfig ./configure --prefix=/usr \
+  --enable-introspection --enable-memconf \
+  --disable-gtk2 --disable-gtk3 --disable-gtk4 --enable-wayland \
+  --disable-xim --disable-dconf \
+  --disable-setup --disable-python2 --disable-python-library \
+  --disable-dbus-python-check --disable-systemd-services \
+  --disable-appindicator --disable-libnotify --disable-engine \
+  --disable-emoji-dict --disable-unicode-dict --disable-tests \
+  --disable-gtk-doc --disable-vala --disable-nls
+'''
+compile = "make"
+install = "DESTDIR=/out make install"
+
+[deps]
+build = ["make", "pkgconf", "python3", "py3-setuptools", "gobject-introspection", "gi-foreign-girs", "glib", "glib-introspected", "iso-codes", "x11-compose-data", "wayland", "wayland-protocols", "libxkbcommon", "dbus-shared", "libffi", "pcre2", "zlib-shared", "expat"]
diff --git a/recipes/incoming-gnome/librsvg.toml b/recipes/incoming-gnome/librsvg.toml
new file mode 100644
index 00000000..ca75168f
--- /dev/null
+++ b/recipes/incoming-gnome/librsvg.toml
@@ -0,0 +1,78 @@
+# librsvg 2.58.5 — el renderizador de SVG de GNOME, escrito en Rust. De acá sale el typelib `Rsvg-2.0`.
+#
+# POR QUÉ ENTRA: gnome-shell lo importa desde JavaScript sin condicional
+# (`js/misc/dependencies.js`: `import 'gi://Rsvg?version=2.0'`). Es lo que dibuja los iconos
+# simbólicos del panel y los fondos vectoriales. Dep de RUNTIME del shell, invisible a su grafo de
+# build.
+#
+# ══ POR QUÉ 2.58 Y NO 2.59+, que es lo que shippea GNOME 48 ═════════════════════════════════════
+# En 2.59 librsvg cambió a **meson + cargo-c**, y `cargo-cbuild` (el binario de lu-zero/cargo-c) no
+# está en el corpus. Empaquetarlo no es una receta más: cargo-c ENLAZA el crate `cargo`, que arrastra
+# libgit2, libssh2, libcurl y openssl como `*-sys`. Es una campaña propia, y por un binario que sólo
+# corre en el constructor.
+#
+# 2.58.5 es la última de la serie autotools + `cargo build` plano, y produce **el mismo `Rsvg-2.0`**
+# —la ABI del typelib no se movió—, así que el shell no nota la diferencia. Es el mismo criterio que
+# ya se aplicó a gnome-desktop 44.5 en esta cola: la versión correcta es la que cierra la cadena, no
+# la más nueva. Cuando cargo-c exista, subir es cambiar dos líneas.
+#
+# ══ LA PERILLA NUEVA DEL LAB: `cargo_vendor = true` ═════════════════════════════════════════════
+# librsvg es POLÍGLOTA — `configure.ac` Y `Cargo.toml` en la misma raíz— y `detect_build_system`
+# asume un sistema por árbol: autotools gana por precedencia. Pero el `Makefile` invoca
+# `cargo build`, y dentro del sandbox hermético ese cargo no tiene red ⇒ el build moría intentando
+# bajar el registro. La perilla fuerza el vendoreo (que ocurre en el fetch, donde SÍ hay red) sin
+# tocar la detección. No entra al ArtifactHash: decide de dónde salen las deps, no cuáles —eso lo
+# fija el `Cargo.lock` del tarball, que ya está bajo el sha256 de la fuente.
+#
+# `CARGO_NET_OFFLINE=true` cierra el círculo: sin él cargo intentaría igual la red antes de mirar el
+# `.cargo/config.toml` vendoreado en algunos caminos.
+#
+# ══ `LIBS=-lunwind`: LA DEUDA DEL UNWINDER, PAGADA ═════════════════════════════════════════════
+# Sin esto el build muere al linkear el binario temporal de la introspección:
+#
+#   ld.lld: error: undefined reference: _Unwind_DeleteException
+#   >>> referenced by ./.libs/librsvg-2.so (disallowed by --no-allow-shlib-undefined)
+#
+# **No es de librsvg: es de la `std` de rustc**, que trae landing pads de unwinding y espera el
+# runtime que en glibc vive en `libgcc_s`. Es exactamente la deuda que el corpus arrastra desde hace
+# tiempo —«las 12 recetas Rust de la lista de matar-gcc son UN problema, no 12: falta un runtime de
+# unwinding»— y el remedio resulta estar a mano: **zig empaqueta la libunwind de LLVM**, y exporta
+# los `_Unwind_*` que hacen falta (verificado con `nm` sobre el `libunwind.a` de su caché). O sea que
+# no hay que traer libgcc: alcanza con pedirle a zig el suyo.
+#
+# Va por `LIBS` y no por `LDFLAGS` a propósito: autotools pone `LIBS` al FINAL de la línea de enlace,
+# que es donde tiene que ir una librería que resuelve símbolos indefinidos. En `LDFLAGS` iría antes
+# de los objetos y el linker la descartaría por no tener nada pendiente todavía.
+#
+# ══ ALCANCE ════════════════════════════════════════════════════════════════════════════════════
+# `--disable-pixbuf-loader`: el loader SVG de gdk-pixbuf necesita correr
+# `gdk-pixbuf-query-loaders` y reescribir el `loaders.cache` del sistema — un efecto sobre OTRO
+# paquete, que en un store direccionable por contenido no se puede hacer desde acá. El shell no lo
+# usa: carga SVG por `imports.gi.Rsvg`, no por GdkPixbuf.
+# `--disable-gtk-doc`: gtk-doc no está en el corpus.
+#
+# ISLA DINÁMICA: el typelib nombra `librsvg-2.so.2` y gjs la dlopea.
+name    = "librsvg"
+version = "2.58.5"
+
+[source]
+tarball = "https://download.gnome.org/sources/librsvg/2.58/librsvg-2.58.5.tar.xz"
+sha256  = "224233a0e347d38c415f15a49f0e0885313e3ecc18f3192055f9304dd2f3a27a"
+cargo_vendor = true
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "dynamic"
+
+[build.phases]
+configure = '''
+set -e
+PKG_CONFIG_PATH=/usr/lib/pkgconfig LIBS=-lunwind ./configure --prefix=/usr \
+  --disable-gtk-doc --disable-pixbuf-loader --enable-introspection --disable-vala
+'''
+compile = "CARGO_NET_OFFLINE=true make"
+install = "CARGO_NET_OFFLINE=true DESTDIR=/out make install"
+
+[deps]
+build = ["make", "pkgconf", "python3", "py3-setuptools", "gobject-introspection", "gi-foreign-girs", "glib", "glib-introspected", "gdk-pixbuf", "cairo-shared", "pango", "harfbuzz", "fribidi", "freetype-shared", "fontconfig-shared", "libxml2-shared", "libpng-shared", "pixman", "expat", "libffi", "pcre2", "zlib-shared"]
diff --git a/recipes/incoming-gnome/x11-compose-data.toml b/recipes/incoming-gnome/x11-compose-data.toml
new file mode 100644
index 00000000..a7e92628
--- /dev/null
+++ b/recipes/incoming-gnome/x11-compose-data.toml
@@ -0,0 +1,76 @@
+# x11-compose-data — la base de secuencias Compose de X11, y NADA de X11.
+#
+# QUÉ ES: los ficheros `/usr/share/X11/locale/…` que definen las teclas muertas y las combinaciones
+# de `Multi_key` (`<Compose> a '` → `á`). Son **datos**, no código: 5192 líneas de reglas para
+# `en_US.UTF-8`. Viven en el tarball de libX11 porque históricamente X11 fue quien las mantuvo, no
+# porque necesiten X11 para existir.
+#
+# POR QUÉ LAS NECESITA UNA DISTRO SIN X11, y es una tensión real que conviene dejar escrita: **ibus
+# las compila DENTRO de libibus**. Su `gen-internal-compose-table` (src/gencomposetable.c:70) lee un
+# fichero `Compose` en tiempo de build y lo convierte en `compose/sequences-<endian>-endian`, que
+# entra como GResource en `ibusresources.c`. Ese paso NO es opcional en su Makefile.am:310 — no hay
+# `--disable-` que lo evite. Sin datos, el build de ibus muere con
+# `Failed to open file "/X11/locale/en/Compose"`.
+#
+# O sea: el escritorio es Wayland-only por decisión, pero la tabla de teclas muertas del mundo Unix
+# sigue publicándose dentro de libX11. Empaquetar esos datos NO reintroduce X11: no hay una sola
+# línea de código, ni un `.so`, ni una dependencia. Es el mismo criterio que `iso-codes`.
+#
+# CÓMO SE GENERAN: los ficheros del tarball son `.pre` y hay que pasarlos por el preprocesador de C.
+# La receta REPRODUCE la regla de upstream (`cpprules.in`) tal cual, en vez de improvisar:
+#   · `cpp -undef -traditional -DWCHAR32=1` — `RAWCPP $(RAWCPPFLAGS) $(CPP_FILES_FLAGS)`.
+#   · el `CPP_SED_MAGIC`, copiado literal. Traduce `XCOMM` a `#` con sed y no con `-DXCOMM=XCOMM`
+#     —el comentario de upstream explica que ese truco no funciona en todos los preprocesadores—,
+#     borra las marcas de línea de cpp, sustituye `X11_LOCALEDATADIR` por la ruta real y colapsa los
+#     `@@` finales en barras invertidas.
+# Verificado: la salida son 5192 líneas con 2337 secuencias de teclas muertas, sintaxis intacta.
+#
+# SÓLO en_US.UTF-8. Es la que ibus busca (`sys_langs[] = { "en_US.UTF-8", "en_US", "en" }`) y la que
+# todas las demás locales incluyen por `include "%L"`. Traer las ~180 locales sería traer el árbol
+# de i18n de X11 entero para que nadie lo lea.
+#
+# `locale.dir` no es decorativo: el configure de ibus decide `X11_LOCALEDATADIR` comprobando que
+# EXISTA (configure.ac:394). Sin él cae a un `$(datadir)` sin expandir y el define queda en
+# `"/X11/locale"`, que es exactamente el error de arriba.
+name    = "x11-compose-data"
+version = "1.8.13"
+
+[source]
+tarball = "https://gitlab.freedesktop.org/xorg/lib/libx11/-/archive/libX11-1.8.13/libx11-libX11-1.8.13.tar.gz"
+sha256  = "d210291f5cd974e5029cce4bde0fc1b8bfc0ce88b8530ea6ba4b1fcf141506ab"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "dynamic"
+
+[build.phases]
+configure = "true"
+compile   = "true"
+install   = '''
+set -e
+LOCALEDIR=/usr/share/X11/locale
+mkdir -p "/out$LOCALEDIR/en_US.UTF-8"
+# La regla de upstream, literal (cpprules.in): cpp crudo + el CPP_SED_MAGIC.
+gen() {
+  cpp -undef -traditional -DWCHAR32=1 < "$1" | LC_CTYPE=C sed \
+    -e '/^\#  *[0-9][0-9]*  *.*$/d' \
+    -e '/^\#line  *[0-9][0-9]*  *.*$/d' \
+    -e '/^[	 ]*XCOMM$/s/XCOMM/\#/' \
+    -e '/^[	 ]*XCOMM[^a-zA-Z0-9_]/s/XCOMM/\#/' \
+    -e '/^[	 ]*XHASH/s/XHASH/\#/' \
+    -e "s,X11_LOCALEDATADIR,$LOCALEDIR,g" \
+    -e '/\@\@$/s/\@\@$/\\/' \
+    -e '/^$/d' > "$2"
+}
+gen nls/en_US.UTF-8/Compose.pre    "/out$LOCALEDIR/en_US.UTF-8/Compose"
+gen nls/en_US.UTF-8/XLC_LOCALE.pre "/out$LOCALEDIR/en_US.UTF-8/XLC_LOCALE"
+gen nls/locale.dir.pre             "/out$LOCALEDIR/locale.dir"
+gen nls/compose.dir.pre            "/out$LOCALEDIR/compose.dir"
+gen nls/locale.alias.pre           "/out$LOCALEDIR/locale.alias"
+cp nls/en_US.UTF-8/XI18N_OBJS      "/out$LOCALEDIR/en_US.UTF-8/XI18N_OBJS"
+wc -l "/out$LOCALEDIR/en_US.UTF-8/Compose"
+'''
+
+[deps]
+build = ["make", "pkgconf"]
diff --git a/scripts/gnome/hydrate-gnome.sh b/scripts/gnome/hydrate-gnome.sh
index f7f821b5..7d2e3dbd 100755
--- a/scripts/gnome/hydrate-gnome.sh
+++ b/scripts/gnome/hydrate-gnome.sh
@@ -32,7 +32,14 @@ else
     recipes/incoming-gnome/gnome-shell.toml
     recipes/incoming-gnome/accountsservice.toml   # imports.gi.AccountsService
     recipes/incoming-gnome/gi-foreign-typelibs.toml  # DBus-1.0/cairo-1.0, que Gdk y Atspi incluyen
-    recipes/incoming-gnome/libgdm.toml               # imports.gi.Gdm (js/misc/dependencies.js)
+    # El resto sale de `js/misc/dependencies.js` del shell — la lista donde DECLARA qué typelibs
+    # exige al arrancar. Cerrarlas de una evita el bucle de «arrancar, ver qué falta, rehacer imagen».
+    recipes/incoming-gnome/libgdm.toml               # imports.gi.Gdm
+    recipes/incoming-gnome/geoclue.toml              # imports.gi.Geoclue
+    recipes/incoming-gnome/libgweather.toml          # imports.gi.GWeather
+    recipes/incoming-gnome/upower.toml               # imports.gi.UPowerGlib
+    recipes/incoming-gnome/librsvg.toml              # imports.gi.Rsvg
+    recipes/incoming-gnome/ibus.toml                 # imports.gi.IBus
   )
 fi