> 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…
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.
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)}")