tawasuyu

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 1373Shell 116Rust 74Markdown 641915 Definiciones

licencias 96% (1129/1165): la fuente git también es evidencia, y el script deja de llamarse «tarball»

Faltaban tres recetas cuya fuente es un repo, no un tarball: `fcft`, `foot` y `glab`. El commit
pineado cumple exactamente el mismo papel que el sha256 —es el árbol EXACTO que construimos— así que
la evidencia estaba igual de disponible, sólo que por otra puerta.

`miembros_de_git()` la abre sin clonar el árbol entero: `--depth 1 --filter=blob:limit=64k` sobre el
commit. Un COPYING nunca pasa de 64k y los fuentes donde vive la concesión tampoco; los que sí pasan
—binarios, assets— son justo los que no interesan. Un repo de cientos de megas se resuelve con unos
pocos. Las tres salieron del `license:` que el propio autor escribe en su `meson.build` (fcft, foot)
y del texto del LICENSE (glab).

Los repos por SSH se saltan diciéndolo: este script hace fetch anónimo y no tiene claves. Son
`hammerd` y `portal-probe`, y ya están resueltas por otra vía (su repo es este mismo).

`analizar()` pasa a consumir un iterable de `(ruta, bytes)` en vez de abrir el tar ella misma, así
que las dos fuentes comparten TODA la lógica de veredicto: la jerarquía de evidencia, la regla de
sólo-la-raíz, la unión de identificaciones y la exigencia de que la concesión hable de la misma
versión que el COPYING. Un criterio, dos puertas.

Y el script se renombra `licencias-tarball.py` → `licencias-fuente.py`, porque el nombre viejo pasó a
ser mentira en el momento en que aprendió a leer git. Un nombre que describe la implementación de
ayer manda a la gente a buscar en el sitio equivocado.

Los 3 ArtifactHash, idénticos antes y después.

b87835f719bb1a8d8f6b4e310d89711489c4ee75 · por Sergio · (hace 1 mes) · padre 9e6e3d5aa · ver el árbol en este commit

Delta semántico · 24

scripts/licencias-fuente.py

scripts/licencias-tarball.py

6 archivos · +553 −482

diff --git a/docs/licencias-evidencia.tsv b/docs/licencias-evidencia.tsv
index 78af5dae..9cfe2e23 100644
--- a/docs/licencias-evidencia.tsv
+++ b/docs/licencias-evidencia.tsv
@@ -1,4 +1,4 @@
-# licencias-evidencia.tsv — lo generó scripts/licencias-tarball.py desde el
+# licencias-evidencia.tsv — lo generó scripts/licencias-fuente.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.
@@ -8,11 +8,15 @@ dwarves	GPL-2.0-only	COPYING + concesión en btf_encoder.c: …/* SPDX-License-I
 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…
+fcft	MIT	meson.build: license: 'MIT'
 file	BSD-2-Clause	COPYING (texto de BSD-2-Clause)
+foot	MIT	meson.build: license: 'MIT'
 giflib	MIT	COPYING (texto de MIT)
+glab	MIT	LICENSE (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'
+hammerd	MIT	el repo es el propio hammer: LICENSE (MIT License) + Cargo.toml: license = "MIT"
 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…
 json-c	MIT	COPYING (texto de MIT)
 libogg	BSD-3-Clause	COPYING (texto de BSD-3-Clause)
@@ -28,6 +32,7 @@ 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)
+portal-probe	MIT	el repo es el propio hammer: LICENSE (MIT License) + Cargo.toml: license = "MIT"
 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…
@@ -35,5 +40,3 @@ 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'
-hammerd	MIT	el repo es el propio hammer: LICENSE (MIT License) + Cargo.toml: license = "MIT"
-portal-probe	MIT	el repo es el propio hammer: LICENSE (MIT License) + Cargo.toml: license = "MIT"
diff --git a/recipes/fcft.toml b/recipes/fcft.toml
index b0c0f080..12ca52ca 100644
--- a/recipes/fcft.toml
+++ b/recipes/fcft.toml
@@ -3,6 +3,7 @@
 # harfbuzz/pixman (canónicas). Último eslabón antes del terminal foot. meson, lib estática.
 name    = "fcft"
 version = "3.3.3"
+license = "MIT"
 
 [source]
 # Fuente GIT por commit, no el archive de codeberg (ADR 0006). El `/archive/3.3.3.tar.gz` lo genera
diff --git a/recipes/foot.toml b/recipes/foot.toml
index 6a9e4ab5..c8650a47 100644
--- a/recipes/foot.toml
+++ b/recipes/foot.toml
@@ -4,6 +4,7 @@
 # pixman/fontconfig/freetype/harfbuzz/libxkbcommon/ncurses (canónicas). Sin PGO (evita cage).
 name    = "foot"
 version = "1.27.0"
+license = "MIT"
 
 [source]
 # Fuente GIT por commit, no el archive de codeberg (ADR 0006), por lo mismo que fcft: el
diff --git a/recipes/glab.toml b/recipes/glab.toml
index 894748cf..9b22ee23 100644
--- a/recipes/glab.toml
+++ b/recipes/glab.toml
@@ -2,6 +2,7 @@
 # El repo tiene varios mains (cmd/glab, cmd/gen-docs); fijamos ./cmd/glab explícito.
 name    = "glab"
 version = "1.102.0"
+license = "MIT"
 
 [source]
 repo   = "https://gitlab.com/gitlab-org/cli"
diff --git a/scripts/licencias-tarball.py b/scripts/licencias-fuente.py
similarity index 83%
rename from scripts/licencias-tarball.py
rename to scripts/licencias-fuente.py
index 82e9d227..509fa3b4 100755
--- a/scripts/licencias-tarball.py
+++ b/scripts/licencias-fuente.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-"""licencias-tarball.py — deduce el SPDX de una receta desde SU PROPIO tarball pineado, offline.
+"""licencias-fuente.py — deduce el SPDX de una receta desde SU PROPIA fuente pineada.
 
 ── QUÉ AÑADE SOBRE LOS HERMANOS ────────────────────────────────────────────────────────────────
 `licencias.sh --sembrar` escribe desde una tabla curada a mano, y `licencias-desambiguar.sh`
@@ -8,7 +8,8 @@ fuera lo mismo: lo que no está en la tabla y lo que no vive en GitHub.
 
 Pero `work/tarballs/` guarda cada tarball indexado por su **sha256**, o sea que el árbol EXACTO que
 la receta pinea ya está en disco. La evidencia más fuerte posible —la declaración del propio autor
-en el commit que construimos— no necesita red: necesita abrir el tar.
+en el commit que construimos— no necesita red: necesita abrir el tar. Y para las recetas de fuente
+git, el `commit` pineado cumple el mismo papel que el sha256: se lee sin clonar el árbol entero.
 
 ── LA JERARQUÍA DE EVIDENCIA, DE MÁS FUERTE A MÁS DÉBIL ────────────────────────────────────────
   1. `Cargo.toml` / `meson.build` de la raíz. El autor declara el SPDX él mismo, en el formato
@@ -32,15 +33,15 @@ veredicto sale como `pendiente` con lo que SÍ se encontró, para que el humano
 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í
+Por defecto NO toca la red: mira sólo lo que ya está en `work/tarballs/`. Con `--fetch` trae a un
+temporal lo que falte —tarballs y fuentes git—, verifica el tarball 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
+Uso:  scripts/licencias-fuente.py            informe + escribe docs/licencias-evidencia.tsv
+      scripts/licencias-fuente.py --fetch    además trae las fuentes que falten (red)
+      scripts/licencias-fuente.py <receta>   una sola, con el detalle a la vista
 """
 import os
 import re
@@ -226,23 +227,12 @@ def declaracion_del_autor(ruta, texto):
     return None, None
 
 
-def analizar(tar_path):
-    """Un solo recorrido del tar. Devuelve (spdx, cita) o (None, pista)."""
-    autor = None
-    reuse = []            # ficheros de LICENSES/ (convención REUSE: el nombre ES el SPDX)
-    candidatos = []       # (prioridad, ruta, texto) de los ficheros de licencia de la RAÍZ
-    familia = None
-    familia_fichero = None
-    varias = None
-    cita_later = None
-    cita_only = None
-    spdx_headers = {}
-
+def miembros_de_tar(tar_path):
+    """(ruta relativa a la raíz, bytes) de cada fichero del tar. Un solo recorrido, en streaming."""
     try:
         tf = tarfile.open(tar_path, "r:*")
     except Exception as e:
-        return None, f"no se pudo abrir el tar: {e}"
-
+        raise RuntimeError(f"no se pudo abrir el tar: {e}")
     with tf:
         for i, m in enumerate(tf):
             if i > MAX_MIEMBROS:
@@ -252,6 +242,64 @@ def analizar(tar_path):
             rel = sin_prefijo(m.name)
             if not rel:
                 continue
+            try:
+                yield rel, tf.extractfile(m).read()
+            except Exception:
+                continue
+
+
+def miembros_de_git(repo, commit, tmp):
+    """Igual, pero desde un repo git, SIN clonar el árbol entero.
+
+    `--depth 1 --filter=blob:limit=64k` sobre el commit PINEADO: trae la historia de un solo commit
+    y sólo los blobs chicos. Un COPYING nunca pasa de 64k, y los fuentes donde vive la concesión
+    tampoco — los que sí (binarios, assets) son justo los que no interesan. Así una receta cuyo
+    árbol pesa cientos de megas se resuelve con unos pocos.
+    """
+    ejecutar = lambda *a: subprocess.run(a, cwd=tmp, capture_output=True, text=False)
+    subprocess.run(["git", "init", "-q", tmp], capture_output=True)
+    ejecutar("git", "remote", "add", "origin", repo)
+    r = ejecutar("git", "fetch", "-q", "--depth", "1", "--filter=blob:limit=64k", "origin", commit)
+    if r.returncode != 0:
+        raise RuntimeError("git fetch falló: "
+                           + r.stderr.decode("utf-8", "replace").strip()[:100])
+    r = ejecutar("git", "ls-tree", "-r", "-z", "--long", "FETCH_HEAD")
+    if r.returncode != 0:
+        raise RuntimeError("git ls-tree falló")
+    n = 0
+    for reg in r.stdout.decode("utf-8", "replace").split("\0"):
+        if not reg or n > MAX_MIEMBROS:
+            break
+        meta, _, rel = reg.partition("\t")
+        campos = meta.split()
+        # <modo> <tipo> <sha> <tamaño>
+        if len(campos) < 4 or campos[1] != "blob":
+            continue
+        try:
+            if campos[3] != "-" and int(campos[3]) > MAX_BYTES:
+                continue
+        except ValueError:
+            pass
+        b = ejecutar("git", "cat-file", "blob", campos[2])
+        if b.returncode == 0:
+            n += 1
+            yield rel, b.stdout
+
+
+def analizar(miembros):
+    """Un solo recorrido de la fuente. Devuelve (spdx, cita) o (None, pista)."""
+    autor = None
+    reuse = []            # ficheros de LICENSES/ (convención REUSE: el nombre ES el SPDX)
+    candidatos = []       # (prioridad, ruta, texto) de los ficheros de licencia de la RAÍZ
+    familia = None
+    familia_fichero = None
+    varias = None
+    cita_later = None
+    cita_only = None
+    spdx_headers = {}
+
+    try:
+        for rel, datos in miembros:
             base = os.path.basename(rel)
 
             if rel.startswith("LICENSES/"):
@@ -273,11 +321,6 @@ def analizar(tar_path):
             if not (interesa_autor or es_licencia or es_fuente):
                 continue
 
-            try:
-                datos = tf.extractfile(m).read()
-            except Exception:
-                continue
-
             if interesa_autor and autor is None:
                 lic, cita = declaracion_del_autor(rel, datos.decode("utf-8", "replace"))
                 if lic:
@@ -309,6 +352,9 @@ def analizar(tar_path):
                     if mm:
                         cita_only = f"{rel}: …{texto[max(0, mm.start()-60):mm.end()+20].strip()}…"
 
+    except RuntimeError as e:
+        return None, str(e)
+
     if candidatos:
         # 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`,
@@ -408,23 +454,42 @@ def main():
         nombre = d["name"]
         if objetivo and nombre != objetivo and os.path.basename(f) != objetivo:
             continue
-        sha = d.get("source", {}).get("sha256")
+        src = d.get("source", {})
+        sha = src.get("sha256")
         if not sha:
-            sin_tar.append((nombre, "fuente git/dir: no hay tarball que mirar"))
-            continue
-        tar_path = os.path.join(CACHE, sha + ".tar")
-        if not os.path.exists(tar_path):
-            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)")))
+            # ── Fuente git: el commit pineado ES la evidencia, igual que un sha256 ──────────────
+            repo, commit = src.get("repo"), src.get("commit")
+            if not (repo and commit):
+                sin_tar.append((nombre, "fuente sin tarball ni repo/commit"))
+                continue
+            if repo.startswith(("ssh://", "git@")):
+                # El fetch de este script es anónimo; un repo por SSH necesita claves que no tiene.
+                sin_tar.append((nombre, "repo por SSH: este script no autentica"))
                 continue
-            tar_path = os.path.join(tmp, sha)
-            fallo = bajar_verificado(url, sha, tar_path)
-            if fallo:
-                sin_tar.append((nombre, fallo))
+            if not con_red:
+                sin_tar.append((nombre, "fuente git (probá --fetch)"))
                 continue
-        spdx, cita = analizar(tar_path)
+            sub = os.path.join(tmp, nombre + "-git")
+            os.makedirs(sub, exist_ok=True)
+            try:
+                spdx, cita = analizar(miembros_de_git(repo, commit, sub))
+            except RuntimeError as e:
+                sin_tar.append((nombre, str(e)))
+                continue
+        else:
+            tar_path = os.path.join(CACHE, sha + ".tar")
+            if not os.path.exists(tar_path):
+                url = src.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(miembros_de_tar(tar_path))
         if spdx:
             filas.append((nombre, spdx, una_linea(cita)))
         else:
@@ -456,14 +521,14 @@ def main():
         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("# licencias-evidencia.tsv — lo generó scripts/licencias-fuente.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")
         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)}")
+    print(f"==> resueltas con evidencia de la fuente: {len(filas)}")
     for n, s, c in filas:
         print(f"   {n:28s} {s:34s} {c[:70]}")
     print(f"\n==> pendientes (hay tarball pero no alcanza para afirmar): {len(pendientes)}")