instalador: el censo sale en JSON, mirar deja de costar lo que escribir, y una candidata que no entra deja de ofrecerse
El censo es el contrato con cualquier cara gráfica (ADR 0021): `--survey --json` emite dev/partuuid/mib/fs/etiqueta/montaje/veredicto/usable/explicacion. Existe para que NO haya un segundo censo: el censo es lo que decide qué no se toca, y dos censos que divergen no muestran de menos — formatean de más. El campo `usable` es la lectura de una sola pregunta, para que una cara no reimplemente el case de los veredictos y se equivoque en el default. Y al ponerle la cara aparecieron tres defectos del motor, los tres sólo visibles desde afuera: - `--survey` exigía mke2fs, los tres mtools y el bzImage. En una máquina ajena —sin store y sin kernel del corpus— moría con «no encuentro el kernel EFI-stub» ANTES de imprimir una línea del terreno, que es justo lo primero que se corre en una máquina desconocida. Mirar necesita sfdisk y blkid, nada más. - `--survey` salía con 1 habiendo funcionado. Un `a && b && c` como ÚLTIMO comando del bucle de impresión decide el estado de salida del script entero: con set -e, mostrar_censo moría si la última fila no tenía PARTUUID (un disco sin tabla, un hueco libre). En este hub la última fila es /dev/sdc. Un if no propaga; el && sí. - el censo ofrecía /dev/sda1 —2 MiB, el hueco de arranque— como «se puede usar», porque el veredicto miraba QUÉ hay dentro y nunca CUÁNTO cabe. Elegirla formatea algo real y muere a mitad de la copia. El piso es ahora el mismo número con el que se talla un hueco, así que las dos puertas no divergen; con --root-part explícito AVISA y no aborta (el banco instala en 1200 MiB y funciona). Un censo vacío tampoco se calla: dentro de esta jaula /dev no tiene un solo dispositivo de bloque y la tabla salía vacía sin decir por qué. Banco de pruebas en verde (25 comprobaciones, tres escenarios).
Semantic delta
No definition changed: this change is text, documentation or data.
2 files · +114 −17
diff --git a/scripts/lib/herramientas-desde-store.sh b/scripts/lib/herramientas-desde-store.sh
index 33a00350..2cbc07b6 100644
--- a/scripts/lib/herramientas-desde-store.sh
+++ b/scripts/lib/herramientas-desde-store.sh
@@ -75,7 +75,11 @@ herramienta() {
fi
fi
if [ -n "$_hr_host" ] && command -v "$_hr_host" >/dev/null 2>&1; then
- echo " ⚠ ${_hr_rel##*/} ← $(command -v "$_hr_host") DEL HOST, sin hash: por acá la imagen no es reproducible" >&2
+ # El aviso se lee también donde no se está armando ninguna imagen —`takana metal survey`
+ # en una máquina ajena cae acá siempre, porque ahí NO HAY store y no puede haberlo—, así
+ # que dice qué pasó y no qué significa: «sin hash» vale en los dos casos; «la imagen no es
+ # reproducible» sonaba a error para el que sólo está mirando el disco.
+ echo " ⚠ ${_hr_rel##*/} ← $(command -v "$_hr_host") del HOST, sin hash (si estás armando una imagen, por acá deja de ser reproducible)" >&2
command -v "$_hr_host"; return 0
fi
echo " ✗ falta ${_hr_rel##*/}: ni sellado ($_hr_store/$_hr_hash-$_hr_receta/$_hr_rel) ni en el PATH ($_hr_host)" >&2
diff --git a/scripts/takana-install-partition.sh b/scripts/takana-install-partition.sh
index 5f3806f8..bd5256fc 100755
--- a/scripts/takana-install-partition.sh
+++ b/scripts/takana-install-partition.sh
@@ -32,6 +32,8 @@
# estables entre arranques (medido) y el kexec cambia quién enumera.
# --survey mirar y NO tocar: el censo del terreno de esta máquina, con el
# PARTUUID de cada partición y qué se puede usar. Empezá por acá.
+# --json (con --survey) el mismo censo en JSON. Es el CONTRATO que consume
+# una cara gráfica: `takana metal survey --json` lo envuelve.
# (sin destino) modo INTERACTIVO: muestra el censo y te deja elegir. Es lo que hace
# que un mismo medio sirva en cualquier máquina.
# --use-free-space no formatear nada existente: crear la partición en el hueco sin
@@ -48,7 +50,7 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
DEST=""; ESP=""; DISK=""; ESP_PART=""; CLAIM_FALLBACK=""; NO_NVRAM=""; ASSUME_YES=""
-USE_FREE=""; ON_ACTIVE="${ON_ACTIVE:-auto}"; SOLO_CENSO=""
+USE_FREE=""; ON_ACTIVE="${ON_ACTIVE:-auto}"; SOLO_CENSO=""; CENSO_JSON=""
while [ $# -gt 0 ]; do
case "$1" in
--root-part) DEST="${2:?}"; shift 2 ;;
@@ -58,6 +60,7 @@ while [ $# -gt 0 ]; do
--claim-fallback) CLAIM_FALLBACK=1; shift ;;
--no-nvram) NO_NVRAM=1; shift ;;
--survey) SOLO_CENSO=1; shift ;;
+ --json) CENSO_JSON=1; shift ;;
--use-free-space) USE_FREE=1; shift ;;
--on-active-root) ON_ACTIVE="${2:?}"; shift 2 ;;
--yes|-y) ASSUME_YES=1; shift ;;
@@ -76,7 +79,6 @@ case "$ON_ACTIVE" in auto|kexec|free-space|abort) ;; *) echo "--on-active-root:
# las GUARDAS, o sea antes de tocar nada. Salen del store por hash vigente; `TOOL_<binario>=` las
# inyecta cuando no hay store (dentro del initramfs de un instalador, p. ej.).
. "$(dirname "$0")/lib/herramientas-desde-store.sh"
-MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1
SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
# Releer la tabla es su propio problema: tras añadir una entrada, el kernel NO se entera solo y el
# device nuevo no aparece. `partx -a` se lo dice; `blockdev --rereadpt` es el ioctl crudo, que falla
@@ -84,11 +86,20 @@ SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
# salen del `util-linux` sellado: en el anfitrión musl no existe ninguno (ni `partprobe`).
PARTX=$(herramienta util-linux usr/sbin/partx partx 2>/dev/null || echo "")
BLOCKDEV=$(herramienta util-linux sbin/blockdev blockdev 2>/dev/null || echo "")
-MMD=$(herramienta mtools usr/bin/mmd mmd) || exit 1
-MCOPY=$(herramienta mtools usr/bin/mcopy mcopy) || exit 1
-MDIR=$(herramienta mtools usr/bin/mdir mdir) || exit 1
-KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage /store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
-[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (set KERNEL=)" >&2; exit 1; }
+# ⚠ **MIRAR no puede costar lo mismo que ESCRIBIR.** Todo lo de abajo —los tres binarios de mtools,
+# mke2fs y el bzImage— sólo hace falta para INSTALAR, y hasta el 2026-09-23 se exigía también para
+# `--survey`: en una máquina ajena, sin store y sin kernel del corpus, el censo moría con «no
+# encuentro el kernel EFI-stub» ANTES de imprimir una sola línea del terreno. Y el censo es
+# justamente lo primero que se corre en una máquina desconocida, donde nada de eso existe todavía.
+# El censo necesita `sfdisk` y `blkid`, nada más.
+if [ -z "$SOLO_CENSO" ]; then
+ MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1
+ MMD=$(herramienta mtools usr/bin/mmd mmd) || exit 1
+ MCOPY=$(herramienta mtools usr/bin/mcopy mcopy) || exit 1
+ MDIR=$(herramienta mtools usr/bin/mdir mdir) || exit 1
+ KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage /store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
+ [ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (set KERNEL=)" >&2; exit 1; }
+fi
HAMMER="${HAMMER:-$ROOT/target/release/takana}"
# ── LAS GUARDAS VAN PRIMERO, Y SON EL PUNTO DE ESTE SCRIPT ─────────────────────────────────────
@@ -228,11 +239,19 @@ discos_del_sistema() {
mib_de_sectores() { echo $(( ${1:-0} / 2048 )); }
-# clasificar <guid-tipo> <fstype> <montaje> → VEREDICTO|ETIQUETA
+# ── mib_minimos — el piso, calculado UNA vez ──────────────────────────────────────────────────
+# `mib_necesarios` mide el árbol y no es gratis (`du -sm` sobre el rootfs); el censo la llamaría una
+# vez por partición. Se cachea en una global, que es la única forma de memoizar en sh.
+mib_minimos() {
+ [ -n "${MIB_MINIMOS:-}" ] || MIB_MINIMOS=$(mib_necesarios)
+ echo "$MIB_MINIMOS"
+}
+
+# clasificar <guid-tipo> <fstype> <montaje> [mib] → VEREDICTO|ETIQUETA
# El veredicto es lo que decide el menú: SOLO_ESP, NO_TOCAR, CANDIDATA, RAIZ_VIVA, EN_USO.
clasificar() {
_cl_guid=$(printf '%s' "${1:-}" | tr 'A-Z' 'a-z')
- _cl_fs="${2:-}"; _cl_mnt="${3:-}"
+ _cl_fs="${2:-}"; _cl_mnt="${3:-}"; _cl_mib="${4:-0}"
case "$_cl_guid" in
"$GUID_ESP") echo "SOLO_ESP|ESP (partición de arranque EFI)"; return ;;
"$GUID_MSR") echo "NO_TOCAR|reservada de Microsoft"; return ;;
@@ -263,10 +282,20 @@ clasificar() {
echo "EN_USO|montada en $_cl_mnt"; return
fi
case "$_cl_fs" in
- ext2|ext3|ext4|xfs|btrfs|f2fs) echo "CANDIDATA|Linux ($_cl_fs) sin montar"; return ;;
- "") echo "CANDIDATA|sin filesystem reconocible"; return ;;
+ ext2|ext3|ext4|xfs|btrfs|f2fs) _cl_q="Linux ($_cl_fs) sin montar" ;;
+ "") _cl_q="sin filesystem reconocible" ;;
+ *) echo "NO_TOCAR|$_cl_fs (desconocido para este instalador)"; return ;;
esac
- echo "NO_TOCAR|$_cl_fs (desconocido para este instalador)"
+ # ⚠ **Una candidata que no entra no es una candidata.** Sin esta puerta el censo de este mismo
+ # hub ofrecía `/dev/sda1` —2 MiB, sin filesystem, el hueco de arranque— como «✓ se puede usar»:
+ # el veredicto sólo miraba QUÉ hay dentro y nunca CUÁNTO cabe. Elegirla formatea algo real y
+ # muere a mitad de la copia, que es el peor momento para enterarse. El piso es el mismo número
+ # con el que se talla un hueco (`mib_necesarios`), así que las dos puertas no pueden divergir.
+ _cl_min=$(mib_minimos)
+ if [ "${_cl_mib:-0}" -lt "$_cl_min" ] 2>/dev/null; then
+ echo "NO_TOCAR|$_cl_q, pero son ${_cl_mib} M y hacen falta al menos ${_cl_min} M"; return
+ fi
+ echo "CANDIDATA|$_cl_q"
}
# censo [--solo-candidatas] → una línea por partición:
@@ -301,13 +330,16 @@ censo() {
_ce_mn=$(awk -v d="$_ce_dev" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true)
# La raíz viva se decide por número de dispositivo, no por el nombre de /proc/mounts.
if es_la_raiz_viva "$_ce_dev"; then _ce_mn="/"; fi
- _ce_cl=$(clasificar "$_ce_ty" "$_ce_fs" "$_ce_mn")
+ _ce_cl=$(clasificar "$_ce_ty" "$_ce_fs" "$_ce_mn" "$(mib_de_sectores "$_ce_sz")")
_ce_ver="${_ce_cl%%|*}"; _ce_exp="${_ce_cl#*|}"
[ "$_ce_solo" = "--solo-candidatas" ] && [ "$_ce_ver" != "CANDIDATA" ] && continue
echo "$_ce_dev|$_ce_uu|$(mib_de_sectores "$_ce_sz")|$_ce_fs|$_ce_lb|$_ce_mn|$_ce_ver|$_ce_exp"
done
# El espacio SIN ASIGNAR es un candidato más, y el único que no destruye nada.
- "$SFDISK" -F "$_ce_d" 2>/dev/null | awk -v d="$_ce_d" 'NF==4 && $1 ~ /^[0-9]+$/ {print d"|-|"int($3/2048)"|-|-|-|LIBRE|espacio sin asignar en "d}'
+ "$SFDISK" -F "$_ce_d" 2>/dev/null | awk -v d="$_ce_d" -v min="$(mib_minimos)" \
+ 'NF==4 && $1 ~ /^[0-9]+$/ { m=int($3/2048);
+ if (m >= min) print d"|-|"m"|-|-|-|LIBRE|espacio sin asignar en "d;
+ else print d"|-|"m"|-|-|-|NO_TOCAR|hueco sin asignar en "d", pero son "m" M y hacen falta al menos "min" M" }'
done
}
@@ -330,9 +362,58 @@ mostrar_censo() {
esac
printf " %-3s %-16s %6s M %-11s %s\n" "$_mc_n" "$_d" "$_m" "${_fs:-—}" "$_mc_marca"
printf " %s%s\n" "$_exp" "$( [ -n "$_lb" ] && [ "$_lb" != "-" ] && printf ' · etiqueta «%s»' "$_lb")"
- [ -n "$_u" ] && [ "$_u" != "-" ] && printf " PARTUUID=%s\n" "$_u"
+ # ⚠ **Un `a && b && c` como ÚLTIMO comando de un bucle decide el estado de salida del
+ # script entero.** Con `set -e`, si la última fila del censo no tiene PARTUUID —un disco sin
+ # tabla, un hueco libre— esta línea daba 1, el `while` daba 1, la tubería daba 1 y
+ # `mostrar_censo` MORÍA ahí: el separador de cierre no se imprimía y `--survey` salía con 1
+ # habiendo funcionado. Medido en este hub el 2026-09-23, donde la última fila es `/dev/sdc`.
+ # Un `if` no propaga nada; el `&&` sí.
+ if [ -n "$_u" ] && [ "$_u" != "-" ]; then printf " PARTUUID=%s\n" "$_u"; fi
done
echo "───────────────────────────────────────────────────────────────────────────────"
+ # Un censo VACÍO no es un error, pero callarlo se lee como «no hay nada que hacer acá». Pasa de
+ # verdad: dentro de esta jaula `/dev` no tiene un solo dispositivo de bloque.
+ if [ -z "$({ if [ -n "$_mc_f" ]; then cat "$_mc_f"; else censo; fi; } | grep -c . 2>/dev/null | grep -v '^0$')" ]; then
+ echo " (no veo NINGÚN disco desde acá — ¿estás dentro de un contenedor, o sin privilegios?)"
+ fi
+}
+
+# ── censo_json — el MISMO censo, en JSON. Es el CONTRATO con cualquier cara gráfica ───────────
+#
+# ⚠ **Existe para que no haya un segundo censo.** La tentación al poner una interfaz encima es que
+# la interfaz lea `sfdisk`/`blkid` por su cuenta y clasifique ella; ahí es donde los dos divergen y
+# uno de los dos empieza a ofrecer lo que el otro protege — y el que se equivoca formatea. El SDD 28
+# §8 ya nombra ese riesgo («un motor, tres frentes»). Entonces: UN censo, UNA clasificación, y las
+# caras consumen esto. Si `veredicto` cambia acá, cambia para todas a la vez.
+#
+# El campo `usable` es la lectura de una sola pregunta —¿se puede instalar acá?— para que una cara
+# no tenga que reimplementar el `case` de los veredictos y equivocarse en el default: lo que no
+# reconozca NO es usable.
+jstr() {
+ printf '"%s"' "$(printf '%s' "${1:-}" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/ /g' | tr -d '\000-\010\013\014\016-\037')"
+}
+censo_json() {
+ _cj_f="${1:-}"
+ printf '{\n'
+ printf ' "version": 1,\n'
+ printf ' "raiz_viva": %s,\n' "$( [ -n "${RAIZ_MAJMIN:-}" ] && jstr "$RAIZ_MAJMIN" || printf 'null' )"
+ printf ' "particiones": [\n'
+ _cj_pri=1
+ { if [ -n "$_cj_f" ]; then cat "$_cj_f"; else censo; fi; } | while IFS='|' read -r _d _u _m _fs _lb _mn _ver _exp; do
+ [ -n "$_d" ] || continue
+ case "$_ver" in CANDIDATA|LIBRE|RAIZ_VIVA) _cj_us=true ;; *) _cj_us=false ;; esac
+ [ "$_cj_pri" = 1 ] || printf ',\n'
+ _cj_pri=0
+ printf ' {"dev": %s, "partuuid": %s, "mib": %s, "fs": %s, "etiqueta": %s, "montaje": %s, "veredicto": %s, "usable": %s, "explicacion": %s}' \
+ "$(jstr "$_d")" \
+ "$( [ "${_u:--}" = "-" ] && printf 'null' || jstr "$_u" )" \
+ "${_m:-0}" \
+ "$( [ "${_fs:--}" = "-" ] || [ -z "${_fs:-}" ] && printf 'null' || jstr "$_fs" )" \
+ "$( [ "${_lb:--}" = "-" ] || [ -z "${_lb:-}" ] && printf 'null' || jstr "$_lb" )" \
+ "$( [ "${_mn:--}" = "-" ] || [ -z "${_mn:-}" ] && printf 'null' || jstr "$_mn" )" \
+ "$(jstr "$_ver")" "$_cj_us" "$(jstr "$_exp")"
+ done
+ printf '\n ]\n}\n'
}
# ── partuuid_de <dev> — la identidad estable de una partición, leída de la TABLA ──────────────
@@ -495,7 +576,7 @@ crear_en_hueco() {
# `--survey`: mirar y no tocar. Es el comando con el que se empieza en una máquina desconocida.
if [ -n "$SOLO_CENSO" ]; then
- mostrar_censo
+ if [ -n "$CENSO_JSON" ]; then censo_json; else mostrar_censo; fi
exit 0
fi
@@ -557,6 +638,18 @@ if es_bloque "$DEST"; then
# 4) NO PUEDE SER LA ESP que vamos a reusar.
[ -n "$ESP" ] && [ "$(readlink -f "$DEST")" = "$(readlink -f "$ESP")" ] && {
echo "takana-install-partition: --root-part y --esp son la MISMA partición" >&2; exit 1; }
+ # 5) ¿ENTRA? Esto AVISA y no aborta, a propósito. El censo esconde del menú lo que no llega al
+ # piso de `mib_necesarios` —que es el mismo con el que se talla un hueco, generoso a
+ # propósito—, pero quien nombra una partición con `--root-part` la está nombrando a mano y
+ # puede saber que su destino justo entra: el banco de pruebas instala en 1200 MiB y funciona.
+ # Abortar acá rechazaría configuraciones que andan; callarse dejaría el fallo para la mitad
+ # de la copia, con el destino ya formateado. Avisar es lo único que no miente.
+ _mib_dest=$(mib_de_sectores "$(cat "/sys/class/block/$_n/size" 2>/dev/null || echo 0)")
+ _mib_pide=$(mib_minimos)
+ if [ "${_mib_dest:-0}" -gt 0 ] && [ "$_mib_dest" -lt "$_mib_pide" ] 2>/dev/null; then
+ echo " ⚠ $DEST son ${_mib_dest} MiB y el sistema pide ~${_mib_pide} MiB con holgura." >&2
+ echo " Sigo porque lo nombraste vos; si no entra, la copia falla con el destino ya formateado." >&2
+ fi
else
[ -f "$DEST" ] || { echo "takana-install-partition: $DEST no existe (ni bloque ni fichero)" >&2; exit 1; }
fi