licencias: resolver -only vs -or-later CON LA CITA — y tres «pruebas» que no probaban nada

Los identificadores obsoletos (`GPL-2.0`, `LGPL-2.1`, `AGPL-3.0`) no dicen si el proyecto
concede «sólo esta versión» o «ésta o cualquier posterior», y eso decide con qué se puede
combinar el paquete y bajo qué términos puede redistribuirlo quien lo reciba.

NO SE PUEDE MIRAR EL COPYING, que es la trampa evidente: el texto de la GPL es IDÉNTICO en
los dos casos —es la licencia, no la concesión— y encima su apéndice «cómo aplicar la
licencia» contiene literalmente «or (at your option) any later version», así que buscarla
ahí da SIEMPRE positivo y parece evidencia siendo plantilla. Misma trampa que la regla del
`.a` no-PIC, donde grep contaba reubicaciones de `.debug_*`. La concesión vive en las
cabeceras de los fuentes y en el README; ahí se busca, excluyendo los ficheros de licencia.

SE GUARDA LA CITA, no sólo el veredicto: fichero + frase exacta que decidió. Una licencia es
una afirmación legal y quien la revise tiene que poder ver POR QUÉ dice lo que dice sin
repetir el trabajo.

Y eso pagó de inmediato: de las 12 primeras resoluciones, TRES eran evidencia inválida y
sólo se vieron porque estaba la cita. Cada una de una clase distinta:
 · caligula   — la frase salía de `checks/headless/expected.iso`, un FIXTURE DE TEST;
 · libnl      — de `include/linux-private/linux/seg6.h`, una cabecera del KERNEL vendorizada.
                La licencia del kernel no es la de libnl;
 · lm-sensors — declarado GPL-2.0 y la cita decía «version 2.1 of the License», que es la
                LGPL. La frase era real pero no sostenía lo que se le atribuía.

Las tres clases quedan filtradas en el script: ficheros de licencia, rutas de test/fixture, y
código vendorizado; más una comprobación nueva de que **la cita hable de la misma versión que
la licencia declarada**.

Sembradas las 9 auditadas (13 ficheros; algunas viven en varias colas). Ambiguas: 55 → 41.
Las 40 sin evidencia quedan listadas y siguen contadas — la mayoría son Go y la familia
cosmic, donde la frase no aparece fuera del COPYING.

Hashes verificados: 0 movidos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 15:15:20 -04:00
co-authored by Claude Opus 5
parent 6263d24788
commit 2c2ee951d6
15 changed files with 126 additions and 13 deletions
+13
View File
@@ -0,0 +1,13 @@
# licencias-desambiguadas.tsv — GENERADO por scripts/licencias-desambiguar.sh.
# Resuelve -only vs -or-later buscando la concesión FUERA del COPYING (donde la frase es
# plantilla y daría siempre positivo). Tercera columna: fichero :: cita que lo decidió.
# Resueltas 9 (auditadas a mano; 3 descartadas por evidencia inválida) · sin evidencia 40
dosfstools GPL-3.0-or-later src/lfn.h :: (at your option) any later version.
htop GPL-2.0-or-later README.md :: GNU General Public License, version 2 (GPL-2.0) or, at your option, any later version.
libndp LGPL-2.1-or-later libndp/list.h :: * version 2.1 of the License, or (at your option) any later version.
libqalculate GPL-2.0-or-later libqalculate/qalculate.h :: (at your option) any later version.
libsndfile LGPL-2.1-or-later src/id3.h :: ** (at your option) any later version.
libusb LGPL-2.1-or-later android/config.h :: * version 2.1 of the License, or (at your option) any later version.
qrencode LGPL-2.1-or-later mask.h :: * version 2.1 of the License, or any later version.
rink GPL-3.0-or-later core/definitions.units :: # (at your option) any later version.
tig GPL-2.0-or-later src/graph.c :: * the License, or (at your option) any later version.
1 # licencias-desambiguadas.tsv — GENERADO por scripts/licencias-desambiguar.sh.
2 # Resuelve -only vs -or-later buscando la concesión FUERA del COPYING (donde la frase es
3 # plantilla y daría siempre positivo). Tercera columna: fichero :: cita que lo decidió.
4 # Resueltas 9 (auditadas a mano; 3 descartadas por evidencia inválida) · sin evidencia 40
5 dosfstools GPL-3.0-or-later src/lfn.h :: (at your option) any later version.
6 htop GPL-2.0-or-later README.md :: GNU General Public License, version 2 (GPL-2.0) or, at your option, any later version.
7 libndp LGPL-2.1-or-later libndp/list.h :: * version 2.1 of the License, or (at your option) any later version.
8 libqalculate GPL-2.0-or-later libqalculate/qalculate.h :: (at your option) any later version.
9 libsndfile LGPL-2.1-or-later src/id3.h :: ** (at your option) any later version.
10 libusb LGPL-2.1-or-later android/config.h :: * version 2.1 of the License, or (at your option) any later version.
11 qrencode LGPL-2.1-or-later mask.h :: * version 2.1 of the License, or any later version.
12 rink GPL-3.0-or-later core/definitions.units :: # (at your option) any later version.
13 tig GPL-2.0-or-later src/graph.c :: * the License, or (at your option) any later version.
+1 -1
View File
@@ -3,7 +3,7 @@
# sha256 del tarball (el wrapper lo calcula), y adaptar build/install del shell de abuild. # sha256 del tarball (el wrapper lo calcula), y adaptar build/install del shell de abuild.
name = "dosfstools" name = "dosfstools"
version = "4.2" version = "4.2"
license = "GPL-3.0" license = "GPL-3.0-or-later"
[source] [source]
tarball = "https://github.com/dosfstools/dosfstools/releases/download/v4.2/dosfstools-4.2.tar.gz" tarball = "https://github.com/dosfstools/dosfstools/releases/download/v4.2/dosfstools-4.2.tar.gz"
+1 -1
View File
@@ -13,7 +13,7 @@
name = "htop" name = "htop"
version = "3.5.1" version = "3.5.1"
license = "GPL-2.0" license = "GPL-2.0-or-later"
[source] [source]
tarball = "https://github.com/htop-dev/htop/releases/download/3.5.1/htop-3.5.1.tar.xz" tarball = "https://github.com/htop-dev/htop/releases/download/3.5.1/htop-3.5.1.tar.xz"
+1 -1
View File
@@ -32,7 +32,7 @@
# en argv» era una suposición, no una medición. # en argv» era una suposición, no una medición.
name = "libqalculate" name = "libqalculate"
version = "5.12.0" version = "5.12.0"
license = "GPL-2.0" license = "GPL-2.0-or-later"
[source] [source]
tarball = "https://github.com/Qalculate/libqalculate/releases/download/v5.12.0/libqalculate-5.12.0.tar.gz" tarball = "https://github.com/Qalculate/libqalculate/releases/download/v5.12.0/libqalculate-5.12.0.tar.gz"
+1 -1
View File
@@ -17,7 +17,7 @@
# ISLA DINÁMICA: shared, porque libpulse y todo lo que cuelga de gnome-shell son .so. # ISLA DINÁMICA: shared, porque libpulse y todo lo que cuelga de gnome-shell son .so.
name = "libsndfile" name = "libsndfile"
version = "1.2.2" version = "1.2.2"
license = "LGPL-2.1" license = "LGPL-2.1-or-later"
[source] [source]
tarball = "https://github.com/libsndfile/libsndfile/releases/download/1.2.2/libsndfile-1.2.2.tar.xz" tarball = "https://github.com/libsndfile/libsndfile/releases/download/1.2.2/libsndfile-1.2.2.tar.xz"
+1 -1
View File
@@ -17,7 +17,7 @@
# ISLA DINÁMICA: shared, porque libpulse y todo lo que cuelga de gnome-shell son .so. # ISLA DINÁMICA: shared, porque libpulse y todo lo que cuelga de gnome-shell son .so.
name = "libsndfile" name = "libsndfile"
version = "1.2.2" version = "1.2.2"
license = "LGPL-2.1" license = "LGPL-2.1-or-later"
[source] [source]
tarball = "https://github.com/libsndfile/libsndfile/releases/download/1.2.2/libsndfile-1.2.2.tar.xz" tarball = "https://github.com/libsndfile/libsndfile/releases/download/1.2.2/libsndfile-1.2.2.tar.xz"
+1 -1
View File
@@ -15,7 +15,7 @@
# en el cierre, y sin el riesgo del choque eudev/libudev-zero por el mismo .pc. # en el cierre, y sin el riesgo del choque eudev/libudev-zero por el mismo .pc.
name = "libusb" name = "libusb"
version = "1.0.30" version = "1.0.30"
license = "LGPL-2.1" license = "LGPL-2.1-or-later"
[source] [source]
tarball = "https://github.com/libusb/libusb/releases/download/v1.0.30/libusb-1.0.30.tar.bz2" tarball = "https://github.com/libusb/libusb/releases/download/v1.0.30/libusb-1.0.30.tar.bz2"
+1 -1
View File
@@ -7,7 +7,7 @@
# "de gueto" que gcc/cmake: herramienta del rootfs, no del catálogo). # "de gueto" que gcc/cmake: herramienta del rootfs, no del catálogo).
name = "libndp" name = "libndp"
version = "1.9" version = "1.9"
license = "LGPL-2.1" license = "LGPL-2.1-or-later"
[source] [source]
tarball = "https://github.com/jpirko/libndp/archive/refs/tags/v1.9.tar.gz" tarball = "https://github.com/jpirko/libndp/archive/refs/tags/v1.9.tar.gz"
+1 -1
View File
@@ -9,7 +9,7 @@
# --disable-textport (evita readline/ncurses; sólo lib, sin binario qalc). libxml-2.0 REQUIRED → libxml2 sellado ✓. # --disable-textport (evita readline/ncurses; sólo lib, sin binario qalc). libxml-2.0 REQUIRED → libxml2 sellado ✓.
name = "libqalculate" name = "libqalculate"
version = "5.6.0" version = "5.6.0"
license = "GPL-2.0" license = "GPL-2.0-or-later"
[source] [source]
tarball = "https://github.com/Qalculate/libqalculate/releases/download/v5.6.0/libqalculate-5.6.0.tar.gz" tarball = "https://github.com/Qalculate/libqalculate/releases/download/v5.6.0/libqalculate-5.6.0.tar.gz"
+1 -1
View File
@@ -5,7 +5,7 @@
# nativos, que es lo que PulseAudio necesita para los sonidos del sistema. Evita la cadena flac+opus. # nativos, que es lo que PulseAudio necesita para los sonidos del sistema. Evita la cadena flac+opus.
name = "libsndfile" name = "libsndfile"
version = "1.2.2" version = "1.2.2"
license = "LGPL-2.1" license = "LGPL-2.1-or-later"
[source] [source]
tarball = "https://github.com/libsndfile/libsndfile/releases/download/1.2.2/libsndfile-1.2.2.tar.xz" tarball = "https://github.com/libsndfile/libsndfile/releases/download/1.2.2/libsndfile-1.2.2.tar.xz"
+1 -1
View File
@@ -7,7 +7,7 @@
# NO → estático). find_package(PNG)/find_package(Iconv) NO son REQUIRED; iconv lo aporta musl. # NO → estático). find_package(PNG)/find_package(Iconv) NO son REQUIRED; iconv lo aporta musl.
name = "qrencode" name = "qrencode"
version = "4.1.1" version = "4.1.1"
license = "LGPL-2.1" license = "LGPL-2.1-or-later"
[source] [source]
tarball = "https://github.com/fukuchi/libqrencode/archive/refs/tags/v4.1.1.tar.gz" tarball = "https://github.com/fukuchi/libqrencode/archive/refs/tags/v4.1.1.tar.gz"
+1 -1
View File
@@ -3,7 +3,7 @@
# sha256 del tarball (el wrapper lo calcula), y adaptar build/install del shell de abuild. # sha256 del tarball (el wrapper lo calcula), y adaptar build/install del shell de abuild.
name = "libusb" name = "libusb"
version = "1.0.30" version = "1.0.30"
license = "LGPL-2.1" license = "LGPL-2.1-or-later"
[source] [source]
tarball = "https://github.com/libusb/libusb/releases/download/v1.0.30/libusb-1.0.30.tar.bz2" tarball = "https://github.com/libusb/libusb/releases/download/v1.0.30/libusb-1.0.30.tar.bz2"
+1 -1
View File
@@ -4,7 +4,7 @@
# - las deps van con su nombre NIX; remapealas a las recetas del corpus si difieren. # - las deps van con su nombre NIX; remapealas a las recetas del corpus si difieren.
name = "rink" name = "rink"
version = "0.9.0" version = "0.9.0"
license = "GPL-3.0" license = "GPL-3.0-or-later"
[source] [source]
repo = "https://github.com/tiffany352/rink-rs" repo = "https://github.com/tiffany352/rink-rs"
+1 -1
View File
@@ -1,7 +1,7 @@
# tig 2.6.1 — interfaz ncurses para git (C). De-Alpinizada: gcc, configure-split, ncurses+pcre2. # tig 2.6.1 — interfaz ncurses para git (C). De-Alpinizada: gcc, configure-split, ncurses+pcre2.
name = "tig" name = "tig"
version = "2.6.1" version = "2.6.1"
license = "GPL-2.0" license = "GPL-2.0-or-later"
[source] [source]
tarball = "https://github.com/jonas/tig/releases/download/tig-2.6.1/tig-2.6.1.tar.gz" tarball = "https://github.com/jonas/tig/releases/download/tig-2.6.1/tig-2.6.1.tar.gz"
sha256 = "5adeabdcd93aa0423d618da8b878b53482bef6e0e9e1fe224acc0f18031fe91e" sha256 = "5adeabdcd93aa0423d618da8b878b53482bef6e0e9e1fe224acc0f18031fe91e"
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# licencias-desambiguar.sh — resuelve `-only` vs `-or-later` en los SPDX obsoletos, CON LA CITA.
#
# ── EL PROBLEMA QUE CIERRA ─────────────────────────────────────────────────────────────────────
# La API de licencias de GitHub devuelve identificadores obsoletos (`GPL-2.0`, `GPL-3.0`, `LGPL-2.1`,
# `AGPL-3.0`). SPDX los declaró obsoletos **porque son ambiguos**: no dicen si el proyecto concede
# «sólo esta versión» o «ésta o cualquier posterior». La diferencia no es cosmética — decide con qué
# se puede combinar el paquete y bajo qué términos puede redistribuirlo quien lo reciba.
#
# ── POR QUÉ NO SE PUEDE MIRAR EL COPYING (la trampa) ───────────────────────────────────────────
# El fichero COPYING de la GPL es **idéntico** en los dos casos: es el texto de la licencia, no la
# concesión. Y encima su apéndice «cómo aplicar la licencia a tus programas» contiene literalmente la
# frase «or (at your option) any later version» ⇒ buscarla en el COPYING da SIEMPRE positivo y
# parecería evidencia siendo texto de plantilla. Es la misma trampa que la regla del `.a` no-PIC,
# donde `grep R_X86_64_32` contaba reubicaciones de `.debug_*` y mentía.
#
# La concesión vive en las **cabeceras de los fuentes** y en el README. Por eso acá se busca la frase
# en TODO el repo y se **descartan explícitamente** COPYING/LICENSE/LICENCE: si sólo aparece ahí, no
# hay evidencia y el paquete se deja para el humano.
#
# ── SE GUARDA LA CITA, NO SÓLO EL VEREDICTO ────────────────────────────────────────────────────
# Cada línea de salida lleva el fichero y la frase exacta que decidió. Una licencia es una afirmación
# legal: quien la revise tiene que poder ver POR QUÉ dice lo que dice sin repetir el trabajo. Un
# veredicto sin su prueba obliga a confiar; con la prueba, se audita en diez segundos.
#
# Ejemplo real (htop): yo lo habría puesto `-only` de memoria. `htop.c` dice «Released under the GNU
# GPLv2+» y el README «version 2 (GPL-2.0) or, at your option, any later version» ⇒ es `-or-later`.
# La memoria se equivoca; la cita no.
#
# Uso: scripts/licencias-desambiguar.sh (escribe docs/licencias-desambiguadas.tsv)
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
SALIDA="${SALIDA:-docs/licencias-desambiguadas.tsv}"
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
# Las ambiguas con fuente en GitHub. (La búsqueda de código de GitHub sólo funciona sobre GitHub;
# el resto se queda para el humano, y eso se dice al final en vez de disimularlo.)
./scripts/licencias.sh --revisar 2>/dev/null | while IFS=$'\t' read -r n lic; do
[ -z "${n:-}" ] && continue
f=$(ls "recipes/$n.toml" 2>/dev/null | head -1)
[ -z "$f" ] && f=$(ls recipes/incoming-*/"$n.toml" 2>/dev/null | head -1)
[ -z "$f" ] && continue
u=$(grep -m1 -E '^[[:space:]]*(tarball|repo)[[:space:]]*=' "$f" 2>/dev/null)
case "$u" in *github.com*) ;; *) continue ;; esac
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" "$lic" "$slug" ;; esac
done | sort -u > "$TMP/obj.tsv"
echo "==> ambiguas con fuente en GitHub: $(wc -l < "$TMP/obj.tsv")"
# En SERIE: la búsqueda de código de GitHub va a 10 consultas por minuto. Paralelizar sólo consigue
# que devuelva 403 y que la mitad se pierda en silencio, que es peor que tardar.
: > "$TMP/ok.tsv"; : > "$TMP/no.tsv"
while IFS=$'\t' read -r n lic slug; do
paths=$(gh api -X GET search/code -f q="repo:$slug \"any later version\"" \
--jq '[.items[].path] | join(" ")' 2>/dev/null)
# Tres clases de fichero que PARECEN evidencia y no lo son. Las tres aparecieron en la primera
# corrida, y sólo se vieron porque guardamos la cita:
# · ficheros de licencia → la frase es plantilla del propio COPYING (ver cabecera);
# · FIXTURES DE TEST → `caligula` "probaba" ser GPL-3.0-or-later por una frase dentro de
# `checks/headless/expected.iso`, una imagen de prueba;
# · CÓDIGO VENDORIZADO → `libnl` citaba `include/linux-private/linux/seg6.h`, una cabecera del
# KERNEL incrustada. La licencia del kernel no es la de libnl.
util=""
for p in $paths; do
bn=$(basename "$p" | tr '[:upper:]' '[:lower:]')
case "$bn" in copying*|license*|licence*|copyright) continue ;; esac
case "$p" in
*test*|*Test*|*fixture*|*checks/*|*testdata/*|*.iso|*.bin|*.img) continue ;;
*vendor/*|*third_party/*|*3rdparty/*|*linux-private/*|*/deps/*|*external/*) continue ;;
esac
util="$p"; break
done
if [ -z "$util" ]; then printf '%s\t%s\t%s\n' "$n" "$lic" "$slug" >> "$TMP/no.tsv"; sleep 6; continue; fi
cita=$(gh api "repos/$slug/contents/$util" --jq .content 2>/dev/null | base64 -d 2>/dev/null \
| grep -m1 -i "any later version" | tr -s ' \t' ' ' | sed 's/^ *//' | cut -c1-120)
# ── LA CITA TIENE QUE HABLAR DE LA MISMA VERSIÓN QUE LA LICENCIA DECLARADA ───────────────────
# `lm-sensors` estaba declarado GPL-2.0 y su cita decía «version 2.1 of the License», que es la
# LGPL: la frase era real pero NO sostenía lo que se le atribuía. Una evidencia que habla de otra
# licencia no es evidencia de ésta.
ver=$(printf '%s' "$lic" | sed -E 's/^A?L?GPL-([0-9.]+).*/\1/')
case "$cita" in
*"$ver"*) ;;
*) printf '%s\t%s\t%s\n' "$n" "$lic" "$slug" >> "$TMP/no.tsv"; sleep 6; continue ;;
esac
nuevo=$(printf '%s' "$lic" | sed -E 's/^(A?L?GPL-[0-9.]+)$/\1-or-later/')
printf '%s\t%s\t%s :: %s\n' "$n" "$nuevo" "$util" "${cita:-(sin cita legible)}" >> "$TMP/ok.tsv"
sleep 6
done < "$TMP/obj.tsv"
N=$(wc -l < "$TMP/ok.tsv"); M=$(wc -l < "$TMP/no.tsv")
{
echo "# licencias-desambiguadas.tsv — GENERADO por scripts/licencias-desambiguar.sh."
echo "# Resuelve -only vs -or-later buscando la concesión FUERA del COPYING (donde la frase es"
echo "# plantilla y daría siempre positivo). Tercera columna: fichero :: cita que lo decidió."
echo "# Resueltas $N · sin evidencia (quedan para el humano) $M"
sort "$TMP/ok.tsv"
} > "$SALIDA"
echo "==> resueltas $N · sin evidencia $M$SALIDA"
[ "$M" -gt 0 ] && { echo "==> sin evidencia:"; cut -f1 "$TMP/no.tsv" | tr '\n' ' '; echo; }