Files
hammer/scripts/licencias.sh
T
sergioandClaude Opus 5 a6a9d59e4f licencias: 936 de 1141 (82%) — detección por evidencia, con sus dos imprecisiones declaradas
Esta mañana eran 0. Tras la tabla curada (228) y este paso, 936. Ninguna receta se
re-hasheó: verificado en 30 de las 708 tocadas, 30 hashes idénticos, 0 cambiados.

DE DÓNDE SALE EL DATO, Y POR QUÉ NO ES ADIVINAR. `scripts/licencias-detectar.sh` consulta
la API /repos/{o}/{r}/license de GitHub para las 673 recetas cuya fuente vive allí. Eso
devuelve el resultado de DETECTAR el fichero LICENSE que el repo tiene de verdad
(licensee), no una etiqueta escrita a mano en una web: es la misma evidencia que veríamos
abriendo el tarball, obtenida sin bajar 673 tarballs por un enlace de 8 Mbps. 610 con SPDX
definido; las 63 que GitHub marca NOASSERTION/other se DESCARTAN — un «no sé» de la fuente
se propaga como hueco, no se redondea a una licencia plausible.

Las familias no-GitHub van curadas por la URL DE FUENTE, que es la evidencia que el nombre
no da. Y ahí cometí el error simétrico al que este mismo fichero advertía: había puesto
`knighttime` entre las «herramientas Go» POR SU NOMBRE, y su tarball sale de
download.kde.org — es un Framework de KDE, LGPL. Descartar por nombre falla igual que
aceptar por nombre. Corregido en la cabecera.

LAS DOS IMPRECISIONES, contables con `licencias.sh --revisar` en vez de escondidas:

 1. SPDX OBSOLETOS Y AMBIGUOS (71 recetas). GitHub devuelve `GPL-3.0`, `LGPL-2.1`,
    `AGPL-3.0`… identificadores que SPDX declaró obsoletos PRECISAMENTE porque no
    distinguen `-only` de `-or-later`, y esa diferencia decide 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. Quedan marcadas para resolver mirando el fuente.

 2. LICENCIAS DOBLES COLAPSADAS. La API devuelve UNA sola licencia y muchos proyectos Rust
    son «MIT OR Apache-2.0» (p.ej. `fd` quedó como Apache-2.0). No es falso —cumplir una de
    las opciones concedidas basta— pero es incompleto. Lo resuelve el cierre estructural:
    leer el campo `license` del Cargo.toml en la fase de fetch.

Y un fallo que habría escrito basura en silencio: `licencias-detectadas.tsv` tiene TRES
columnas (añade el owner/repo consultado, para poder auditar) y el sembrador leía dos, así
que la licencia se habría llevado pegado el slug — `license = "MIT<TAB>owner/repo"`, sin
que nada lo validara. Arreglado antes de sembrar.

Quedan 205 sin licencia y 71 por desambiguar.

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

151 lines
9.2 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
# 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
hallada=0
for f in recipes/"$pkg".toml recipes/incoming-*/"$pkg".toml; do
[ -f "$f" ] || continue
hallada=1
if tiene_licencia "$f"; then ya=$((ya+1)); 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 · ya tenían $ya · en la tabla pero 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}%)"
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