1# `takana-os.net` — el sitio del proyecto
2
3Una sola página, autocontenida, en dos idiomas y dos temas. **No le pide nada a terceros:** las
4tipografías de la marca (Chivo e IBM Plex, las dos SIL OFL) viajan acá en `fuentes/`, subconjuntos
5`latin` y `latin-ext`, 154 KB en total. Un sitio sobre no depender de binarios ajenos que cargara
6las fuentes de Google en cada visita sería una contradicción barata.
7
8| | |
9|---|---|
10| `index.html` | la página entera: CSS y JS adentro, el símbolo como `<symbol>` SVG inline |
11| `fuentes.css` + `fuentes/` | las ocho caras `@font-face`, servidas de acá |
12| `simbolo.svg` | el favicon — el martillo tocapu 7×7, derivado de `docs/marca/simbolo-fondo-claro.svg` |
13
14## Las cuatro secciones
15
16`01 descargar` · `02 las decisiones` · `03 los componentes` · `04 el código`.
17
18**`03` es una ficha de distro, no un semáforo** (2026-09-23, a pedido del usuario: «que no presuma
19inmadurez»). Antes era una tabla con 🟢/🟡/⚪ y una fila que decía «distro redonda — todavía no»:
20una página que le cuenta al visitante lo que le falta antes que lo que tiene. Ahora son tres tablas
21al estilo DistroWatch — el sistema (init, libc, compilador, kernel, gráficos, audio, almacén,
22catálogo), los cinco escritorios con su versión y su clausura sellada, y los paquetes principales
23con su número de versión.
24
25⚠ **Esas versiones salen de las recetas y hay que refrescarlas.** Están a mano en el HTML: si una
26receta sube de versión, la página miente hasta que alguien la toque. Se sacan así:
27
28```sh
29for r in bash coreutils git curl openssl python3 go rust llvm21 firefox …; do
30 printf '%-12s %s\n' "$r" "$(grep -m1 '^version' recipes/$r.toml | sed 's/.*= *"//; s/"//')"
31done
32python3 -c "import json;d=json.load(open('docs/state/build-state.json'));print(d['totals'])"
33ls /store | wc -l
34```
35
36**`04` junta lo que eran dos secciones** («los repositorios» y «cómo se llama»): las dos direcciones,
37el reparto del repo y los tres bloques de comandos son el mismo asunto — dónde vive y cómo se llama.
38
39## El diagrama de los dos pisos
40
41**SVG inline, no una imagen** (2026-09-23). El usuario trajo una infografía generada con ChatGPT y
42no se publicó: llevaba los logos de **NixOS y Alpine** dentro del recuadro del laboratorio —lo que
43afirma una filiación que no existe: takana *importa recetas* de nixpkgs y de aports, no *es* eso— y
44un logo que no es el oficial de `docs/marca`. Además el texto iba quemado en el PNG: ni seleccionable,
45ni traducible con el botón ES/EN, ni adaptable al tema, y 1,9 MB con fondo fotográfico en una página
46que se trae 154 KB de tipografías justamente para no depender de nadie.
47
48Lo que sí valía era la IDEA, así que se dibujó de nuevo: los colores van por variable (cambia con el
49tema), el texto es `<text data-l="es|en">` (lo traduce el mismo CSS que el resto y lo lee un lector de
50pantalla), y muestra el MECANISMO en vez de una lista de logos — **hidratación** subiendo del almacén
51al FHS y **diario** bajando de lo que cambias a mano, que es lo que hace que los dos pisos sean uno.
52
53⚠ `var(--mono)` **no funciona como atributo de presentación** (`font-family="var(--mono)"`): `var()`
54sólo vale en una declaración CSS. Va por clase (`.diagrama .mono`).
55
56⚠ En pantalla angosta el diagrama **scrollea dentro de su caja** (`min-width:404px`): encoger un
57diagrama hasta que las etiquetas no se lean es peor que dejarlo desplazarse.
58
59Comprobado rasterizándolo con el `rsvg-convert` del propio corpus, en los dos temas, antes de
60publicarlo — la primera versión tenía las etiquetas de las dos flechas chocando en el medio y eso no
61se ve leyendo el XML.
62
63## El castellano de la página es NEUTRO
64
65Se quitó el voseo el 2026-09-23 a pedido del usuario: «en abierto suena muy regional argentino».
66Ocho formas (`podés`, `trabajás`, `preferís`, `verificás`, `bajás`, `Probá`, `disponés`, `vos`)
67pasaron a tuteo neutro. El control para que no vuelva a colarse:
68
69```sh
70python3 - <<'PY'
71import re
72h=open('web/takana-os.net/index.html').read()
73es=" ".join(re.sub('<[^>]+>','',t) for t in re.findall(r'data-l="es">(.*?)</span>', h, re.S))
74print([w for w in re.findall(r'\b\w+(?:ás|és|ís)\b|\bvos\b', es) if w.lower() not in
75 ('después','además','través','inglés','país','más','así','aquí','allí','sólo','demás',
76 'detrás','atrás','interés','revés')] or "sin voseo")
77PY
78```
79
80## La sección «las decisiones»
81
82**Trece fichas plegables** (`<details>`, sin una línea de JavaScript), cada una con tres lecturas de
83lo mismo: contra qué se decide, qué implica por dentro, y qué quiere decir sin jerga. Nacen cerradas
84a pedido del usuario (2026-09-23): trece fichas abiertas son una pared, y la aseveración del título
85basta para elegir cuál abrir.
86
87Las secciones «los dos pisos» y «la frontera» se fundieron aquí como fichas 01 y 03 — eran el mismo
88material contado dos veces, en una sección y en una ficha. Y entraron dos que faltaban: **la
89instalación** (un archivo, sin ISO y sin red) y **el escritorio propio + tawasuyu**, que son lo que
90alguien de fuera pregunta primero. **No son opinión: cada una sale de un ADR o de una decisión ya tomada
91en `docs/`** — fuentes clavadas (ADR 0006), `zig cc` (0003), `arje` de init (0007) y el arranque por
92grafo (0010), multi-origen con índice firmado (0014), imágenes ajenas enjauladas (0015), y las de
93Wayland-only y PipeWire, que viven en los frentes de escritorio y audio.
94
95⚠ **Si una decisión cambia, esta página miente hasta que alguien la toque.** Es el precio de
96explicar el diseño en una landing y no en un doc generado: se gana claridad y se paga en
97mantenimiento. Las fichas se armaron con un guion de un solo uso — la página es la fuente, no hay
98generador que mantener.
99
100La paleta y la tipografía salen de **`docs/marca/README.md`** y no se inventan acá. La única
101desviación, deliberada: en tema claro el texto de acento usa un brasa oscurecido (`#9E3F16`), porque
102el `#DF6B2A` de la marca sobre alpaca da 2,9:1 y no se lee. Las combinaciones de la página están
103medidas: ninguna baja de 4,5:1.
104
105## El instalador: `/descargas/` vive FUERA del repo
106
107`https://takana-os.net/descargas/takana-instalador-<variante>.sh` es **un fichero que trae todo
108adentro**:
109cabecera `sh` + un `tar.gz` pegado detrás con el binario `takana`, el instalador que corre en RAM, su
110kernel y el censo del terreno. Se baja, se le da permiso y se corre; no baja nada más.
111
112⚠ **No está en el repo, y no puede estarlo.** Son 137 MiB: en git serían 137 MiB para siempre en
113cada clon, y además **el espejo de GitHub rechaza cualquier fichero de más de 100 MiB** — o sea que
114el `push` se rompería y nadie relacionaría la causa con el sitio. Vive en `/work/sergio/descargas`,
115que está en `sdc` (255 G) y no en la raíz de 5,8 G, que se llena y tira la caja entera.
116
117```sh
118scripts/instalador-paquete.sh # arma el paquete (y comprueba su coherencia)
119scripts/instalador-autoextraible.sh # lo convierte en UN fichero, y lo desempaqueta para probarlo
120cp work/variantes/*.sh work/variantes/*.img /work/sergio/descargas/
121( cd /work/sergio/descargas && for f in takana-instalador-*; do case "$f" in *.sha256) ;; *) sha256sum "$f" > "$f.sha256";; esac; done )
122```
123
124El `.sha256` que se publica al lado es **el del fichero entero**, para comprobar la descarga
125(`sha256sum -c takana-instalador.sh.sha256`). Es distinto del que el `.sh` lleva adentro, que es
126el **del payload** y lo verifica él solo antes de desempaquetar (`./takana-instalador.sh --sha256`
127lo imprime).
128
129⚠ **El hash NO se escribe en `index.html`, y es deliberado.** La primera versión lo traía pegado en
130la página y habría empezado a mentir en el primer rebuild — que es el modo de fallo que este mismo
131README ya avisa para la sección «las decisiones». La página muestra el comando; el número lo sirve
132el fichero de al lado, que se regenera junto al `.sh`.
133
134⚠ **La extensión es `.sh` y no `.run`, y por eso el vhost fuerza `Content-Disposition: attachment`.**
135`.sh` dice lo que el fichero ES —una cabecera de shell con el sistema pegado detrás, legible con
136`head` antes de correrlo— pero invita al navegador a MOSTRARLO: 580 MiB de binario volcados en una
137pestaña. La cabecera lo cierra para las siete descargas a la vez y no depende de que Caddy adivine
138el tipo (hoy no manda ningún `Content-Type` para estos ficheros).
139
140⚠ `/descargas` queda **fuera del `encode`** del vhost: el `.sh` ya viene comprimido, y volver a
141comprimirlo por HTTP gasta CPU en cada descarga sin ahorrar un byte.
142
143### En la página: los enlaces NO están en el HTML
144
145⚠ **Ninguna URL de descarga aparece en `index.html`** (a pedido del usuario, 2026-09-23): el
146selector de variante las compone en JavaScript al pulsarlo, y el path se arma por trozos
147(`'/de'+'scargas/'`). Un rastreador que lee el documento no encuentra una sola, y estos ficheros
148pesan de 138 MiB a 3,4 GiB — no son para que se los baje un bot. Se comprueba con
149`grep -c "descargas/takana-instalador" index.html` ⇒ **0**.
150
151El precio: **sin JavaScript no hay descargas**. Por eso hay un `<noscript>` con la ruta en TEXTO
152PLANO, no como enlace — un enlace ahí devolvería el problema que esto evita.
153
154**Los pesos se corrigen solos.** La tabla en JS (`PESO`) es el valor medido al publicar; al pintar
155cada variante se dispara un `HEAD` y, si responde, el peso real lo pisa. Así una reconstrucción del
156instalador no deja la página mintiendo — que ya pasó: la tabla anterior decía 3,2 y 3,5 GiB cuando
157los ficheros eran 3,1 y 3,4. Si el `HEAD` falla, queda el medido; nunca en blanco.
158
159**El botón de copiar lo pone el JS**, uno por cada `.codigo`, y copia sin el `$` del prompt. No hay
160que escribirlo diez veces en el HTML. Vive en una **banda propia arriba del bloque**
161(`padding-top:2.7rem`) y no flotando sobre la primera línea: un comando largo hace scrollear el
162`<pre>` por debajo del botón, y ahí sí lo pisaría.
163
164**Todo del mismo tamaño, a propósito** (2026-09-23): el selector es `grid-template-columns:1fr 1fr`
165con `grid-auto-rows:1fr` —siempre dos y dos, las cuatro del mismo alto— y los bloques de comandos
166llevan `min-height:7.8rem`, que es lo que ocupan tres líneas con su relleno. Por eso `full`, que no
167tiene archivo único, **no esconde el bloque**: lo deja del mismo tamaño con el aviso adentro. Si se
168escondiera, la página saltaría al elegirlo.
169
170### La prosa del instalador salió de la página, a propósito
171
172El usuario pidió quitar la sobreexplicación (2026-09-23): la sección de descargas es ahora el
173selector, los comandos y nada más. Lo que se quitó **no se perdió** — vive donde se ejecuta:
174`--check`/`--survey`/`--extraer` y el sha256 del payload en `scripts/instalador-autoextraible.sh`,
175`takana.console` en `scripts/instalador-initramfs.sh`, el kexec y las dos confirmaciones en
176`docs/32-instalador-de-usuario.md`, y «el medio desde el que arrancaste no se ofrece» en
177`scripts/takana-install-partition.sh:291`. La página lo estaba repitiendo, no guardando.
178
179### Las variantes: qué sistema se instala
180
181`scripts/instalador-variantes.sh [servidor|mirada|full]` arma las tres, en sus dos medios.
182La diferencia es **qué rootfs se copia al disco**, no cómo: el instalador y el censo son los mismos.
183
184| variante | composición | `.sh` | USB |
185|---|---|---|---|
186| `base` | sólo el piso de arranque | 81 MiB | 357 MiB |
187| `mirada` | + `[perfil.base]` + `[perfil.escritorio-mirada]` | 310 MiB | 1018 MiB |
188| `servidor` | `.sh`: `base` + `servidor-minimo` · USB: `base` + `servidor` | 386 MiB | 2,4 GiB |
189| `full` | + `base` + `cli` + `servidor` + **los cinco escritorios** | **no hay** | 8,6 GiB |
190
191**Todo lo publicado va SIN SÍMBOLOS DE DEPURACIÓN** (decisión del usuario, 2026-09-23). Medido por
192variante, sobre el rootfs que se instala:
193
194| | antes | después | |
195|---|---|---|---|
196| `base` | 265 MiB | 102 MiB | −61 % |
197| `mirada` | 1348 | 764 | −43 % |
198| `servidor-minimo` | 1456 | 933 | −35 % |
199| `servidor` | 2925 | 2113 | −27 % |
200| `full` | 17814 | 8002 | −55 % |
201
202⚠ **Esto NO contradice al [SDD 23](../../docs/23-plan-rehasheo.md)**, que dice —con razón— que un
203`strip` posterior no sirve: eso vale para el STORE, donde el `ArtifactHash` es de ENTRADA y quedaría
204apuntando a un contenido que un rebuild ya no reproduce. Lo que se stripea acá es una COPIA que no
205es un artefacto: no se sella, nadie verifica su hash contra una receta, y **el store conserva los
206símbolos intactos** para quien depure. `strip` es determinista, así que la imagen sigue siendo
207reproducible. `SIN_STRIP=1` lo apaga.
208
209⚠ Y hay una **guarda** antes de stripear: si el árbol compartiera inodos con el stage, stripear
210mutaría el store —y con él todos los artefactos que enlazan a ese fichero, en silencio y para
211siempre—. `cp -a` copia contenido y no enlaza al origen, pero eso es propiedad del comando de
212arriba, no de este bloque: si alguien lo cambia a `cp -al` por rapidez, el guardián grita antes de
213tocar nada.
214
215⚠ **«base» son DOS cosas con el mismo nombre y confundirlas da un rootfs que no arranca.** El
216*piso de arranque* es `product-rootfs` —arje-zero como PID 1 + busybox— y **no está en ningún
217perfil**; el *userland base* es `[perfil.base]`, 359 binarios, y **no tiene ni busybox ni
218arje-zero adentro** (medido: `ls base/bin`). Cada variante es `product-rootfs ∪ perfil.base ∪ …`.
219El [SDD 27 §1](../../docs/27-perfiles-instalables-y-el-instalador.md) ya había medido la otra
220mitad del mismo hueco: los escritorios no heredan `base`.
221
222⚠ **Cada perfil se hidrata en su propio árbol y después se funden**, y no uno encima de otro: la
223auditoría del bit de privilegio corre contra las declaraciones `[[setuid]]` del perfil que se está
224hidratando, así que al poner `escritorio-mirada` sobre `base` veía el `doas` 4755 que dejó `base`
225—declarado y legítimo— y lo denunciaba como un bit que nadie declaró.
226
227⚠ **`servidor` NO es la misma cosa en los dos medios, y es la misma física.** El `.sh` carga el
228sistema entero en memoria —es lo que le permite soltar el disco— así que cada MiB del rootfs es RAM
229exigida en la máquina de quien instala. Por ahí va `servidor-minimo`: **los mismos servicios uno por
230uno** (sshd, caddy, gitea, cron, hora, cortafuegos, los daemons propios), sin la cadena de compilar
231(`rust` 364 MiB, `lld21` 109) y sin heredar `cli` (54 raíces de toolbox interactivo). **1456 MiB
232contra 2925**, y el fichero baja de 1,2 GiB a 580 MiB. Por USB va `servidor` entero, que es el
233destino de verdad. No es una opinión sobre qué debe llevar un servidor: es una restricción del
234MEDIO, y está escrita en los dos sitios —acá y en el `descripcion` del perfil— para que nadie la
235lea como un recorte arbitrario.
236
237⚠ **GNOME estuvo fuera medio día y ya entra.** Se caía por dos cosas suyas, las dos medidas el
2382026-09-23 y las dos resueltas. `accountsservice` trae `/var/lib/AccountsService/users` en **0700**:
239hidratar como usuario normal no puede leerlo y el nodo no proyectaba — se arma con privilegio, que
240ya hacía falta igual por los 0500 de ModemManager. Y `usr/libexec/libgtop_server2` llega **4555**
241desde el tarball de upstream sin que ningún `[[setuid]]` lo declare: el aviso del auditor decía «o
242entra con su motivo, **o sale**» y hasta ese día sólo existía la primera mitad. Ahora
243`takana setuid --apply` **retira** el bit no declarado rompiendo el enlace duro, deja el store
244intacto y dice cuál quitó. Es la opción segura —quita privilegio, nunca lo da— y la coherente con el
245invariante: `of_tree` no hashea ese bit, así que un artefacto que lo trae está fuera de contrato.
246
247⚠ **La holgura del ext4 se escala con el árbol, no es un número fijo.** `du` mide el contenido, no
248lo que el ext4 NECESITA (inodos, journal, mapas de bloques): con 128 MiB fijos alcanzaba para 1 GiB
249y `mke2fs` murió con «Could not allocate block» a mitad de poblar un árbol de 16 GiB, después de
250veinte minutos de trabajo. Hoy es un 8 % del árbol, nunca menos de 128 MiB.
251
252⚠ **Armar una imagen con escritorios pide privilegio**, y las variantes chicas nunca lo tocaron:
253ModemManager instala sus `fcc-unlock.available.d/*` en **0500** tal como vienen de upstream, así que
254copiar el árbol como usuario normal falla. Se copia con `sudo` y **sin `chown`**: `cp -a` como root
255conserva dueño y modo, que es lo que la imagen tiene que llevar.
256
257⚠ **`full` no tiene `.sh`, y es física.** El fichero único carga el instalador entero en memoria
258—es lo que le permite soltar el disco— y ahí viven a la vez el initramfs comprimido y su contenido
259desempaquetado. Con 3,2 GiB de rootfs eso pide ~8 GiB de RAM en la máquina donde se instale. El USB
260no tiene ese límite: corre desde el propio medio. La guarda que lo detecta **mide la RAM de la
261máquina que construye, no la de la que instala** — acá acierta por casualidad (el hub tiene 7,6 GiB,
262parecido a un portátil) y queda anotado sin barrer.
263
264### La imagen USB, para cuando el salto no se puede
265
266`takana-instalador-usb.img` (519 MiB) es **el mismo instalador por otro camino**: mismo árbol, mismo
267censo, mismas guardas. Se graba con `dd` y arranca por la ruta de medios removibles del firmware.
268
269⚠ **No lleva el instalador en la ESP, y no es un capricho:** el cuelgue del primer viaje a metal fue
270un initrd de ~100 MiB cargado por el EFI-stub ([ADR 0010](../../docs/adr/0010-arranque-grafo-mirada.md)
271paso 2), y el del instalador son ~122 MiB. El USB usa el patrón que **sí** está probado —el de un
272disco instalado—: la ESP lleva el kernel y un pivote de 736 KiB, y el pivote encuentra la ext4 por
273`LABEL=takana-medio` y hace `switch_root`. La etiqueta NO es `hammer-root` a propósito: si el medio
274se llamara igual que la raíz de un sistema instalado, el pivote podría montar el disco del usuario
275en vez del USB, y el que pierde es el disco.
276
277```sh
278scripts/instalador-usb.sh # arma work/takana-instalador-usb.img
279scripts/instalador-usb-test.sh --instalar # lo arranca en OVMF y lo contesta por el serial
280cp work/takana-instalador-usb.img /work/sergio/descargas/
281( cd /work/sergio/descargas && sha256sum takana-instalador-usb.img > takana-instalador-usb.img.sha256 )
282```
283
284**Está probado de punta a punta, no sólo servido:** `VIA_RUN=1 scripts/metal-salto-test.sh` mete el
285fichero publicado en una VM con `linux-metal-kexec`, lo corre, y comprueba sobre el DISCO que se
286verificó solo, se desempaquetó, saltó e instaló — 13 comprobaciones, con el `bootmgfw.efi` del
287vecino byte a byte igual y la tabla de particiones sin cambiar.
288
289Y el USB, `scripts/instalador-usb-test.sh --instalar`: **arranca por firmware de verdad** (OVMF por
290`pflash`, sin `-kernel`, que es la única forma de probar la tabla, la ESP, el stub y el nombre
291`\initramfs.cpio.gz` que el cmdline horneado exige), **contesta el menú tecleando por el serial** y
292comprueba el disco destino. 12 en verde.
293
294⚠ El arnés corre con `-vga none`, y eso decide qué rama del código se prueba: sin pantalla el
295instalador detecta el `vtconsole` `dummy` y se va al serial. **Con** pantalla —el caso del usuario—
296la pregunta sale por `tty0`. Las dos ramas existen porque el cmdline HORNEADO del kernel pone `tty0`
297primero y `ttyS0` último, o sea que `/dev/console` es el serial: en un portátil sin puerto serie, sin
298esta detección, el censo y la pregunta se irían a un sitio que no existe y el arranque parecería
299colgado.
300
301## Las dos direcciones que publica
302
303- **código** → `https://git.gioser.net/sergio/takana` (público; `git clone` por HTTPS)
304- **paquetes** → `https://takana-os.net/repo/`, que es el MISMO `/srv/repo` firmado que sirve
305 `repo.gioser.net`. Comprobado: `takana install age --repo https://repo.gioser.net --trust ./trust`
306 responde `release: trusted (by release)`.
307
308Y **desde el 2026-09-21 se puede mirar el repo sin construir nada**, que antes no se podía:
309`repo list` y `repo verify` aceptan la misma lista de orígenes que `install`. Hasta ese día sólo
310miraban directorios locales y ante una URL respondían «repo vacío» **con salida 0** — 173 paquetes
311leídos como ninguno.
312
313```sh
314takana repo list --repo https://takana-os.net/repo # 173 paquete(s)
315takana repo verify --repo https://takana-os.net/repo --trust ./trust # release: trusted
316```
317
318## Cómo se sirve
319
320Caddy, en la caja `takana`, vhost `takana-os.net, www.takana-os.net`. La raíz es
321**`/work/sergio/takana/web/takana-os.net`**, el árbol de trabajo vivo: *publicar es guardar el
322fichero*. No lo refresca `publicar-webs.sh` a propósito — un `pull` sobre el árbol compartido mueve
323HEAD bajo los pies de otro agente (CLAUDE.md §2 ter).
324
325Este README **no se sirve**: son notas de operación, y el matcher del vhost las 404ea.
326
327Los bloques, por si hay que rehacer la caja (`caddy validate` → `arjectl restart caddy`, **nunca**
328`caddy reload`: el Caddyfile lleva `admin off`, SDD 28 §6.14):
329
330```caddyfile
331takana-os.net, www.takana-os.net {
332 import acceso
333 @comprimible not path /descargas/*
334 encode @comprimible gzip zstd
335
336 redir /descargas /descargas/ permanent
337 handle_path /descargas/* {
338 root * /work/sergio/descargas
339 header Content-Disposition "attachment"
340 file_server browse
341 }
342
343 redir /repo /repo/ permanent
344 handle_path /repo/* {
345 root * /srv/repo
346 file_server browse
347 }
348
349 handle {
350 root * /work/sergio/takana/web/takana-os.net
351 @oculto path /.* */.* /README.md
352 respond @oculto 404
353 file_server
354 }
355}
356
357git.takana-os.net {
358 import acceso
359 redir https://git.gioser.net{uri} permanent
360}
361
362takana.gioser.net {
363 import acceso
364 redir https://takana-os.net{uri} permanent
365}
366```
367
368`scripts/servidor/publicar-webs.sh` comprueba los cuatro: los dos que sirven ficheros por sha
369contra el disco, y los dos que redirigen por destino.
370
371## Los nombres
372
373| nombre | qué hace | estado |
374|---|---|---|
375| `takana-os.net`, `www` | el sitio | ✅ con certificado de Let's Encrypt (2026-09-21) |
376| `takana-os.net/repo/` | el repo de paquetes, el mismo `/srv/repo` | ✅ |
377| `takana-os.net/descargas/` | el instalador en UN fichero, y la imagen USB (`/work/sergio/descargas`) | ✅ (2026-09-23) |
378| `takana.gioser.net` | **301 → `takana-os.net`** | ✅ |
379| `git.takana-os.net` | **301 → `git.gioser.net`** | ⏳ vhost puesto, **falta el registro A** |
380
381`git.takana-os.net` sólo redirige, y es deliberado: gitea tiene UN `ROOT_URL` con el que arma cada
382enlace y cada URL de clon que muestra, así que colgarlo de un segundo nombre daría un sitio que se
383contradice —entrás por uno y te ofrece clonar del otro— y moverlo de verdad rompe los clones y el
384CI que ya apuntan al viejo.
385
386Los registros van a `2.29.29.217`, la IP de la caja:
387
388```
389takana-os.net. A 2.29.29.217 ✅ puesto
390www.takana-os.net. A 2.29.29.217 ✅ puesto
391git.takana-os.net. A 2.29.29.217 ⏳ falta
392```
393
394El certificado sale solo en la primera petición tras el DNS — y **tarda unos segundos más que el
395primer `curl`**: si da error, reintentar antes de ir a mirar logs (SDD 28 §6.14). Si el nombre ya
396resolvía cuando Caddy arrancó pero el certificado no llegó, **reiniciar caddy fuerza el reintento**
397sin esperar el backoff: fue lo que hizo falta acá.
README.md
takana-os.net — el sitio del proyecto
Una sola página, autocontenida, en dos idiomas y dos temas. No le pide nada a terceros: las
tipografías de la marca (Chivo e IBM Plex, las dos SIL OFL) viajan acá en fuentes/, subconjuntos
latin y latin-ext, 154 KB en total. Un sitio sobre no depender de binarios ajenos que cargara
las fuentes de Google en cada visita sería una contradicción barata.
| |
index.html | la página entera: CSS y JS adentro, el símbolo como <symbol> SVG inline |
fuentes.css + fuentes/ | las ocho caras @font-face, servidas de acá |
simbolo.svg | el favicon — el martillo tocapu 7×7, derivado de docs/marca/simbolo-fondo-claro.svg |
Las cuatro secciones
01 descargar · 02 las decisiones · 03 los componentes · 04 el código.
03 es una ficha de distro, no un semáforo (2026-09-23, a pedido del usuario: «que no presuma
inmadurez»). Antes era una tabla con 🟢/🟡/⚪ y una fila que decía «distro redonda — todavía no»:
una página que le cuenta al visitante lo que le falta antes que lo que tiene. Ahora son tres tablas
al estilo DistroWatch — el sistema (init, libc, compilador, kernel, gráficos, audio, almacén,
catálogo), los cinco escritorios con su versión y su clausura sellada, y los paquetes principales
con su número de versión.
⚠ Esas versiones salen de las recetas y hay que refrescarlas. Están a mano en el HTML: si una
receta sube de versión, la página miente hasta que alguien la toque. Se sacan así:
for r in bash coreutils git curl openssl python3 go rust llvm21 firefox …; do
printf '%-12s %s\n' "$r" "$(grep -m1 '^version' recipes/$r.toml | sed 's/.*= *"//; s/"//')"
done
python3 -c "import json;d=json.load(open('docs/state/build-state.json'));print(d['totals'])"
ls /store | wc -l
04 junta lo que eran dos secciones («los repositorios» y «cómo se llama»): las dos direcciones,
el reparto del repo y los tres bloques de comandos son el mismo asunto — dónde vive y cómo se llama.
El diagrama de los dos pisos
SVG inline, no una imagen (2026-09-23). El usuario trajo una infografía generada con ChatGPT y
no se publicó: llevaba los logos de NixOS y Alpine dentro del recuadro del laboratorio —lo que
afirma una filiación que no existe: takana importa recetas de nixpkgs y de aports, no es eso— y
un logo que no es el oficial de docs/marca. Además el texto iba quemado en el PNG: ni seleccionable,
ni traducible con el botón ES/EN, ni adaptable al tema, y 1,9 MB con fondo fotográfico en una página
que se trae 154 KB de tipografías justamente para no depender de nadie.
Lo que sí valía era la IDEA, así que se dibujó de nuevo: los colores van por variable (cambia con el
tema), el texto es <text data-l="es|en"> (lo traduce el mismo CSS que el resto y lo lee un lector de
pantalla), y muestra el MECANISMO en vez de una lista de logos — hidratación subiendo del almacén
al FHS y diario bajando de lo que cambias a mano, que es lo que hace que los dos pisos sean uno.
⚠ var(--mono) no funciona como atributo de presentación (font-family="var(--mono)"): var()
sólo vale en una declaración CSS. Va por clase (.diagrama .mono).
⚠ En pantalla angosta el diagrama scrollea dentro de su caja (min-width:404px): encoger un
diagrama hasta que las etiquetas no se lean es peor que dejarlo desplazarse.
Comprobado rasterizándolo con el rsvg-convert del propio corpus, en los dos temas, antes de
publicarlo — la primera versión tenía las etiquetas de las dos flechas chocando en el medio y eso no
se ve leyendo el XML.
El castellano de la página es NEUTRO
Se quitó el voseo el 2026-09-23 a pedido del usuario: «en abierto suena muy regional argentino».
Ocho formas (podés, trabajás, preferís, verificás, bajás, Probá, disponés, vos)
pasaron a tuteo neutro. El control para que no vuelva a colarse:
python3 - <<'PY'
import re
h=open('web/takana-os.net/index.html').read()
es=" ".join(re.sub('<[^>]+>','',t) for t in re.findall(r'data-l="es">(.*?)</span>', h, re.S))
print([w for w in re.findall(r'\b\w+(?:ás|és|ís)\b|\bvos\b', es) if w.lower() not in
('después','además','través','inglés','país','más','así','aquí','allí','sólo','demás',
'detrás','atrás','interés','revés')] or "sin voseo")
PY
La sección «las decisiones»
Trece fichas plegables (<details>, sin una línea de JavaScript), cada una con tres lecturas de
lo mismo: contra qué se decide, qué implica por dentro, y qué quiere decir sin jerga. Nacen cerradas
a pedido del usuario (2026-09-23): trece fichas abiertas son una pared, y la aseveración del título
basta para elegir cuál abrir.
Las secciones «los dos pisos» y «la frontera» se fundieron aquí como fichas 01 y 03 — eran el mismo
material contado dos veces, en una sección y en una ficha. Y entraron dos que faltaban: la
instalación (un archivo, sin ISO y sin red) y el escritorio propio + tawasuyu, que son lo que
alguien de fuera pregunta primero. No son opinión: cada una sale de un ADR o de una decisión ya tomada
en docs/ — fuentes clavadas (ADR 0006), zig cc (0003), arje de init (0007) y el arranque por
grafo (0010), multi-origen con índice firmado (0014), imágenes ajenas enjauladas (0015), y las de
Wayland-only y PipeWire, que viven en los frentes de escritorio y audio.
⚠ Si una decisión cambia, esta página miente hasta que alguien la toque. Es el precio de
explicar el diseño en una landing y no en un doc generado: se gana claridad y se paga en
mantenimiento. Las fichas se armaron con un guion de un solo uso — la página es la fuente, no hay
generador que mantener.
La paleta y la tipografía salen de docs/marca/README.md y no se inventan acá. La única
desviación, deliberada: en tema claro el texto de acento usa un brasa oscurecido (#9E3F16), porque
el #DF6B2A de la marca sobre alpaca da 2,9:1 y no se lee. Las combinaciones de la página están
medidas: ninguna baja de 4,5:1.
El instalador: /descargas/ vive FUERA del repo
https://takana-os.net/descargas/takana-instalador-<variante>.sh es un fichero que trae todo
adentro:
cabecera sh + un tar.gz pegado detrás con el binario takana, el instalador que corre en RAM, su
kernel y el censo del terreno. Se baja, se le da permiso y se corre; no baja nada más.
⚠ No está en el repo, y no puede estarlo. Son 137 MiB: en git serían 137 MiB para siempre en
cada clon, y además el espejo de GitHub rechaza cualquier fichero de más de 100 MiB — o sea que
el push se rompería y nadie relacionaría la causa con el sitio. Vive en /work/sergio/descargas,
que está en sdc (255 G) y no en la raíz de 5,8 G, que se llena y tira la caja entera.
scripts/instalador-paquete.sh # arma el paquete (y comprueba su coherencia)
scripts/instalador-autoextraible.sh # lo convierte en UN fichero, y lo desempaqueta para probarlo
cp work/variantes/*.sh work/variantes/*.img /work/sergio/descargas/
( cd /work/sergio/descargas && for f in takana-instalador-*; do case "$f" in *.sha256) ;; *) sha256sum "$f" > "$f.sha256";; esac; done )
El .sha256 que se publica al lado es el del fichero entero, para comprobar la descarga
(sha256sum -c takana-instalador.sh.sha256). Es distinto del que el .sh lleva adentro, que es
el del payload y lo verifica él solo antes de desempaquetar (./takana-instalador.sh --sha256
lo imprime).
⚠ El hash NO se escribe en index.html, y es deliberado. La primera versión lo traía pegado en
la página y habría empezado a mentir en el primer rebuild — que es el modo de fallo que este mismo
README ya avisa para la sección «las decisiones». La página muestra el comando; el número lo sirve
el fichero de al lado, que se regenera junto al .sh.
⚠ La extensión es .sh y no .run, y por eso el vhost fuerza Content-Disposition: attachment.
.sh dice lo que el fichero ES —una cabecera de shell con el sistema pegado detrás, legible con
head antes de correrlo— pero invita al navegador a MOSTRARLO: 580 MiB de binario volcados en una
pestaña. La cabecera lo cierra para las siete descargas a la vez y no depende de que Caddy adivine
el tipo (hoy no manda ningún Content-Type para estos ficheros).
⚠ /descargas queda fuera del encode del vhost: el .sh ya viene comprimido, y volver a
comprimirlo por HTTP gasta CPU en cada descarga sin ahorrar un byte.
En la página: los enlaces NO están en el HTML
⚠ Ninguna URL de descarga aparece en index.html (a pedido del usuario, 2026-09-23): el
selector de variante las compone en JavaScript al pulsarlo, y el path se arma por trozos
('/de'+'scargas/'). Un rastreador que lee el documento no encuentra una sola, y estos ficheros
pesan de 138 MiB a 3,4 GiB — no son para que se los baje un bot. Se comprueba con
grep -c "descargas/takana-instalador" index.html ⇒ 0.
El precio: sin JavaScript no hay descargas. Por eso hay un <noscript> con la ruta en TEXTO
PLANO, no como enlace — un enlace ahí devolvería el problema que esto evita.
Los pesos se corrigen solos. La tabla en JS (PESO) es el valor medido al publicar; al pintar
cada variante se dispara un HEAD y, si responde, el peso real lo pisa. Así una reconstrucción del
instalador no deja la página mintiendo — que ya pasó: la tabla anterior decía 3,2 y 3,5 GiB cuando
los ficheros eran 3,1 y 3,4. Si el HEAD falla, queda el medido; nunca en blanco.
El botón de copiar lo pone el JS, uno por cada .codigo, y copia sin el $ del prompt. No hay
que escribirlo diez veces en el HTML. Vive en una banda propia arriba del bloque
(padding-top:2.7rem) y no flotando sobre la primera línea: un comando largo hace scrollear el
<pre> por debajo del botón, y ahí sí lo pisaría.
Todo del mismo tamaño, a propósito (2026-09-23): el selector es grid-template-columns:1fr 1fr
con grid-auto-rows:1fr —siempre dos y dos, las cuatro del mismo alto— y los bloques de comandos
llevan min-height:7.8rem, que es lo que ocupan tres líneas con su relleno. Por eso full, que no
tiene archivo único, no esconde el bloque: lo deja del mismo tamaño con el aviso adentro. Si se
escondiera, la página saltaría al elegirlo.
La prosa del instalador salió de la página, a propósito
El usuario pidió quitar la sobreexplicación (2026-09-23): la sección de descargas es ahora el
selector, los comandos y nada más. Lo que se quitó no se perdió — vive donde se ejecuta:
--check/--survey/--extraer y el sha256 del payload en scripts/instalador-autoextraible.sh,
takana.console en scripts/instalador-initramfs.sh, el kexec y las dos confirmaciones en
docs/32-instalador-de-usuario.md, y «el medio desde el que arrancaste no se ofrece» en
scripts/takana-install-partition.sh:291. La página lo estaba repitiendo, no guardando.
Las variantes: qué sistema se instala
scripts/instalador-variantes.sh [servidor|mirada|full] arma las tres, en sus dos medios.
La diferencia es qué rootfs se copia al disco, no cómo: el instalador y el censo son los mismos.
| variante | composición | .sh | USB |
base | sólo el piso de arranque | 81 MiB | 357 MiB |
mirada | + [perfil.base] + [perfil.escritorio-mirada] | 310 MiB | 1018 MiB |
servidor | .sh: base + servidor-minimo · USB: base + servidor | 386 MiB | 2,4 GiB |
full | + base + cli + servidor + los cinco escritorios | no hay | 8,6 GiB |
Todo lo publicado va SIN SÍMBOLOS DE DEPURACIÓN (decisión del usuario, 2026-09-23). Medido por
variante, sobre el rootfs que se instala:
| antes | después | |
base | 265 MiB | 102 MiB | −61 % |
mirada | 1348 | 764 | −43 % |
servidor-minimo | 1456 | 933 | −35 % |
servidor | 2925 | 2113 | −27 % |
full | 17814 | 8002 | −55 % |
⚠ Esto NO contradice al SDD 23, que dice —con razón— que un
strip posterior no sirve: eso vale para el STORE, donde el ArtifactHash es de ENTRADA y quedaría
apuntando a un contenido que un rebuild ya no reproduce. Lo que se stripea acá es una COPIA que no
es un artefacto: no se sella, nadie verifica su hash contra una receta, y el store conserva los
símbolos intactos para quien depure. strip es determinista, así que la imagen sigue siendo
reproducible. SIN_STRIP=1 lo apaga.
⚠ Y hay una guarda antes de stripear: si el árbol compartiera inodos con el stage, stripear
mutaría el store —y con él todos los artefactos que enlazan a ese fichero, en silencio y para
siempre—. cp -a copia contenido y no enlaza al origen, pero eso es propiedad del comando de
arriba, no de este bloque: si alguien lo cambia a cp -al por rapidez, el guardián grita antes de
tocar nada.
⚠ «base» son DOS cosas con el mismo nombre y confundirlas da un rootfs que no arranca. El
piso de arranque es product-rootfs —arje-zero como PID 1 + busybox— y no está en ningún
perfil; el userland base es [perfil.base], 359 binarios, y no tiene ni busybox ni
arje-zero adentro (medido: ls base/bin). Cada variante es product-rootfs ∪ perfil.base ∪ ….
El SDD 27 §1 ya había medido la otra
mitad del mismo hueco: los escritorios no heredan base.
⚠ Cada perfil se hidrata en su propio árbol y después se funden, y no uno encima de otro: la
auditoría del bit de privilegio corre contra las declaraciones [[setuid]] del perfil que se está
hidratando, así que al poner escritorio-mirada sobre base veía el doas 4755 que dejó base
—declarado y legítimo— y lo denunciaba como un bit que nadie declaró.
⚠ servidor NO es la misma cosa en los dos medios, y es la misma física. El .sh carga el
sistema entero en memoria —es lo que le permite soltar el disco— así que cada MiB del rootfs es RAM
exigida en la máquina de quien instala. Por ahí va servidor-minimo: los mismos servicios uno por
uno (sshd, caddy, gitea, cron, hora, cortafuegos, los daemons propios), sin la cadena de compilar
(rust 364 MiB, lld21 109) y sin heredar cli (54 raíces de toolbox interactivo). 1456 MiB
contra 2925, y el fichero baja de 1,2 GiB a 580 MiB. Por USB va servidor entero, que es el
destino de verdad. No es una opinión sobre qué debe llevar un servidor: es una restricción del
MEDIO, y está escrita en los dos sitios —acá y en el descripcion del perfil— para que nadie la
lea como un recorte arbitrario.
⚠ GNOME estuvo fuera medio día y ya entra. Se caía por dos cosas suyas, las dos medidas el
2026-09-23 y las dos resueltas. accountsservice trae /var/lib/AccountsService/users en 0700:
hidratar como usuario normal no puede leerlo y el nodo no proyectaba — se arma con privilegio, que
ya hacía falta igual por los 0500 de ModemManager. Y usr/libexec/libgtop_server2 llega 4555
desde el tarball de upstream sin que ningún [[setuid]] lo declare: el aviso del auditor decía «o
entra con su motivo, o sale» y hasta ese día sólo existía la primera mitad. Ahora
takana setuid --apply retira el bit no declarado rompiendo el enlace duro, deja el store
intacto y dice cuál quitó. Es la opción segura —quita privilegio, nunca lo da— y la coherente con el
invariante: of_tree no hashea ese bit, así que un artefacto que lo trae está fuera de contrato.
⚠ La holgura del ext4 se escala con el árbol, no es un número fijo. du mide el contenido, no
lo que el ext4 NECESITA (inodos, journal, mapas de bloques): con 128 MiB fijos alcanzaba para 1 GiB
y mke2fs murió con «Could not allocate block» a mitad de poblar un árbol de 16 GiB, después de
veinte minutos de trabajo. Hoy es un 8 % del árbol, nunca menos de 128 MiB.
⚠ Armar una imagen con escritorios pide privilegio, y las variantes chicas nunca lo tocaron:
ModemManager instala sus fcc-unlock.available.d/* en 0500 tal como vienen de upstream, así que
copiar el árbol como usuario normal falla. Se copia con sudo y sin chown: cp -a como root
conserva dueño y modo, que es lo que la imagen tiene que llevar.
⚠ full no tiene .sh, y es física. El fichero único carga el instalador entero en memoria
—es lo que le permite soltar el disco— y ahí viven a la vez el initramfs comprimido y su contenido
desempaquetado. Con 3,2 GiB de rootfs eso pide ~8 GiB de RAM en la máquina donde se instale. El USB
no tiene ese límite: corre desde el propio medio. La guarda que lo detecta mide la RAM de la
máquina que construye, no la de la que instala — acá acierta por casualidad (el hub tiene 7,6 GiB,
parecido a un portátil) y queda anotado sin barrer.
La imagen USB, para cuando el salto no se puede
takana-instalador-usb.img (519 MiB) es el mismo instalador por otro camino: mismo árbol, mismo
censo, mismas guardas. Se graba con dd y arranca por la ruta de medios removibles del firmware.
⚠ No lleva el instalador en la ESP, y no es un capricho: el cuelgue del primer viaje a metal fue
un initrd de ~100 MiB cargado por el EFI-stub (ADR 0010
paso 2), y el del instalador son ~122 MiB. El USB usa el patrón que sí está probado —el de un
disco instalado—: la ESP lleva el kernel y un pivote de 736 KiB, y el pivote encuentra la ext4 por
LABEL=takana-medio y hace switch_root. La etiqueta NO es hammer-root a propósito: si el medio
se llamara igual que la raíz de un sistema instalado, el pivote podría montar el disco del usuario
en vez del USB, y el que pierde es el disco.
scripts/instalador-usb.sh # arma work/takana-instalador-usb.img
scripts/instalador-usb-test.sh --instalar # lo arranca en OVMF y lo contesta por el serial
cp work/takana-instalador-usb.img /work/sergio/descargas/
( cd /work/sergio/descargas && sha256sum takana-instalador-usb.img > takana-instalador-usb.img.sha256 )
Está probado de punta a punta, no sólo servido: VIA_RUN=1 scripts/metal-salto-test.sh mete el
fichero publicado en una VM con linux-metal-kexec, lo corre, y comprueba sobre el DISCO que se
verificó solo, se desempaquetó, saltó e instaló — 13 comprobaciones, con el bootmgfw.efi del
vecino byte a byte igual y la tabla de particiones sin cambiar.
Y el USB, scripts/instalador-usb-test.sh --instalar: arranca por firmware de verdad (OVMF por
pflash, sin -kernel, que es la única forma de probar la tabla, la ESP, el stub y el nombre
\initramfs.cpio.gz que el cmdline horneado exige), contesta el menú tecleando por el serial y
comprueba el disco destino. 12 en verde.
⚠ El arnés corre con -vga none, y eso decide qué rama del código se prueba: sin pantalla el
instalador detecta el vtconsole dummy y se va al serial. Con pantalla —el caso del usuario—
la pregunta sale por tty0. Las dos ramas existen porque el cmdline HORNEADO del kernel pone tty0
primero y ttyS0 último, o sea que /dev/console es el serial: en un portátil sin puerto serie, sin
esta detección, el censo y la pregunta se irían a un sitio que no existe y el arranque parecería
colgado.
Las dos direcciones que publica
- código →
https://git.gioser.net/sergio/takana (público; git clone por HTTPS)
- paquetes →
https://takana-os.net/repo/, que es el MISMO /srv/repo firmado que sirve
repo.gioser.net. Comprobado: takana install age --repo https://repo.gioser.net --trust ./trust
responde release: trusted (by release).
Y desde el 2026-09-21 se puede mirar el repo sin construir nada, que antes no se podía:
repo list y repo verify aceptan la misma lista de orígenes que install. Hasta ese día sólo
miraban directorios locales y ante una URL respondían «repo vacío» con salida 0 — 173 paquetes
leídos como ninguno.
takana repo list --repo https://takana-os.net/repo # 173 paquete(s)
takana repo verify --repo https://takana-os.net/repo --trust ./trust # release: trusted
Cómo se sirve
Caddy, en la caja takana, vhost takana-os.net, www.takana-os.net. La raíz es
/work/sergio/takana/web/takana-os.net, el árbol de trabajo vivo: publicar es guardar el
fichero. No lo refresca publicar-webs.sh a propósito — un pull sobre el árbol compartido mueve
HEAD bajo los pies de otro agente (CLAUDE.md §2 ter).
Este README no se sirve: son notas de operación, y el matcher del vhost las 404ea.
Los bloques, por si hay que rehacer la caja (caddy validate → arjectl restart caddy, nunca
caddy reload: el Caddyfile lleva admin off, SDD 28 §6.14):
takana-os.net, www.takana-os.net {
import acceso
@comprimible not path /descargas/*
encode @comprimible gzip zstd
redir /descargas /descargas/ permanent
handle_path /descargas/* {
root * /work/sergio/descargas
header Content-Disposition "attachment"
file_server browse
}
redir /repo /repo/ permanent
handle_path /repo/* {
root * /srv/repo
file_server browse
}
handle {
root * /work/sergio/takana/web/takana-os.net
@oculto path /.* */.* /README.md
respond @oculto 404
file_server
}
}
git.takana-os.net {
import acceso
redir https://git.gioser.net{uri} permanent
}
takana.gioser.net {
import acceso
redir https://takana-os.net{uri} permanent
}
scripts/servidor/publicar-webs.sh comprueba los cuatro: los dos que sirven ficheros por sha
contra el disco, y los dos que redirigen por destino.
Los nombres
| nombre | qué hace | estado |
takana-os.net, www | el sitio | ✅ con certificado de Let's Encrypt (2026-09-21) |
takana-os.net/repo/ | el repo de paquetes, el mismo /srv/repo | ✅ |
takana-os.net/descargas/ | el instalador en UN fichero, y la imagen USB (/work/sergio/descargas) | ✅ (2026-09-23) |
takana.gioser.net | 301 → takana-os.net | ✅ |
git.takana-os.net | 301 → git.gioser.net | ⏳ vhost puesto, falta el registro A |
git.takana-os.net sólo redirige, y es deliberado: gitea tiene UN ROOT_URL con el que arma cada
enlace y cada URL de clon que muestra, así que colgarlo de un segundo nombre daría un sitio que se
contradice —entrás por uno y te ofrece clonar del otro— y moverlo de verdad rompe los clones y el
CI que ya apuntan al viejo.
Los registros van a 2.29.29.217, la IP de la caja:
takana-os.net. A 2.29.29.217 ✅ puesto
www.takana-os.net. A 2.29.29.217 ✅ puesto
git.takana-os.net. A 2.29.29.217 ⏳ falta
El certificado sale solo en la primera petición tras el DNS — y tarda unos segundos más que el
primer curl: si da error, reintentar antes de ir a mirar logs (SDD 28 §6.14). Si el nombre ya
resolvía cuando Caddy arrancó pero el certificado no llegó, reiniciar caddy fuerza el reintento
sin esperar el backoff: fue lo que hizo falta acá.