Files
hammer/scripts/licencias.sh
T
sergioandClaude Opus 5 c8d1357f06 licencias: la declaración del autor manda — y arreglo 5 recetas que ROMPÍ y commiteé
Dos cosas: una mejora de calidad y un fallo mío que hay que contar entero.

═══ EL FALLO: escribí un error JSON dentro de 5 recetas y las commiteé rotas ═══
En `a6a9d59`, cinco recetas quedaron con
    license = "{"message":"Not Found","documentation_url":"...","status":"404"}"
Causa: cuando el repo da 404 (renombrado, borrado, privado), `gh api` escribe el CUERPO DEL
ERROR en stdout y el filtro `--jq` falla, así que la variable queda valiendo el JSON. Mi
`case` sólo descartaba ""/null/NOASSERTION/other ⇒ el JSON pasó como si fuera una licencia.
Las comillas ROMPEN el TOML, y esas 5 recetas dejaron de parsear y de tener ArtifactHash.

No lo vio nadie leyendo: lo cazó la comparación de hashes, al detectar que `cargo-sort`
había cambiado. Afectadas: cargo-sort, cargo-audit, cargo-binstall, waybackurls, usbutils.

La lección no es «se me pasó un caso», es que **validé los «no sé» conocidos en vez de la
FORMA del dato**. Ahora se acepta sólo lo que tiene forma de expresión SPDX, y la barrera
está en los DOS sitios que escriben (detectar y sembrar), más un GUARDIÁN en el informe que
sale ≠0 si alguna licencia tiene forma imposible. Un fallo aguas arriba disfrazado de dato
hay que verlo sin buscarlo.

Y de paso normalizadas 11 licencias con la barra antigua de Cargo (`MIT/Apache-2.0`), que
no es SPDX válido. Traducir la barra a OR no es inferir: Cargo documentó esa equivalencia.

═══ LA MEJORA: `scripts/licencias-cargo.sh` ═══
Lee la licencia que el AUTOR declara en su Cargo.toml, pidiéndola AL TAG QUE LA RECETA
PINEA (`?ref=v<version>`, con HEAD como último recurso y anotando cuál se usó). 176
obtenidas, 167 al tag exacto. Es la fuente más autoritativa de las que usamos y corrige los
dos defectos de la detección por API de una vez:
 · las licencias DOBLES dejan de colapsar: `fd` pasa de `Apache-2.0` a `MIT OR Apache-2.0`,
   `ripgrep` a `Unlicense OR MIT`, `bat` a `MIT OR Apache-2.0`;
 · los SPDX AMBIGUOS caen de 71 a 55, porque el autor sí escribe -only/-or-later.
66 corregidas, 7 nuevas. Y aparecieron cosas que la detección había perdido: `eza` es
EUPL-1.2, una copyleft europea que se habría empaquetado creyendo otra cosa.

Jerarquía de evidencia, escrita en la cabecera del script: nombre (prohibido) < familia/URL
de fuente < API de licencias de GitHub < Cargo.toml del autor.

997 de 1141 (87%). Verificación COMPLETA sobre las 77 recetas tocadas —no una muestra—:
0 hashes cambiados, y 5 que ahora parsean y en HEAD no.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:51:09 -04:00

189 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# licencias.sh — mide y puebla el campo `license` de las recetas. SDD 19 §2.1.
#
# ── EL PROBLEMA ─────────────────────────────────────────────────────────────────────────────────
# Medido el 2026-08-07: **0 de 1141 recetas** declaraban licencia. (Un informe anterior dijo «5 de
# 771»; los dos números estaban mal. Los «5» eran falsos positivos de `grep license`: el paquete
# `addlicense`, el paquete `cargo-bundle-licenses`, una línea `install ... /usr/share/licenses/` y un
# comentario. Y las recetas son 1141, no 771. Lección: contar con `grep -l <palabra>` sobre TOML
# cuenta comentarios y nombres, no campos.)
#
# Sin ese dato la distro no puede cumplir la GPL cuando redistribuye binarios: la obligación es
# acompañar el binario del texto de la licencia y poder entregar la fuente correspondiente. Es
# BLOQUEANTE para publicar, y es la deuda que más barato sale pagar temprano: a 1141 recetas es un
# rato de trabajo, a 2600 es un proyecto.
#
# ── POR QUÉ ESTO NO RE-HASHEA NADA (lo que hace la deuda pagable) ───────────────────────────────
# `Recipe::hash_inputs` es una LISTA BLANCA: sólo entran source, compiler, target, link, patches,
# flags, phases y deps. `license` no entra, igual que `evidence` y `slots`. O sea que se puede poblar
# en las 1141 recetas YA SELLADAS sin mover un solo ArtifactHash ni invalidar el baseline de
# reproducibilidad. Está clavado con un test: `licencia_round_trip_y_no_afecta_el_hash`.
#
# ── DÓNDE VA LA LÍNEA (trampa de TOML, cuesta una tarde si se ignora) ───────────────────────────
# En TOML una clave suelta después de un `[table]` pertenece a ESA tabla. `license = "..."` puesto al
# final del fichero acaba dentro de `[deps]` y **se pierde en silencio**, porque serde ignora los
# campos que no conoce: no hay error, simplemente no está. Va ARRIBA, junto a `name` y `version`.
#
# ── LA REGLA DE HONESTIDAD: NO SE ADIVINA ───────────────────────────────────────────────────────
# Declarar mal una licencia es PEOR que no declararla — convierte un hueco visible en una afirmación
# falsa sobre la que alguien va a construir. Por eso este script no infiere de la URL ni del nombre.
# Sólo puebla desde `docs/licencias-conocidas.tsv`, una tabla curada y auditable a mano, y desde los
# ficheros COPYING/LICENSE de árboles de fuente realmente extraídos. Lo que no tiene evidencia se
# queda vacío y SE CUENTA. Un hueco contado es deuda; un hueco rellenado a ojo es una mentira.
#
# ── EL ARREGLO ESTRUCTURAL, QUE ES OTRO TICKET ──────────────────────────────────────────────────
# Poblar a mano tapa el agujero de hoy pero no impide que vuelva a abrirse: cada receta nueva nace
# sin licencia. El cierre de verdad es capturarla donde ya pasa el dato gratis — hammer YA descarga y
# extrae cada tarball al construir, así que la fase de fetch puede guardar el COPYING/LICENSE del
# árbol junto al artefacto, sin descargas extra. Y el texto de la licencia dentro del paquete conviene
# inyectarlo en `hammer pack`, NO en la fase install de la receta: pack es aguas abajo del
# ArtifactHash, así que cumplir la GPL no cuesta reconstruir el corpus. Ver SDD 19.
#
# Uso: scripts/licencias.sh informe de cobertura (default, no toca nada)
# scripts/licencias.sh --sembrar escribe las licencias de la tabla curada
# scripts/licencias.sh --faltan lista las recetas sin licencia, para trabajarlas
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
TABLA="${TABLA:-docs/licencias-conocidas.tsv}"
MODO="${1:-informe}"
recetas() { ls recipes/*.toml recipes/incoming-*/*.toml 2>/dev/null; }
# Un campo `license` de verdad: clave a principio de línea, ANTES del primer `[table]`.
# (Ver arriba: después de un `[table]` la clave pertenece a la tabla y no la lee el parser.)
tiene_licencia() {
awk '/^\[/{exit} /^[[:space:]]*license[[:space:]]*=/{print "si"; exit}' "$1" | grep -q si
}
licencia_de() {
awk -F'"' '/^\[/{exit} /^[[:space:]]*license[[:space:]]*=/{print $2; exit}' "$1"
}
case "$MODO" in
# ── sembrar: escribir las licencias de la tabla curada ──────────────────────────────────────────
--sembrar)
[ -s "$TABLA" ] || { echo "!! falta la tabla curada: $TABLA"; exit 1; }
puestas=0; ya=0; sin_receta=0; corregidas=0
# TRES campos, no dos: `licencias-detectadas.tsv` añade una tercera columna con el `owner/repo`
# consultado (la procedencia, para poder auditar). Leyendo sólo dos variables, `lic` se tragaría
# también el slug y se escribiría `license = "MIT<TAB>owner/repo"` en la receta — basura silenciosa,
# porque nada valida el contenido. `_resto` la absorbe.
while IFS=$'\t' read -r pkg lic _resto; do
case "$pkg" in ''|'#'*) continue ;; esac
[ -n "${lic:-}" ] || continue
# ÚLTIMA BARRERA antes de tocar una receta: la licencia tiene que TENER FORMA de expresión SPDX.
# Sin esto, cualquier fallo de una fuente aguas arriba se escribe como si fuera un dato — y el
# 2026-08-07 pasó: un cuerpo de error JSON de la API de GitHub acabó dentro de 5 recetas, con
# comillas, rompiendo el TOML y dejando de calcularse su ArtifactHash. Validar en el sitio que
# ESCRIBE es lo que corta la clase entera de fallo, no sólo el caso conocido.
case "$lic" in
*[!A-Za-z0-9.+\ -]*) echo " ?? $pkg: descarto licencia con forma inválida: $(printf '%.40s' "$lic")"; continue ;;
esac
hallada=0
for f in recipes/"$pkg".toml recipes/incoming-*/"$pkg".toml; do
[ -f "$f" ] || continue
hallada=1
if tiene_licencia "$f"; then
# FORZAR=1: reemplazar lo ya declarado. Sólo tiene sentido con una fuente MÁS autoritativa
# que la que puso el valor actual — el caso real es `licencias-cargo.tsv`, la declaración
# del propio autor, que corrige los dos defectos de la detección por API (SPDX ambiguo y
# licencias dobles colapsadas). Con la tabla curada NO se usa: pisaría a mano lo verificado.
if [ "${FORZAR:-0}" = 1 ] && [ "$(licencia_de "$f")" != "$lic" ]; then
esc=$(printf '%s' "$lic" | sed 's/[&/\]/\\&/g')
sed -i "0,/^[[:space:]]*license[[:space:]]*=.*/s//license = \"$esc\"/" "$f"
corregidas=$((corregidas+1))
else
ya=$((ya+1))
fi
continue
fi
# Insertar DESPUÉS de la línea `version = ...`, que está en la raíz del TOML y antes de
# cualquier `[table]`. Es el único sitio seguro (ver la trampa de TOML en la cabecera).
if grep -qE '^version[[:space:]]*=' "$f"; then
awk -v L="license = \"$lic\"" '
{print}
!done && /^version[[:space:]]*=/ {print L; done=1}
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
puestas=$((puestas+1))
else
echo " ?? $pkg: sin línea `version` en la raíz, no toco el fichero"
fi
done
[ "$hallada" = 0 ] && sin_receta=$((sin_receta+1))
done < "$TABLA"
echo "==> sembradas $puestas · corregidas $corregidas · ya tenían $ya · sin receta $sin_receta"
echo "==> verificá que el hash NO se movió: ./scripts/store-gc.sh (debe dar los mismos vigentes)"
;;
# ── faltan: la lista de trabajo ─────────────────────────────────────────────────────────────────
--faltan)
recetas() { ls recipes/*.toml recipes/incoming-*/*.toml 2>/dev/null; }
for f in $(recetas); do tiene_licencia "$f" || basename "$f" .toml; done
;;
# ── revisar: las que están declaradas pero con un SPDX AMBIGUO ─────────────────────────────────
# La detección por API de GitHub deja dos imprecisiones conocidas, y esto las hace contables en vez
# de esconderlas:
#
# 1. SPDX OBSOLETOS Y AMBIGUOS. GitHub devuelve `GPL-3.0`, `GPL-2.0`, `LGPL-2.1`, `LGPL-3.0`,
# `AGPL-3.0` — identificadores que SPDX declaró obsoletos **precisamente porque son ambiguos**:
# no distinguen «sólo esta versión» (`-only`) de «esta o posterior» (`-or-later`), y esa
# diferencia tiene efectos legales reales sobre con qué se puede combinar el paquete.
# **NO se normalizan a ciegas**: mapear `GPL-3.0` → `GPL-3.0-or-later` sería inventar el dato que
# falta, que es justo lo que este fichero prohíbe. Se marcan para que un humano las resuelva
# mirando la cabecera del fuente.
#
# 2. LICENCIAS DOBLES COLAPSADAS. La API devuelve UNA licencia; muchos proyectos Rust son
# «MIT OR Apache-2.0» y sólo se recoge una de las dos (p.ej. `fd` quedó como Apache-2.0). No es
# falso —Apache-2.0 sí es una de las opciones concedidas, y cumplir una basta— pero es
# INCOMPLETO: pierde una opción que el autor otorgó. Se resuelve con el cierre estructural
# (leer el `license` del Cargo.toml en la fase de fetch), no a mano.
--revisar)
n=0
for f in $(recetas); do
l=$(licencia_de "$f")
case "$l" in
GPL-[0-9]*|LGPL-[0-9]*|AGPL-[0-9]*)
case "$l" in *-only|*-or-later|*OR*|*AND*|*WITH*) ;; *) echo "$(basename "$f" .toml) $l"; n=$((n+1));; esac ;;
esac
done
echo "==> $n recetas con SPDX ambiguo (falta -only / -or-later). Resolver mirando el fuente." >&2
;;
# ── informe (default) ───────────────────────────────────────────────────────────────────────────
*)
tot=0; con=0
: > /tmp/.lic-cuenta.$$
for f in $(recetas); do
tot=$((tot+1))
if tiene_licencia "$f"; then con=$((con+1)); licencia_de "$f" >> /tmp/.lic-cuenta.$$; fi
done
pct=0; [ "$tot" -gt 0 ] && pct=$((con*100/tot))
echo "==> licencias declaradas: $con de $tot recetas (${pct}%)"
# ── GUARDIÁN: licencias con FORMA INVÁLIDA ───────────────────────────────────────────────────
# El 2026-08-07 se escribió el cuerpo de un error JSON de la API de GitHub dentro de 5 recetas
# (`license = "{"message":"Not Found",...}"`). Las comillas rompen el TOML ⇒ esas recetas dejaron
# de parsear y de tener ArtifactHash, y así se COMMITEARON. No lo vio nadie leyendo: lo cazó la
# comparación de hashes. Por eso el informe lo comprueba SIEMPRE y sale distinto de cero: un dato
# con forma imposible es un fallo aguas arriba disfrazado de dato, y hay que verlo sin buscarlo.
malas=0
for f in $(recetas); do
l=$(licencia_de "$f")
[ -z "$l" ] && continue
case "$l" in *[!A-Za-z0-9.+\ -]*) echo "!! FORMA INVÁLIDA en $f: $(printf '%.50s' "$l")"; malas=$((malas+1));; esac
done
if [ "$malas" -gt 0 ]; then
echo "!! $malas recetas con licencia de forma imposible — probablemente TOML roto. Arreglar YA."
exit 1
fi
if [ "$con" -gt 0 ]; then
echo "==> reparto:"
sort /tmp/.lic-cuenta.$$ | uniq -c | sort -rn | head -25 | sed 's/^/ /'
fi
rm -f /tmp/.lic-cuenta.$$
[ "$con" -lt "$tot" ] && echo "==> faltan $((tot-con)). Lista: scripts/licencias.sh --faltan"
echo "==> recordá: NO se rellena a ojo. Sin evidencia, se deja vacío y se cuenta (ver cabecera)."
;;
esac