licencias: 96% (1124/1164) — --fetch para los tarballs que no estaban en caché

Ocho paquetes más (giflib, pcre2, nghttp2, libogg, libvorbis, speexdsp, libuv, tea) con la misma
regla de evidencia. Su tarball no estaba en `work/tarballs/`, así que el modo `--fetch` lo baja a un
temporal, lo verifica contra el sha256 QUE LA RECETA PINEA —o es el árbol exacto o no se mira— y lo
borra. No se escribe en la caché de hammer a propósito: es suya, y un fichero puesto ahí por otro
camino es una vía de envenenamiento que nadie audita.

Dos correcciones al criterio, las dos porque produjo una afirmación falsa:

  · `gmp` salía GPL-3.0-or-later. Su `COPYING` es la GPLv3, pero al lado trae `COPYING.LESSERv3` y
    `COPYINGv2` porque la biblioteca es LGPL. La regla anterior —«el `COPYING` a secas gana al
    sufijado»— arregla socat, cuyo `COPYING.OpenSSL` es una excepción, y rompe gmp, donde el extra
    nombra otra VERSIÓN y no una dep. Esa diferencia es demasiado fina para codificarla sin
    equivocarse, así que ahora se identifican TODOS los ficheros de la raíz y sólo hay veredicto si
    dicen lo mismo. gmp, ffmpeg y los tres `gi-*` quedan pendientes, que es lo correcto: en ffmpeg
    la respuesta ni siquiera está en el árbol, la deciden los flags de la receta.

  · Pero «varios ficheros» no es «ambigüedad»: el `COPYING` de pcre2 son dos líneas apuntando a
    `LICENCE.md`, y los dos dicen BSD-3. Por eso se unen las identificaciones en vez de contar
    ficheros — ambiguo es que digan cosas DISTINTAS.

Y el TSV pasa a ser ACUMULATIVO. Una receta ya sembrada sale del conjunto de entrada porque ya tiene
`license`, así que regenerar el fichero entero borraba su cita — y la cita es el rastro de
auditoría, lo único que deja revisar un veredicto sin repetir el trabajo. Un fichero de evidencia
que se olvida de lo que ya probó no es evidencia.

Los 10 ArtifactHash afectados, idénticos antes y después.
This commit is contained in:
Sergio
2026-09-05 15:43:51 +00:00
parent b606bf988b
commit 96855001c6
12 changed files with 112 additions and 25 deletions
+14 -6
View File
@@ -1,29 +1,37 @@
# licencias-evidencia.tsv — lo generó scripts/licencias-tarball.py desde el
# TARBALL PINEADO de cada receta. Tres columnas: paquete, SPDX, y la CITA que lo
# decidió. REVISAR A MANO antes de sembrar: es evidencia, no un veredicto firmado.
# Se ACUMULA: las filas de lo ya sembrado se conservan como rastro de auditoría.
adwaita-hello LGPL-2.1-or-later meson.build: license: 'LGPL-2.1-or-later'
dav1d BSD-2-Clause COPYING (texto de BSD-2-Clause)
dwarves GPL-2.0-only COPYING + concesión en btf_encoder.c: …/* SPDX-License-Identifier: GPL-2.0-only Copyright (C) 20…
elfutils-libdw GPL-3.0-or-later COPYING + concesión en tests/run-eu-search-die.sh: …ion; either version 3 of the License, or # (at your option) any later version. # # elfutils is di…
elfutils GPL-3.0-or-later COPYING + concesión en tests/run-eu-search-die.sh: …ion; either version 3 of the License, or # (at your option) any later version. # # elfutils is di…
elfutils-libdw GPL-3.0-or-later COPYING + concesión en tests/run-eu-search-die.sh: …ion; either version 3 of the License, or # (at your option) any later version. # # elfutils is di…
exiv2 GPL-2.0-or-later COPYING + concesión en README.md: …ation; either version 2 of the License, or (at your option) any later version. Exiv2 program is…
file BSD-2-Clause COPYING (texto de BSD-2-Clause)
giflib MIT COPYING (texto de MIT)
go BSD-3-Clause LICENSE (texto de BSD-3-Clause)
gtk4-hello LGPL-2.1-or-later meson.build: license: 'LGPL-2.1-or-later'
hammer-edit LGPL-2.1-or-later meson.build: license: 'LGPL-2.1-or-later'
dav1d BSD-2-Clause COPYING (texto de BSD-2-Clause)
exiv2 GPL-2.0-or-later COPYING + concesión en README.md: …ation; either version 2 of the License, or (at your option) any later version. Exiv2 program is…
intltool GPL-2.0-or-later COPYING + concesión en intltool-extract.in: …on; either version 2 of the # License, or (at your option) any later version. # # Intltool is d…
libpcap BSD-3-Clause LICENSE (texto de BSD-3-Clause)
libzip BSD-3-Clause LICENSE (texto de BSD-3-Clause)
mtdev MIT COPYING (texto de MIT)
json-c MIT COPYING (texto de MIT)
libogg BSD-3-Clause COPYING (texto de BSD-3-Clause)
libpcap BSD-3-Clause LICENSE (texto de BSD-3-Clause)
libsodium ISC LICENSE (texto de ISC)
libssh2 BSD-3-Clause COPYING (texto de BSD-3-Clause)
libuv MIT LICENSE (texto de MIT)
libvorbis BSD-3-Clause COPYING (texto de BSD-3-Clause)
libzip BSD-3-Clause LICENSE (texto de BSD-3-Clause)
mesa-llvmpipe MIT meson.build: license: 'MIT'
mesa-swrast MIT meson.build: license: 'MIT'
mtdev MIT COPYING (texto de MIT)
nano GPL-3.0-or-later COPYING + concesión en NEWS: …Finally, nano is now licensed under the GNU GPL version 3 or later, and its documentat…
nghttp2 MIT COPYING (texto de MIT)
pcre2-shared BSD-3-Clause LICENCE.md (texto de BSD-3-Clause)
scdoc MIT COPYING (texto de MIT)
seatd MIT meson.build: license: 'MIT'
socat GPL-2.0-or-later COPYING + concesión en socat_buildscript_for_android.sh: …; either version 2.1 of the License, or (at your option) any later version. The GNU C Libr…
sourceview-hello LGPL-2.1-or-later meson.build: license: 'LGPL-2.1-or-later'
speexdsp BSD-3-Clause COPYING (texto de BSD-3-Clause)
tea MIT LICENSE (texto de MIT)
tllist MIT meson.build: license: 'MIT'
1 # licencias-evidencia.tsv — lo generó scripts/licencias-tarball.py desde el
2 # TARBALL PINEADO de cada receta. Tres columnas: paquete, SPDX, y la CITA que lo
3 # decidió. REVISAR A MANO antes de sembrar: es evidencia, no un veredicto firmado.
4 # Se ACUMULA: las filas de lo ya sembrado se conservan como rastro de auditoría.
5 adwaita-hello
6 dav1d
7 dwarves
elfutils-libdw
8 elfutils
9 elfutils-libdw
10 exiv2
11 file
12 giflib
13 go
14 gtk4-hello
15 hammer-edit
dav1d
exiv2
16 intltool
libpcap
libzip
mtdev
17 json-c
18 libogg
19 libpcap
20 libsodium
21 libssh2
22 libuv
23 libvorbis
24 libzip
25 mesa-llvmpipe
26 mesa-swrast
27 mtdev
28 nano
29 nghttp2
30 pcre2-shared
31 scdoc
32 seatd
33 socat
34 sourceview-hello
35 speexdsp
36 tea
37 tllist
+1
View File
@@ -3,6 +3,7 @@
# sha256 del tarball (el wrapper lo calcula), y adaptar build/install del shell de abuild.
name = "giflib"
version = "5.2.2"
license = "MIT"
[source]
tarball = "https://downloads.sourceforge.net/sourceforge/giflib/giflib-5.2.2.tar.gz"
@@ -7,6 +7,7 @@
# --disable-symvers como el canónico (musl no maneja symbol versioning de la forma que asume pcre2).
name = "pcre2-shared"
version = "10.47"
license = "BSD-3-Clause"
[source]
tarball = "https://github.com/PhilipHazel/pcre2/releases/download/pcre2-10.47/pcre2-10.47.tar.bz2"
+1
View File
@@ -10,6 +10,7 @@
# ISLA DINÁMICA: shared, porque libsoup y todo el stack GNOME de esta onda son .so.
name = "nghttp2"
version = "1.64.0"
license = "MIT"
[source]
tarball = "https://github.com/nghttp2/nghttp2/releases/download/v1.64.0/nghttp2-1.64.0.tar.xz"
+1
View File
@@ -1,6 +1,7 @@
# libogg 1.3.5 — contenedor Ogg (C). Dep de libvorbis (→ libcanberra → plasma-workspace). Autotools limpio.
name = "libogg"
version = "1.3.5"
license = "BSD-3-Clause"
[source]
tarball = "https://downloads.xiph.org/releases/ogg/libogg-1.3.5.tar.xz"
sha256 = "c4d91be36fc8e54deae7575241e03f4211eb102afb3fc0775fbbc1b740016705"
+1
View File
@@ -1,6 +1,7 @@
# libvorbis 1.3.7 — códec Vorbis + vorbisfile (C). Lo exige libcanberra (lector de sonidos .ogg). Autotools.
name = "libvorbis"
version = "1.3.7"
license = "BSD-3-Clause"
[source]
tarball = "https://downloads.xiph.org/releases/vorbis/libvorbis-1.3.7.tar.xz"
sha256 = "b33cc4934322bcbf6efcbacf49e3ca01aadbea4114ec9589d1b1e9d20f72954b"
+1
View File
@@ -7,6 +7,7 @@
# --disable-symvers como el canónico (musl no maneja symbol versioning de la forma que asume pcre2).
name = "pcre2-shared"
version = "10.47"
license = "BSD-3-Clause"
[source]
tarball = "https://github.com/PhilipHazel/pcre2/releases/download/pcre2-10.47/pcre2-10.47.tar.bz2"
+1
View File
@@ -2,6 +2,7 @@
# PulseAudio lo usa para el remuestreo de audio (su resampler por defecto).
name = "speexdsp"
version = "1.2.1"
license = "BSD-3-Clause"
[source]
tarball = "https://downloads.us.xiph.org/releases/speex/speexdsp-1.2.1.tar.gz"
+1
View File
@@ -1,6 +1,7 @@
# libuv 1.52.1 — lib C base (de-Alpinizada, Etapa G). compiler=zig-cc (migrado de gcc, matar-gcc 2026-07-16), configure-split, estático.
name = "libuv"
version = "1.52.1"
license = "MIT"
[source]
tarball = "https://dist.libuv.org/dist/v1.52.1/libuv-v1.52.1.tar.gz"
+1
View File
@@ -7,6 +7,7 @@
# --disable-symvers como el canónico (musl no maneja symbol versioning de la forma que asume pcre2).
name = "pcre2-shared"
version = "10.47"
license = "BSD-3-Clause"
[source]
tarball = "https://github.com/PhilipHazel/pcre2/releases/download/pcre2-10.47/pcre2-10.47.tar.bz2"
+1
View File
@@ -3,6 +3,7 @@
# sha256 del tarball (el wrapper lo calcula), y adaptar build/install del shell de abuild.
name = "tea"
version = "0.9.2"
license = "MIT"
[source]
tarball = "https://gitea.com/gitea/tea/archive/v0.9.2.tar.gz"
+88 -19
View File
@@ -31,14 +31,26 @@ quien la revise tiene que poder auditarla sin repetir el trabajo. Lo que no alca
veredicto sale como `pendiente` con lo que SÍ se encontró, para que el humano arranque desde ahí.
NADA se escribe en las recetas: la salida es un TSV que revisa una persona y siembra `licencias.sh`.
── EL MODO `--fetch` ───────────────────────────────────────────────────────────────────────────
Por defecto NO toca la red: mira sólo lo que ya está en `work/tarballs/`. Con `--fetch` baja los
tarballs que faltan a un temporal, los verifica por sha256 —el mismo sha que la receta pinea, así
que o es el árbol exacto o no se mira— y los borra al terminar. No escribe en la caché de hammer a
propósito: la caché es suya y un fichero puesto ahí por otro camino es una vía de envenenamiento
que nadie audita.
Uso: scripts/licencias-tarball.py informe + escribe docs/licencias-evidencia.tsv
scripts/licencias-tarball.py --fetch además baja los tarballs que falten (red)
scripts/licencias-tarball.py <receta> una sola, con el detalle a la vista
"""
import os
import re
import sys
import glob
import shutil
import hashlib
import tarfile
import tempfile
import subprocess
import tomllib
RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -298,20 +310,29 @@ def analizar(tar_path):
cita_only = f"{rel}: …{texto[max(0, mm.start()-60):mm.end()+20].strip()}"
if candidatos:
exactos = [c for c in candidatos if c[0] == 0]
if not exactos and len(candidatos) > 1:
# ffmpeg trae COPYING.GPLv2, COPYING.GPLv3, COPYING.LGPLv2.1 y COPYING.LGPLv3 en la
# raíz: cuál rige lo deciden los FLAGS de la receta (`--enable-gpl`), no el árbol.
# Elegir el primero por orden alfabético sería inventar.
return None, ("la raíz trae " + str(len(candidatos)) + " ficheros de licencia y ninguno "
"canónico (" + ", ".join(sorted(c[1] for c in candidatos))
+ "): decide la receta, no el árbol")
_, familia_fichero, texto = sorted(candidatos)[0]
hits = identificar(texto)
if len(hits) == 1:
familia = hits[0]
elif len(hits) > 1:
varias = hits
# Se identifica CADA fichero de la raíz y se unen los resultados. Que haya varios no es
# ambigüedad por sí solo: el `COPYING` de pcre2 son dos líneas que apuntan a `LICENCE.md`,
# y los dos dicen BSD-3. Ambiguo es que digan COSAS DISTINTAS.
#
# Probé antes con «el `COPYING` a secas gana al sufijado»: arregla socat (cuyo
# `COPYING.OpenSSL` es una excepción) y ROMPE gmp, que junto a `COPYING` (GPLv3) trae
# `COPYING.LESSERv3` porque la biblioteca es LGPL — el veredicto salía GPL-3.0-or-later, que
# es falso. La diferencia entre «el extra nombra una dep» y «el extra nombra otra versión»
# es demasiado fina para codificarla sin equivocarse, y equivocarse acá es afirmar algo que
# no es. ffmpeg es el caso límite: sus cuatro COPYING.* los desempatan los FLAGS de la
# receta (`--enable-gpl`), o sea que la respuesta ni siquiera está en el árbol.
vistos = {}
for _, ruta, texto in sorted(candidatos):
for h in identificar(texto):
vistos.setdefault(h, ruta)
if len(vistos) == 1:
familia, familia_fichero = next(iter(vistos.items()))
elif len(vistos) > 1:
return None, ("la raíz dice " + str(len(vistos)) + " licencias distintas ("
+ ", ".join(f"{k} en {v}" for k, v in sorted(vistos.items()))
+ "): la expresión la compone un humano")
else:
familia_fichero = sorted(candidatos)[0][1]
# 1. Declaración del autor — si es un SPDX de verdad. meson deja escribir cualquier cadena, y
# `gsd-schemas`/`libgdm` declaran «GPL2+», que NO es un identificador: sembrarlo sería meter
@@ -358,8 +379,29 @@ def analizar(tar_path):
return None, "sin COPYING ni declaración en el árbol"
def bajar_verificado(url, sha, destino):
"""Baja `url` a `destino` y confirma su sha256. Devuelve el porqué si no se pudo."""
r = subprocess.run(
["curl", "-fsSL", "--max-time", "180", "-o", destino, url],
capture_output=True, text=True,
)
if r.returncode != 0:
return f"curl falló ({r.returncode}): {r.stderr.strip()[:80]}"
h = hashlib.sha256()
with open(destino, "rb") as fh:
for bloque in iter(lambda: fh.read(1 << 20), b""):
h.update(bloque)
if h.hexdigest() != sha:
return f"sha256 NO coincide: bajó {h.hexdigest()[:16]}…, la receta pinea {sha[:16]}"
return None
def main():
objetivo = sys.argv[1] if len(sys.argv) > 1 else None
args = [a for a in sys.argv[1:]]
con_red = "--fetch" in args
args = [a for a in args if a != "--fetch"]
objetivo = args[0] if args else None
tmp = tempfile.mkdtemp(prefix="licencias-") if con_red else None
filas, pendientes, sin_tar = [], [], []
for f, d in recetas_sin_licencia():
@@ -372,14 +414,25 @@ def main():
continue
tar_path = os.path.join(CACHE, sha + ".tar")
if not os.path.exists(tar_path):
sin_tar.append((nombre, "tarball no cacheado"))
continue
url = d.get("source", {}).get("tarball")
if not (con_red and url):
sin_tar.append((nombre, "tarball no cacheado"
+ ("" if con_red else " (probá --fetch)")))
continue
tar_path = os.path.join(tmp, sha)
fallo = bajar_verificado(url, sha, tar_path)
if fallo:
sin_tar.append((nombre, fallo))
continue
spdx, cita = analizar(tar_path)
if spdx:
filas.append((nombre, spdx, una_linea(cita)))
else:
pendientes.append((nombre, una_linea(cita)))
if tmp:
shutil.rmtree(tmp, ignore_errors=True)
if objetivo:
for n, s, c in filas:
print(f"{n}\t{s}\n evidencia: {c}")
@@ -387,12 +440,28 @@ def main():
print(f"{n}\tPENDIENTE — {una_linea(c)}")
return
# ACUMULATIVO, no regenerado: una receta ya sembrada sale del conjunto de entrada (porque ya
# tiene `license`), así que reescribir el fichero entero borraría su cita. Y la cita es el
# rastro de auditoría — lo único que deja revisar un veredicto sin repetir el trabajo. Un
# fichero de evidencia que se olvida de lo que ya probó no es evidencia.
previas = {}
if os.path.exists(SALIDA):
for linea in open(SALIDA):
if linea.startswith("#") or not linea.strip():
continue
partes = linea.rstrip("\n").split("\t")
if len(partes) == 3:
previas[partes[0]] = tuple(partes)
for n, sp, c in filas:
previas[n] = (n, sp, c)
with open(SALIDA, "w") as fh:
fh.write("# licencias-evidencia.tsv — lo generó scripts/licencias-tarball.py desde el\n")
fh.write("# TARBALL PINEADO de cada receta. Tres columnas: paquete, SPDX, y la CITA que lo\n")
fh.write("# decidió. REVISAR A MANO antes de sembrar: es evidencia, no un veredicto firmado.\n")
for n, s, c in filas:
fh.write(f"{n}\t{s}\t{c}\n")
fh.write("# Se ACUMULA: las filas de lo ya sembrado se conservan como rastro de auditoría.\n")
for fila in sorted(previas.values()):
fh.write("\t".join(fila) + "\n")
print(f"==> resueltas con evidencia del tarball: {len(filas)}")
for n, s, c in filas: