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>
This commit is contained in:
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/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"
|
||||
@@ -53,8 +53,17 @@ export TMP
|
||||
xargs -P "$JOBS" -I{} sh -c '
|
||||
n=$(printf "%s" "{}" | cut -f1); slug=$(printf "%s" "{}" | cut -f2)
|
||||
lic=$(gh api "repos/$slug/license" --jq ".license.spdx_id" 2>/dev/null)
|
||||
# ⚠ VALIDAR LA FORMA, NO SÓLO LOS «NO SÉ» CONOCIDOS. Cuando el repo da 404 (renombrado, borrado,
|
||||
# privado), `gh api` escribe el CUERPO DEL ERROR JSON en stdout y el filtro --jq falla ⇒ `lic`
|
||||
# queda valiendo `{"message":"Not Found",...}`. Filtrando sólo ""/null/NOASSERTION/other, ese JSON
|
||||
# pasaba como si fuera una licencia y se escribía en la receta: comillas incluidas, lo que además
|
||||
# ROMPE el TOML y hace que el ArtifactHash deje de calcularse. Pasó de verdad en 5 recetas el
|
||||
# 2026-08-07 y lo cazó la verificación de hashes, no la vista.
|
||||
# ⇒ Regla: aceptar sólo lo que TIENE FORMA de expresión SPDX (letras, dígitos, . - + y los
|
||||
# operadores OR/AND/WITH). Todo lo demás es un fallo disfrazado de dato.
|
||||
case "$lic" in
|
||||
""|null|NOASSERTION|other) printf "%s\t%s\n" "$n" "$slug" >> "$TMP/sin.tsv" ;;
|
||||
*[!A-Za-z0-9.+\ -]*) printf "%s\t%s\n" "$n" "$slug" >> "$TMP/sin.tsv" ;;
|
||||
*) printf "%s\t%s\t%s\n" "$n" "$lic" "$slug" >> "$TMP/con.tsv" ;;
|
||||
esac
|
||||
' < "$TMP/objetivos.tsv"
|
||||
|
||||
+41
-3
@@ -63,7 +63,7 @@ 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
|
||||
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,
|
||||
@@ -71,11 +71,32 @@ case "$MODO" in
|
||||
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 ya=$((ya+1)); continue; fi
|
||||
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
|
||||
@@ -90,7 +111,7 @@ case "$MODO" in
|
||||
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 "==> 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)"
|
||||
;;
|
||||
|
||||
@@ -139,6 +160,23 @@ case "$MODO" in
|
||||
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/^/ /'
|
||||
|
||||
Reference in New Issue
Block a user