Files
hammer/scripts/licencias-cargo.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

82 lines
5.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# licencias-cargo.sh — lee la licencia que el AUTOR declara en su `Cargo.toml`, al tag exacto que la
# receta pinea. Es la fuente más autoritativa de las tres que usamos, y corrige a las otras dos.
#
# ── LA JERARQUÍA DE EVIDENCIA, DE PEOR A MEJOR ──────────────────────────────────────────────────
# 1. El nombre de la receta ......... NO es evidencia. Prohibido (ver docs/licencias-conocidas.tsv).
# 2. La familia / URL de fuente ..... buena para agrupar (X.Org=MIT, KDE Frameworks=LGPL doble).
# 3. La API de licencias de GitHub .. detecta el fichero LICENSE real, pero con DOS pérdidas:
# devuelve identificadores obsoletos y ambiguos (`GPL-3.0` sin decir -only/-or-later) y
# **colapsa las licencias dobles a una sola**.
# 4. El `Cargo.toml` del autor ...... ← ESTO. Una expresión SPDX escrita por quien tiene la potestad
# de licenciar, sin ambigüedad y sin colapsar. Corrige los dos defectos de (3) de una vez.
#
# Ejemplo medido: la API dejó `fd` como `Apache-2.0`; su Cargo.toml dice `MIT OR Apache-2.0`. No era
# falso —cumplir Apache-2.0 basta— pero perdía una opción que el autor concedió. Con esto se recupera.
#
# ── SE PIDE AL TAG QUE LA RECETA PINEA, NO A HEAD ───────────────────────────────────────────────
# Ésa era la limitación registrada de la detección por API (mira la rama por defecto, no la versión
# que empaquetamos). Acá se prueba `?ref=v<version>` y `?ref=<version>` antes de caer a HEAD, y se
# ANOTA con qué ref se resolvió cada una, para que la tercera columna diga si el dato es exacto o
# aproximado. Un dato con su procedencia se puede auditar; uno sin ella hay que creerlo.
#
# ── LO QUE NO HACE ──────────────────────────────────────────────────────────────────────────────
# `license.workspace = true` (la licencia vive en el workspace padre) y `license-file` (licencia a
# medida, sin SPDX) se DESCARTAN en vez de adivinarse. Y no toca las recetas que no son Cargo: los
# módulos Go no tienen un campo equivalente en `go.mod`, así que ésos siguen dependiendo de (2) y (3).
#
# Uso: scripts/licencias-cargo.sh (escribe docs/licencias-cargo.tsv)
# FORZAR=1 TABLA=docs/licencias-cargo.tsv scripts/licencias.sh --sembrar
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
SALIDA="${SALIDA:-docs/licencias-cargo.tsv}"
JOBS="${JOBS:-12}"
command -v gh >/dev/null && gh auth status >/dev/null 2>&1 || { echo "!! 'gh' ausente o sin auth"; exit 1; }
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
# Todas las recetas con fuente en GitHub (tengan licencia o no: éste CORRIGE, no sólo rellena).
for f in recipes/*.toml recipes/incoming-*/*.toml; do
[ -f "$f" ] || continue
u=$(grep -m1 -E '^[[:space:]]*(tarball|repo)[[:space:]]*=' "$f" 2>/dev/null)
case "$u" in *github.com*) ;; *) continue ;; esac
n=$(basename "$f" .toml)
v=$(awk -F'"' '/^\[/{exit} /^[[:space:]]*version[[:space:]]*=/{print $2; exit}' "$f")
slug=$(printf '%s\n' "$u" | sed -E 's#.*github\.com[:/]+([^/"]+)/([^/"]+).*#\1/\2#; s/\.git$//')
case "$slug" in */*) printf '%s\t%s\t%s\n' "$n" "$slug" "${v:-}" ;; esac
done | sort -u > "$TMP/obj.tsv"
echo "==> recetas con fuente en GitHub: $(wc -l < "$TMP/obj.tsv")"
export TMP
xargs -P "$JOBS" -I{} sh -c '
n=$(printf "%s" "{}" | cut -f1); slug=$(printf "%s" "{}" | cut -f2); v=$(printf "%s" "{}" | cut -f3)
for ref in "v$v" "$v" ""; do
[ -z "$v" ] && [ -n "$ref" ] && continue
if [ -n "$ref" ]; then q="?ref=$ref"; etiq="$ref"; else q=""; etiq="HEAD(aprox)"; fi
c=$(gh api "repos/$slug/contents/Cargo.toml$q" --jq ".content" 2>/dev/null) || continue
[ -z "$c" ] && continue
lic=$(printf "%s" "$c" | base64 -d 2>/dev/null | awk -F\" "/^\[/{if(t)exit; if(\$0 ~ /^\[package\]/)t=1; next} t&&/^[[:space:]]*license[[:space:]]*=/{print \$2; exit}")
case "$lic" in
""|*workspace*|*true*) break ;;
*) printf "%s\t%s\t%s\n" "$n" "$lic" "$etiq" >> "$TMP/ok.tsv"; break ;;
esac
done
' < "$TMP/obj.tsv"
N=$(wc -l < "$TMP/ok.tsv" 2>/dev/null || echo 0)
EXACT=$(grep -vc 'HEAD(aprox)' "$TMP/ok.tsv" 2>/dev/null || echo 0)
{
echo "# licencias-cargo.tsv — GENERADO por scripts/licencias-cargo.sh. No editar a mano."
echo "#"
echo "# La licencia que el AUTOR declara en su Cargo.toml: expresión SPDX escrita por quien tiene"
echo "# la potestad de licenciar. Sin ambigüedad -only/-or-later y SIN colapsar las dobles ⇒ manda"
echo "# sobre la detección por API de GitHub, que pierde ambas cosas. Sembrar con FORZAR=1."
echo "#"
echo "# Tercera columna = el ref con el que se resolvió. Un tag concreto significa que el dato es"
echo "# de la VERSIÓN QUE EMPAQUETAMOS; 'HEAD(aprox)' que no se halló el tag y puede diferir."
echo "# Total $N · al tag exacto $EXACT · aproximadas $((N-EXACT))"
sort "$TMP/ok.tsv" 2>/dev/null
} > "$SALIDA"
echo "==> $N licencias de Cargo.toml ($EXACT al tag exacto) → $SALIDA"