From b87835f719bb1a8d8f6b4e310d89711489c4ee75 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sat, 5 Sep 2026 18:07:17 +0000 Subject: [PATCH] =?UTF-8?q?licencias=2096%=20(1129/1165):=20la=20fuente=20?= =?UTF-8?q?git=20tambi=C3=A9n=20es=20evidencia,=20y=20el=20script=20deja?= =?UTF-8?q?=20de=20llamarse=20=C2=ABtarball=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/licencias-evidencia.tsv | 9 +- recipes/fcft.toml | 1 + recipes/foot.toml | 1 + recipes/glab.toml | 1 + ...cencias-tarball.py => licencias-fuente.py} | 151 +++++++++++++----- 5 files changed, 117 insertions(+), 46 deletions(-) rename scripts/{licencias-tarball.py => licencias-fuente.py} (83%) 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 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 una sola, con el detalle a la vista """ import os import re @@ -226,8 +227,67 @@ def declaracion_del_autor(ruta, texto): return None, None -def analizar(tar_path): - """Un solo recorrido del tar. Devuelve (spdx, cita) o (None, pista).""" +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: + raise RuntimeError(f"no se pudo abrir el tar: {e}") + with tf: + for i, m in enumerate(tf): + if i > MAX_MIEMBROS: + break + if not m.isfile() or m.size > MAX_BYTES: + continue + 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() + # + 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 @@ -239,19 +299,7 @@ def analizar(tar_path): spdx_headers = {} try: - tf = tarfile.open(tar_path, "r:*") - except Exception as e: - return None, f"no se pudo abrir el tar: {e}" - - with tf: - for i, m in enumerate(tf): - if i > MAX_MIEMBROS: - break - if not m.isfile() or m.size > MAX_BYTES: - continue - rel = sin_prefijo(m.name) - if not rel: - continue + 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 - tar_path = os.path.join(tmp, sha) - fallo = bajar_verificado(url, sha, tar_path) - if fallo: - sin_tar.append((nombre, fallo)) + 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 - spdx, cita = analizar(tar_path) + if not con_red: + sin_tar.append((nombre, "fuente git (probá --fetch)")) + continue + 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)}")