gioser

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 1534Shell 199Rust 109Markdown 993713 Sutichaykuna

⎇ 2f9253886ff843d9701ac61a9c3ff4f2cd43ef33 scripts/poda-fuentes.sh Shell · 114 siq'ikuna · 6.8 KB Ñawpa willay ch'uya

poda-fuentes: no corría en takana — y la causa era que la caja NO TIENE /dev/fd 80e04872d · Sergio ·

1#!/usr/bin/env bash
2# poda-fuentes.sh — recolector de `work/sources/`. Hermano de `store-gc.sh`, pero el criterio no
3# tiene nada que ver, y confundirlos sería caro.
4#
5# ── QUÉ SE ESTABA ACUMULANDO ────────────────────────────────────────────────────────────────────
6# Medido el 2026-08-31 en gioser: `work/sources` pesaba **79 G** — 106 árboles, media de 745 MB,
7# los peores `pixi` 3,5 G y siete apps cosmic por encima de 3 G cada una. El volumen del store
8# (`/dev/sdb`, 246 G) estaba al **97%, 8,8 G libres**, con la granja escribiendo ahí.
9#
10# Y esos 3,5 G no son fuente: el árbol de `work/sources/<nombre>-<sha>` es a la vez el sitio donde
11# se extrae Y donde se compila (takana parchea y vendorea in situ). Lo que engorda es el `target/`
12# de cargo, los `.o`, el `vmlinux`. Es RESIDUO DE BUILD, no código.
13#
14# ── POR QUÉ BORRARLO NO CUESTA NADA (esto es lo que hace segura la poda) ────────────────────────
15# No es una caché. `takana-build/src/fetch.rs` tiene exactamente dos caminos —`fetch_git` (línea
16# 73) y `fetch_tarball` (línea 234)— y **los dos hacen `remove_dir_all` del árbol y lo re-extraen,
17# incondicionalmente, en cada build**. No hay ni una rama que reutilice un árbol existente.
18#
19# ⇒ Borrar un árbol no cuesta ni la re-extracción: ésa ocurre igual la próxima vez que se construya
20#   esa receta. Lo ÚNICO que se pierde es poder mirar por dentro el último build de esa receta
21#   —el post-mortem de uno que falló—, y ese valor se muere en un día.
22#
23# ── POR QUÉ TOMA `work/.farm-build.lock` (y no es opcional) ─────────────────────────────────────
24# El único modo de que esto haga daño es borrar el árbol que un `bwrap` está usando para compilar:
25# el ADR 0012 en su forma más directa, y el mismo desastre medido en CLAUDE.md §1 (93 de 205
26# recetas KDE muertas con `cc is not a full path`). El lock es el punto de serialización que ya
27# usan `farm-worker-loop.sh` y `campana-deuda.sh`, así que tomarlo acá nos serializa con la granja.
28#
29# ⚠ Y hace falta DE VERDAD, no por prudencia: el filtro es por mtime del directorio raíz, que sólo
30#   cambia cuando se añaden o quitan entradas de primer nivel. Un build largo en vuelo puede tener
31#   un raíz con mtime de hace dos días mientras compila abajo. El mtime NO distingue "viejo" de
32#   "lento". El lock sí.
33#
34# ── POR QUÉ EL SUELO ES EN HORAS Y NO EN DÍAS ───────────────────────────────────────────────────
35# La lección de `cache-ci-no-envejece`: aquel cron podaba a +10 días sobre datos que se reescriben
36# enteros cada ráfaga, o sea que liberaba CERO y parecía que protegía. **Un guardián calibrado a
37# una escala que el dato nunca alcanza no protege.** Acá los 106 árboles abarcaban 3 días (76 de
38# uno solo) ⇒ un umbral en días grandes no habría tocado nada. 24 h deja el post-mortem del día y
39# se lleva el resto.
40#
41# ── POR QUÉ NO SE CONDICIONA A ESPACIO LIBRE ────────────────────────────────────────────────────
42# Porque borrar no cuesta nada. Un umbral por espacio ("podar sólo bajo 20 G") deja que la basura
43# se acumule hasta rozar el borde y convierte una poda barata y plana en un pico cerca del límite,
44# que es justo cuando un build en vuelo puede quedarse sin disco a mitad. Se mantiene plano.
45#
46# Uso:   scripts/poda-fuentes.sh [--aplicar] [--horas N]
47#        (sin --aplicar es DRY-RUN: mide y no borra)
48set -uo pipefail
49ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
50FUENTES="${FUENTES:-work/sources}"
51HORAS="${HORAS:-24}"; APLICAR=0; ESPERA="${ESPERA:-60}"
52while [ $# -gt 0 ]; do
53  case "$1" in
54    --aplicar) APLICAR=1 ;;
55    --horas) HORAS="$2"; shift ;;
56    *) echo "opción desconocida: $1" >&2; exit 2 ;;
57  esac
58  shift
59done
60
61[ -d "$FUENTES" ] || { echo "no existe $FUENTES — nada que podar"; exit 0; }
62
63gigas() { echo "$1" | awk '{printf "%.1f", $1/1073741824}'; }
64
65# `-mmin +N` en vez de `-mtime`: `-mtime +0` es ">24h" por redondeo hacia abajo y no deja expresar
66# nada más fino. Con minutos el umbral se dice tal cual.
67MINUTOS=$((HORAS * 60))
68
69poda() {
70  local libre_antes libre_despues n
71  # ⚠ `df -B1 --output=avail` es GNU y **busybox no lo tiene** (medido en la caja takana el
72  # 2026-09-17). `df -k` + awk da los mismos bytes y anda en los dos mundos.
73  libre_antes=$(df -k "$FUENTES" | tail -1 | awk '{print $4*1024}')
74  # ⚠ NADA DE `<(...)` ACÁ. La sustitución de procesos de bash necesita `/dev/fd`, y una caja
75  # takana **no lo tiene** (gioser sí: `/dev/fd -> /proc/self/fd`). El síntoma no nombra la causa:
76  # `line 72: /dev/fd/63: No such file or directory` y después `viejos: unbound variable`, porque el
77  # `mapfile` nunca corrió. Un fichero temporal anda en cualquier shell y en cualquier /dev.
78  local lista; lista=$(mktemp)
79  find "$FUENTES" -mindepth 1 -maxdepth 1 -type d -mmin "+$MINUTOS" | sort > "$lista"
80  viejos=()
81  while IFS= read -r _l; do [ -n "$_l" ] && viejos+=("$_l"); done < "$lista"
82  rm -f "$lista"
83  n=${#viejos[@]}
84  local quedan
85  quedan=$(find "$FUENTES" -mindepth 1 -maxdepth 1 -type d -not -mmin "+$MINUTOS" | wc -l)
86
87  echo "── poda de $FUENTES · suelo ${HORAS}h · libres $(gigas "$libre_antes") G"
88  echo "   $n árbol(es) por encima del suelo · $quedan se quedan (post-mortem del día)"
89  [ "$n" -eq 0 ] && { echo "   nada que podar"; return 0; }
90
91  if [ "$APLICAR" -eq 0 ]; then
92    # El dry-run NO mide con `du`: recorrer 79 G de árboles cuesta minutos y este script corre
93    # dentro del lock. Se nombran los candidatos y se deja el tamaño para el --aplicar, que lo saca
94    # gratis del `df` de antes y después.
95    printf '   · %s\n' "${viejos[@]}" | head -20
96    [ "$n" -gt 20 ] && echo "   … y $((n - 20)) más"
97    echo "   DRY-RUN: no se borró nada (usar --aplicar)"
98    return 0
99  fi
100
101  printf '%s\0' "${viejos[@]}" | xargs -0 -r rm -rf
102  libre_despues=$(df -k "$FUENTES" | tail -1 | awk '{print $4*1024}')
103  echo "   borrados $n · libres $(gigas "$libre_despues") G · LIBERADO $(gigas $((libre_despues - libre_antes))) G"
104}
105
106# `-w`: si hay un build en vuelo NO se espera indefinidamente. Este script lo llama el cron cada 30
107# min; quedarse colgado del lock durante un kernel de 2 h dejaría al cron sin cosechar. Saltar un
108# ciclo no cuesta nada porque la basura no se va a ningún lado.
109exec 9>work/.farm-build.lock
110if ! flock -w "$ESPERA" 9; then
111  echo "── poda de $FUENTES: build en vuelo (lock tomado tras ${ESPERA}s) ⇒ este ciclo NO toca"
112  exit 0
113fi
114poda