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.
This commit is contained in:
@@ -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"
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 19 and column 88.
|
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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()
|
||||
# <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
|
||||
@@ -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)}")
|
||||
Reference in New Issue
Block a user