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 188Rust 55Shell 30Markdown 281211 Definitions

⎇ a6a9d59e4f0d354cf2c9066538c6fa7016fc6721 scripts/licencias.sh Shell · 150 lines · 9.2 KB History raw

licencias: 936 de 1141 (82%) — detección por evidencia, con sus dos imprecisiones declaradas a6a9d59e4 · sergio ·

1#!/usr/bin/env bash
2# licencias.sh — mide y puebla el campo `license` de las recetas. SDD 19 §2.1.
3#
4# ── EL PROBLEMA ─────────────────────────────────────────────────────────────────────────────────
5# Medido el 2026-08-07: **0 de 1141 recetas** declaraban licencia. (Un informe anterior dijo «5 de
6# 771»; los dos números estaban mal. Los «5» eran falsos positivos de `grep license`: el paquete
7# `addlicense`, el paquete `cargo-bundle-licenses`, una línea `install ... /usr/share/licenses/` y un
8# comentario. Y las recetas son 1141, no 771. Lección: contar con `grep -l <palabra>` sobre TOML
9# cuenta comentarios y nombres, no campos.)
10#
11# Sin ese dato la distro no puede cumplir la GPL cuando redistribuye binarios: la obligación es
12# acompañar el binario del texto de la licencia y poder entregar la fuente correspondiente. Es
13# BLOQUEANTE para publicar, y es la deuda que más barato sale pagar temprano: a 1141 recetas es un
14# rato de trabajo, a 2600 es un proyecto.
15#
16# ── POR QUÉ ESTO NO RE-HASHEA NADA (lo que hace la deuda pagable) ───────────────────────────────
17# `Recipe::hash_inputs` es una LISTA BLANCA: sólo entran source, compiler, target, link, patches,
18# flags, phases y deps. `license` no entra, igual que `evidence` y `slots`. O sea que se puede poblar
19# en las 1141 recetas YA SELLADAS sin mover un solo ArtifactHash ni invalidar el baseline de
20# reproducibilidad. Está clavado con un test: `licencia_round_trip_y_no_afecta_el_hash`.
21#
22# ── DÓNDE VA LA LÍNEA (trampa de TOML, cuesta una tarde si se ignora) ───────────────────────────
23# En TOML una clave suelta después de un `[table]` pertenece a ESA tabla. `license = "..."` puesto al
24# final del fichero acaba dentro de `[deps]` y **se pierde en silencio**, porque serde ignora los
25# campos que no conoce: no hay error, simplemente no está. Va ARRIBA, junto a `name` y `version`.
26#
27# ── LA REGLA DE HONESTIDAD: NO SE ADIVINA ───────────────────────────────────────────────────────
28# Declarar mal una licencia es PEOR que no declararla — convierte un hueco visible en una afirmación
29# falsa sobre la que alguien va a construir. Por eso este script no infiere de la URL ni del nombre.
30# Sólo puebla desde `docs/licencias-conocidas.tsv`, una tabla curada y auditable a mano, y desde los
31# ficheros COPYING/LICENSE de árboles de fuente realmente extraídos. Lo que no tiene evidencia se
32# queda vacío y SE CUENTA. Un hueco contado es deuda; un hueco rellenado a ojo es una mentira.
33#
34# ── EL ARREGLO ESTRUCTURAL, QUE ES OTRO TICKET ──────────────────────────────────────────────────
35# Poblar a mano tapa el agujero de hoy pero no impide que vuelva a abrirse: cada receta nueva nace
36# sin licencia. El cierre de verdad es capturarla donde ya pasa el dato gratis — hammer YA descarga y
37# extrae cada tarball al construir, así que la fase de fetch puede guardar el COPYING/LICENSE del
38# árbol junto al artefacto, sin descargas extra. Y el texto de la licencia dentro del paquete conviene
39# inyectarlo en `hammer pack`, NO en la fase install de la receta: pack es aguas abajo del
40# ArtifactHash, así que cumplir la GPL no cuesta reconstruir el corpus. Ver SDD 19.
41#
42# Uso:  scripts/licencias.sh                 informe de cobertura (default, no toca nada)
43#       scripts/licencias.sh --sembrar       escribe las licencias de la tabla curada
44#       scripts/licencias.sh --faltan        lista las recetas sin licencia, para trabajarlas
45set -uo pipefail
46ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
47TABLA="${TABLA:-docs/licencias-conocidas.tsv}"
48MODO="${1:-informe}"
49
50recetas() { ls recipes/*.toml recipes/incoming-*/*.toml 2>/dev/null; }
51
52# Un campo `license` de verdad: clave a principio de línea, ANTES del primer `[table]`.
53# (Ver arriba: después de un `[table]` la clave pertenece a la tabla y no la lee el parser.)
54tiene_licencia() {
55  awk '/^\[/{exit} /^[[:space:]]*license[[:space:]]*=/{print "si"; exit}' "$1" | grep -q si
56}
57
58licencia_de() {
59  awk -F'"' '/^\[/{exit} /^[[:space:]]*license[[:space:]]*=/{print $2; exit}' "$1"
60}
61
62case "$MODO" in
63# ── sembrar: escribir las licencias de la tabla curada ──────────────────────────────────────────
64--sembrar)
65  [ -s "$TABLA" ] || { echo "!!  falta la tabla curada: $TABLA"; exit 1; }
66  puestas=0; ya=0; sin_receta=0
67  # TRES campos, no dos: `licencias-detectadas.tsv` añade una tercera columna con el `owner/repo`
68  # consultado (la procedencia, para poder auditar). Leyendo sólo dos variables, `lic` se tragaría
69  # también el slug y se escribiría `license = "MIT<TAB>owner/repo"` en la receta — basura silenciosa,
70  # porque nada valida el contenido. `_resto` la absorbe.
71  while IFS=$'\t' read -r pkg lic _resto; do
72    case "$pkg" in ''|'#'*) continue ;; esac
73    [ -n "${lic:-}" ] || continue
74    hallada=0
75    for f in recipes/"$pkg".toml recipes/incoming-*/"$pkg".toml; do
76      [ -f "$f" ] || continue
77      hallada=1
78      if tiene_licencia "$f"; then ya=$((ya+1)); continue; fi
79      # Insertar DESPUÉS de la línea `version = ...`, que está en la raíz del TOML y antes de
80      # cualquier `[table]`. Es el único sitio seguro (ver la trampa de TOML en la cabecera).
81      if grep -qE '^version[[:space:]]*=' "$f"; then
82        awk -v L="license = \"$lic\"" '
83          {print}
84          !done && /^version[[:space:]]*=/ {print L; done=1}
85        ' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
86        puestas=$((puestas+1))
87      else
88        echo "  ??  $pkg: sin línea `version` en la raíz, no toco el fichero"
89      fi
90    done
91    [ "$hallada" = 0 ] && sin_receta=$((sin_receta+1))
92  done < "$TABLA"
93  echo "==> sembradas $puestas · ya tenían $ya · en la tabla pero sin receta $sin_receta"
94  echo "==> verificá que el hash NO se movió:  ./scripts/store-gc.sh   (debe dar los mismos vigentes)"
95  ;;
96
97# ── faltan: la lista de trabajo ─────────────────────────────────────────────────────────────────
98--faltan)
99  recetas() { ls recipes/*.toml recipes/incoming-*/*.toml 2>/dev/null; }
100  for f in $(recetas); do tiene_licencia "$f" || basename "$f" .toml; done
101  ;;
102
103# ── revisar: las que están declaradas pero con un SPDX AMBIGUO ─────────────────────────────────
104# La detección por API de GitHub deja dos imprecisiones conocidas, y esto las hace contables en vez
105# de esconderlas:
106#
107#  1. SPDX OBSOLETOS Y AMBIGUOS. GitHub devuelve `GPL-3.0`, `GPL-2.0`, `LGPL-2.1`, `LGPL-3.0`,
108#     `AGPL-3.0` — identificadores que SPDX declaró obsoletos **precisamente porque son ambiguos**:
109#     no distinguen «sólo esta versión» (`-only`) de «esta o posterior» (`-or-later`), y esa
110#     diferencia tiene efectos legales reales sobre con qué se puede combinar el paquete.
111#     **NO se normalizan a ciegas**: mapear `GPL-3.0` → `GPL-3.0-or-later` sería inventar el dato que
112#     falta, que es justo lo que este fichero prohíbe. Se marcan para que un humano las resuelva
113#     mirando la cabecera del fuente.
114#
115#  2. LICENCIAS DOBLES COLAPSADAS. La API devuelve UNA licencia; muchos proyectos Rust son
116#     «MIT OR Apache-2.0» y sólo se recoge una de las dos (p.ej. `fd` quedó como Apache-2.0). No es
117#     falso —Apache-2.0 sí es una de las opciones concedidas, y cumplir una basta— pero es
118#     INCOMPLETO: pierde una opción que el autor otorgó. Se resuelve con el cierre estructural
119#     (leer el `license` del Cargo.toml en la fase de fetch), no a mano.
120--revisar)
121  n=0
122  for f in $(recetas); do
123    l=$(licencia_de "$f")
124    case "$l" in
125      GPL-[0-9]*|LGPL-[0-9]*|AGPL-[0-9]*)
126        case "$l" in *-only|*-or-later|*OR*|*AND*|*WITH*) ;; *) echo "$(basename "$f" .toml)	$l"; n=$((n+1));; esac ;;
127    esac
128  done
129  echo "==> $n recetas con SPDX ambiguo (falta -only / -or-later). Resolver mirando el fuente." >&2
130  ;;
131
132# ── informe (default) ───────────────────────────────────────────────────────────────────────────
133*)
134  tot=0; con=0
135  : > /tmp/.lic-cuenta.$$
136  for f in $(recetas); do
137    tot=$((tot+1))
138    if tiene_licencia "$f"; then con=$((con+1)); licencia_de "$f" >> /tmp/.lic-cuenta.$$; fi
139  done
140  pct=0; [ "$tot" -gt 0 ] && pct=$((con*100/tot))
141  echo "==> licencias declaradas: $con de $tot recetas (${pct}%)"
142  if [ "$con" -gt 0 ]; then
143    echo "==> reparto:"
144    sort /tmp/.lic-cuenta.$$ | uniq -c | sort -rn | head -25 | sed 's/^/    /'
145  fi
146  rm -f /tmp/.lic-cuenta.$$
147  [ "$con" -lt "$tot" ] && echo "==> faltan $((tot-con)). Lista:  scripts/licencias.sh --faltan"
148  echo "==> recordá: NO se rellena a ojo. Sin evidencia, se deja vacío y se cuenta (ver cabecera)."
149  ;;
150esac