gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 815Rust 56Shell 37Markdown 291229 Definitions

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.

96855001c6e9276014d68fc9f9f85ec765673e28 · by Sergio · (1 month ago) · parent b606bf988 · browse the tree at this commit

Semantic delta · 3

scripts/licencias-tarball.py

12 files · +112 −25

diff --git a/docs/licencias-evidencia.tsv b/docs/licencias-evidencia.tsv
index 8824b860..72a9705b 100644
--- a/docs/licencias-evidencia.tsv
+++ b/docs/licencias-evidencia.tsv
@@ -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'
diff --git a/recipes/giflib.toml b/recipes/giflib.toml
index ad213ab9..0de2c0e1 100644
--- a/recipes/giflib.toml
+++ b/recipes/giflib.toml
@@ -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"
diff --git a/recipes/incoming-cosmic/pcre2-shared.toml b/recipes/incoming-cosmic/pcre2-shared.toml
index 7ebc72d7..4dcb0927 100644
--- a/recipes/incoming-cosmic/pcre2-shared.toml
+++ b/recipes/incoming-cosmic/pcre2-shared.toml
@@ -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"
diff --git a/recipes/incoming-gnome/nghttp2.toml b/recipes/incoming-gnome/nghttp2.toml
index f749b3b5..654639a7 100644
--- a/recipes/incoming-gnome/nghttp2.toml
+++ b/recipes/incoming-gnome/nghttp2.toml
@@ -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"
diff --git a/recipes/incoming-kde/libogg.toml b/recipes/incoming-kde/libogg.toml
index 067e98a1..4e1123ed 100644
--- a/recipes/incoming-kde/libogg.toml
+++ b/recipes/incoming-kde/libogg.toml
@@ -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"
diff --git a/recipes/incoming-kde/libvorbis.toml b/recipes/incoming-kde/libvorbis.toml
index 0f302ee0..d4ee8541 100644
--- a/recipes/incoming-kde/libvorbis.toml
+++ b/recipes/incoming-kde/libvorbis.toml
@@ -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"
diff --git a/recipes/incoming-kde/pcre2-shared.toml b/recipes/incoming-kde/pcre2-shared.toml
index 7ebc72d7..4dcb0927 100644
--- a/recipes/incoming-kde/pcre2-shared.toml
+++ b/recipes/incoming-kde/pcre2-shared.toml
@@ -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"
diff --git a/recipes/incoming-kde/speexdsp.toml b/recipes/incoming-kde/speexdsp.toml
index 07956aa6..718dda26 100644
--- a/recipes/incoming-kde/speexdsp.toml
+++ b/recipes/incoming-kde/speexdsp.toml
@@ -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"
diff --git a/recipes/libuv.toml b/recipes/libuv.toml
index 9b020452..ff01cb35 100644
--- a/recipes/libuv.toml
+++ b/recipes/libuv.toml
@@ -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"
diff --git a/recipes/pcre2-shared.toml b/recipes/pcre2-shared.toml
index 7ebc72d7..4dcb0927 100644
--- a/recipes/pcre2-shared.toml
+++ b/recipes/pcre2-shared.toml
@@ -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"
diff --git a/recipes/tea.toml b/recipes/tea.toml
index 90e54d9e..a1ebba74 100644
--- a/recipes/tea.toml
+++ b/recipes/tea.toml
@@ -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"
diff --git a/scripts/licencias-tarball.py b/scripts/licencias-tarball.py
index 9b3f440a..82e9d227 100755
--- a/scripts/licencias-tarball.py
+++ b/scripts/licencias-tarball.py
@@ -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: