# pdfcraft — el banco de trabajo de PDF: leer, anotar, reordenar, firmar # # ── DE DÓNDE SALE ESTA FAMILIA ───────────────────────────────────────────────────────────────── # ArtCraft publica doce aplicaciones de escritorio en **Rust puro sobre egui + wgpu**, Apache-2.0 o # MIT, cada una en su propio repo de `github.com/storytold`. Para esta casa son interesantes por una # razón concreta y medible: **no tienen ni una línea de GTK ni de Qt**, que es el muro que deja a # `libreoffice` en «59 recetas y tres muros ⇒ no es un ticket, es un año» # (`docs/plan-apps-usuario-final.md:48`). Acá cada app es UNA receta `cargo_vendor`. # # La primera que se midió fue `gridcraft`, y la medición está en su receta: sella en 12 min, abre # ventana en el perfil `escritorio-sway` y **pinta** — `wgpu` toma el backend **GL** sobre EGL/GLES # con llvmpipe (GL 4.5), sin Vulkan y sin ninguna variable de entorno, porque el default de # `egui-wgpu` es `Backends::from_env().unwrap_or(PRIMARY | GL)`. # # ⚠ **Son 0.x y se mueven solas.** Portar esto es comprar su cinta de correr (SDD 26 §2). Se pinea # el **sha del tag**, no `main`, y por eso la versión de arriba es una release y no «lo de hoy». # # ── X11: NO SE PARCHEA, Y ESO ES UNA DECISIÓN MEDIDA ────────────────────────────────────────── # Las doce declaran `eframe` con los features `wayland` **y** `x11`, y la primera versión de esto # arrancaba el feature `x11` con un `sed`. Se quitó, por tres cosas que se midieron al hacerlo: # 1. **No hay muro de build**: lo que `x11` arrastra es `x11rb` (protocolo X en Rust puro) y # `x11-dl` (que hace `dlopen`, no enlaza). Ninguna receta nueva, ni un `.so` de X en el link. # 2. **No hay muro de runtime**: winit elige Wayland cuando `WAYLAND_DISPLAY` está puesto, y en # esta distro `libX11` directamente no existe ⇒ el camino X es inerte, no roto. # 3. **Quitarlo SÍ cuesta**: cambia el grafo ⇒ el `Cargo.lock` deja de cerrar y hay que soltar el # `--locked` del compile (medido: `error: cannot update the lock file … because --locked`). # Y varias de estas apps declaran `winit` con `x11` por su cuenta y lo USAN en el código # (`photocraft` abre por Xwayland si el usuario lo elige en Preferencias), así que un `sed` # ciego rompe el build en unas y es un no-op en otras. # ⇒ Se deja el feature y se conserva el `--locked`, que es el pin fuerte. La distro sigue siendo # Wayland-only por lo que NO lleva en la imagen, no por lo que esta receta le recorta al upstream. # # ── LA FORMA ─────────────────────────────────────────────────────────────────────────────────── # `link = "dynamic"` + `-crt-static` como `boveda`/`shuma-pregunta`/`mirada-greeter`: winit y wgpu # cargan `libwayland-client`, `libEGL`/`libvulkan` por `dlopen`, y un binario `crt-static` no puede. # El lanzador, el MIME y los iconos son de UPSTREAM — lo contrario de `boveda.toml`, donde hay que # autorar el `.desktop` — y están enhebrados: `Icon=`/`StartupWMClass=` del `.desktop`, el nombre de # los iconos hicolor y el `with_app_id()` de la app coinciden en `ai.storyteller.pdfcraft`, así que # el emparejamiento ventana↔lanzador funciona en Wayland sin parchear nada. # # ⚠ `aws-lc-sys` aparece en el lock y sería un muro de verdad (C + cmake + perl, y en musl es # pelea). Lo que la app USA es `ureq` con `rustls-no-provider` + `_ring`, o sea el proveedor de # Rust puro; `aws-lc-sys` queda en el lock sin compilarse. **El lock lista lo posible, no lo que # se construye** — si esta receta falla ahí, la salida es un `--no-default-features` con el # feature de `ring`, no abandonar la app. # # # ── ⚠ DOS COSAS QUE ESTA FAMILIA DESMINTIÓ AL CONSTRUIRSE (2026-10-13) ──────────────────────── # 1. **El panic de `zbus-lockstep-macros` NO se dispara en estas versiones.** `vectorcraft` compiló # `atspi-common` 0.13.0 + `zbus-lockstep-macros` 0.5.2 **sin** el `LOCKSTEP_XML_PATH` del compile # y salió `rc=0`, con el subdirectorio `xml/schemas/` presente en el crate (comprobado en el # registro: `…/atspi-common-0.13.0/xml/schemas/`). O sea que lo de `boveda`/`mirada-greeter` era # de otra combinación, no de la mera presencia del subdir. El bloque se deja en las demás como # seguro barato —cuesta un `cp`— pero **no hay que leerlo como «sin esto no compila»**. # 2. **Los features de `eframe` del manifiesto de la app NO son los del build.** El `apps// # Cargo.toml` de `vectorcraft` pide `eframe` sin `wgpu` y sin `accesskit`, y aun así el binario # corre sobre `egui_wgpu` y compila la torre de `accesskit`+`atspi`: los features son ADITIVOS y # se los enciende otro miembro del workspace (`-ui-egui`). Leer sólo el manifiesto de la app # para decidir qué camino gráfico toma una de éstas da la respuesta equivocada; lo que manda es # lo que aparece en el log del build y en el del binario corriendo. # ⚠ **Medido al correrla (2026-10-13): esta app se queda SIN PORTAPAPELES en esta distro.** # egui_winit::clipboard: Failed to initialize arboard clipboard: … X11 server connection timed out # Su `arboard` no enciende `wayland-data-control` ⇒ intenta X11, que acá no existe, y expira. Son # OCHO de las doce; ver la adenda del 2026-10-13 en `docs/plan-apps-usuario-final.md`. name = "pdfcraft" version = "0.7.1" license = "MIT OR Apache-2.0" [source] # `v0.7.1`, el tag vigente al 2026-10-12. Se pinea el SHA: un tag se mueve. repo = "https://github.com/storytold/pdfcraft" commit = "7163ba3dee8becfff6871c9a65d0d8314314742b" # ⚠ `vendor_cargo_deps` **sobrescribe** `/.cargo/config.toml` con `std::fs::write` # (`crates/takana-build/src/fetch.rs:852`), y estos repos traen uno propio. Hoy es inocuo —dentro # sólo hay `[alias] xtask`, que estas recetas no usan— pero si un bump mete ahí un `rustflags` o un # `linker`, desaparece sin un solo aviso y el fallo no va a nombrar al config. # El `Cargo.lock` está committeado ⇒ el vendoreo va con `--locked` y queda pineado en el tiempo. # # ⚠ **`cargo_vendor_dir` NO es cosmético acá: sin él esta receta FALLA, y falló** (medido # 2026-10-13, `rc=1` a los 8 segundos). pdfcraft **committea su propio `vendor/`** con cuatro crates # —`hayro`, `hayro-syntax`, `hayro-interpret`, `hayro-jbig2`— enganchados por # `[patch.crates-io]` **POR RUTA**. El `cargo vendor` de takana escribe por defecto en `vendor/`, así # que los BORRA, y el error no nombra nada de esto: # # failed to load checksum `.cargo-checksum.json` of hayro-interpret v0.7.0 # failed to read `/src/vendor/hayro-interpret/.cargo-checksum.json` # # Es exactamente la trampa que ya documentan `boveda.toml` (smithay en tawasuyu) y `mise.toml` # (su `vendor/aqua-registry`). Mandando el vendoreo a `.hammer-cargo-vendor/` el árbol de upstream # queda intacto. **Y ojo con el efecto colateral**: el bloque `LOCKSTEP_XML_PATH` de abajo tiene que # mirar ESA ruta y no `vendor/`, o la comprobación a gritos grita por el motivo equivocado. cargo_vendor_dir = ".hammer-cargo-vendor" [build] compiler = "zig-cc" target = "x86_64-linux-musl" link = "dynamic" flags = ["-p", "pdfcraft", "--bin", "pdfcraft"] [build.phases] compile = ''' set -e # LOCKSTEP_XML_PATH — la causa entera está en `mirada-greeter.toml` y repetida en `boveda.toml`: # `accesskit` → `atspi` → `atspi-common`, que trae `zbus-lockstep-macros` como dep NORMAL y cuyo # `#[validate]` recorre el dir `xml/` llamando `.extension().expect()` en CADA entrada ⇒ PANICA al # toparse con el subdirectorio `xml/schemas/`. Apuntando la variable a un dir con sólo los `.xml` se # evita. La comprobación es a gritos: un `cp` mudo dejaría la variable apuntando a un dir VACÍO y el # panic volvería con el error lejos de la causa. XMLDIR="$PWD/.atspi-lockstep-xml" mkdir -p "$XMLDIR" ls .hammer-cargo-vendor/atspi-common/xml/*.xml >/dev/null 2>&1 || { echo "no hay .hammer-cargo-vendor/atspi-common/xml/*.xml — ¿cambió el vendoreo o la versión de atspi? sin esto zbus-lockstep-macros panica en xml/schemas/" >&2; exit 1; } cp .hammer-cargo-vendor/atspi-common/xml/*.xml "$XMLDIR"/ export LOCKSTEP_XML_PATH="$XMLDIR" # El envoltorio de zig-cc, igual que en las otras winit+wgpu del corpus. printf '%s\n' '#!/bin/sh' 'for a do' 'case "$a" in --target=*) a=--target=x86_64-linux-musl ;; esac' 'set -- "$@" "$a"' 'shift' 'done' 'exec zig cc -mcpu=baseline "$@"' > "$PWD/.hammer-zig-cc" chmod +x "$PWD/.hammer-zig-cc" printf '%s\n' '#!/bin/sh' 'exec zig ar "$@"' > "$PWD/.hammer-zig-ar" chmod +x "$PWD/.hammer-zig-ar" RF="-C linker=$PWD/.hammer-zig-cc" rustc -vV | grep -q 'host: .*-alpine-' || RF="$RF -C link-self-contained=no" CC="$PWD/.hammer-zig-cc" AR="$PWD/.hammer-zig-ar" RUSTFLAGS="$RF" cargo rustc --release --locked --offline -p pdfcraft --bin pdfcraft -- -C target-feature=-crt-static ''' install = ''' set -e test -x target/release/pdfcraft || { echo "no hay target/release/pdfcraft — ¿cambió el nombre del bin?" >&2; exit 1; } install -Dm755 target/release/pdfcraft /out/usr/bin/pdfcraft install -Dm644 packaging/linux/ai.storyteller.pdfcraft.desktop /out/usr/share/applications/ai.storyteller.pdfcraft.desktop install -Dm644 packaging/linux/ai.storyteller.pdfcraft.mime.xml /out/usr/share/mime/packages/ai.storyteller.pdfcraft.xml # Los iconos se recorren con un glob y NO con una lista de tamaños: `effectcraft` y `filmcraft` # traen el doble de entradas que las demás, y una lista fija los habría instalado a medias en # silencio. Se cuenta lo instalado y se falla si es cero. n=0 for p in assets/app-icon/hicolor/*/apps/ai.storyteller.pdfcraft.*; do [ -f "$p" ] || continue install -Dm644 "$p" "/out/usr/share/icons/hicolor/${p#assets/app-icon/hicolor/}" n=$((n+1)) done [ "$n" -gt 0 ] || { echo "ningún icono instalado — ¿cambió assets/app-icon/hicolor/?" >&2; exit 1; } install -Dm644 LICENSE-MIT /out/usr/share/licenses/pdfcraft/LICENSE-MIT install -Dm644 LICENSE-APACHE /out/usr/share/licenses/pdfcraft/LICENSE-APACHE # Un `.desktop` vacío o un binario de 0 bytes son la forma de fallo de la casa: artefacto presente, # función ausente (CLAUDE.md §3). test -s /out/usr/share/applications/ai.storyteller.pdfcraft.desktop test -s /out/usr/bin/pdfcraft ''' [deps] # El mismo juego que `boveda`/`shuma-pregunta`/`mirada-greeter`: el stack gráfico se consulta en # build-time y se carga por `dlopen` en runtime. Las fuentes vienen EMBEBIDAS (`default_fonts` de # eframe), así que no hace falta fontconfig ni freetype. build = ["mesa", "wayland", "wayland-protocols", "libdrm", "expat", "zlib", "zstd", "libffi", "pkgconf"]