licencias: 95% (1114/1164) — la evidencia sale del tarball pineado, offline
`licencias.sh --sembrar` escribe desde una tabla curada a mano y `licencias-desambiguar.sh` resuelve
`-only` vs `-or-later` preguntándole a la búsqueda de código de GitHub. Los dos dejan fuera lo mismo:
lo que nadie curó y lo que no vive en GitHub.
Pero `work/tarballs/` indexa cada tarball por su sha256, así que el árbol EXACTO que la receta pinea
ya está en disco: la declaración del propio autor, en el commit que construimos, sin red. De las 76
sin licencia, 55 tenían su tarball cacheado y 25 salieron con evidencia citable.
`scripts/licencias-tarball.py` la busca en tres niveles: la declaración del autor (`Cargo.toml`,
`meson.build`), un único fichero en `LICENSES/` (REUSE, que usa KDE), y el texto del COPYING más la
CONCESIÓN buscada en las cabeceras de los fuentes — excluyendo COPYING/LICENSE, porque el apéndice
de la GPL trae literalmente «or (at your option) any later version» y buscarla ahí da siempre
positivo siendo plantilla. Es la regla que ya fijó `licencias-desambiguar.sh`, aplicada al árbol
pineado en vez de a GitHub.
La primera versión resolvía 40, y CUATRO estaban mal. Las dejo escritas porque son la forma del
error, no accidentes:
· `socat` salía BSD-2 por su `COPYING.OpenSSL`, que es la excepción, no la licencia. Un `COPYING`
a secas gana ahora a cualquier sufijado.
· `pigz` salía Apache-2.0 por `zopfli/COPYING`: la licencia de una pieza VENDORIZADA leída como la
del contenedor. Sólo se mira la raíz.
· `openssh` salía MIT porque su `LICENCE` es un compendio de cuatro y me quedaba con la primera
que pegara. Si el texto trae varias, no hay veredicto: lo compone un humano.
· `nano` salía GPL-3.0-**or-later** citando el «either version 2» de su `aclocal.m4` — plantilla de
autotools, no del proyecto, y encima de otra versión. Ahora la cita tiene que venir de un fichero
del autor Y hablar de la misma versión mayor que el COPYING.
Y dos bugs míos que producían el mismo daño en silencio: la marca de BSD-3 era «Neither the name of»,
que libpcap y libzip no usan («The names of the authors may not be used to endorse») ⇒ se declaraban
BSD-2; y el mayor de versión lo sacaba de `"GPL-3.0".rsplit("-",1)[0][-1]`, que da «L», así que la
comprobación de coherencia rechazaba TODA cita válida.
La validación final no es una regex: un identificador vale si tenemos su texto en `licenses/`. La
obligación legal es acompañar el binario del TEXTO, así que un SPDX que no podemos entregar no
adelanta nada y sí crea una afirmación que no se sostiene. Eso es lo que atrapa el «GPL2+» que meson
deja escribir en `gsd-schemas` y `libgdm`, que no es un SPDX sino taquigrafía.
Nada se sembró a ojo: cada línea de `docs/licencias-evidencia.tsv` lleva la cita que la decidió, y
las 50 que quedan salen listadas con lo que SÍ se encontró en vez de rellenarse.
Comprobado que no se invalida nada: los 26 ArtifactHash afectados son idénticos antes y después
(`license` no está en `hash_inputs`) — medido, no supuesto.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# 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.
|
||||
adwaita-hello LGPL-2.1-or-later meson.build: license: 'LGPL-2.1-or-later'
|
||||
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…
|
||||
file BSD-2-Clause COPYING (texto de BSD-2-Clause)
|
||||
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)
|
||||
libsodium ISC LICENSE (texto de ISC)
|
||||
libssh2 BSD-3-Clause COPYING (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…
|
||||
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'
|
||||
tllist MIT meson.build: license: 'MIT'
|
||||
|
@@ -3,6 +3,7 @@
|
||||
# libadwaita-1 + las privadas de gdk/gsk/appstream que el .pc no declara. Mismo patrón que gtk4-hello.
|
||||
name = "adwaita-hello"
|
||||
version = "1.7.6"
|
||||
license = "LGPL-2.1-or-later"
|
||||
|
||||
[source]
|
||||
tarball = "https://download.gnome.org/sources/libadwaita/1.7/libadwaita-1.7.6.tar.xz"
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
#
|
||||
name = "dwarves"
|
||||
version = "1.30"
|
||||
license = "GPL-2.0-only"
|
||||
|
||||
[source]
|
||||
tarball = "https://fedorapeople.org/~acme/dwarves/dwarves-1.30.tar.xz"
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#
|
||||
name = "elfutils-libdw"
|
||||
version = "0.194"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[source]
|
||||
tarball = "https://sourceware.org/elfutils/ftp/0.194/elfutils-0.194.tar.bz2"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
name = "elfutils"
|
||||
version = "0.194"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[source]
|
||||
tarball = "https://sourceware.org/elfutils/ftp/0.194/elfutils-0.194.tar.bz2"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# sha256 del tarball (el wrapper lo calcula), y adaptar build/install del shell de abuild.
|
||||
name = "file"
|
||||
version = "5.47"
|
||||
license = "BSD-2-Clause"
|
||||
|
||||
[source]
|
||||
tarball = "https://astron.com/pub/file/file-5.47.tar.gz"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# frente futuro, análogo a mrustc para Rust.
|
||||
name = "go"
|
||||
version = "1.26.4"
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
[source]
|
||||
tarball = "https://go.dev/dl/go1.26.4.linux-amd64.tar.gz"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# de GUI del lab es usable por apps, no solo "compila". deps = cierre .pc completo + gtk4.
|
||||
name = "gtk4-hello"
|
||||
version = "4.18.6"
|
||||
license = "LGPL-2.1-or-later"
|
||||
|
||||
[source]
|
||||
tarball = "https://download.gnome.org/sources/gtk/4.18/gtk-4.18.6.tar.xz"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# estático (gtk4+libadwaita+gtksourceview). No es una demo: es una app instalable y usable.
|
||||
name = "hammer-edit"
|
||||
version = "1.0.0"
|
||||
license = "LGPL-2.1-or-later"
|
||||
|
||||
[source]
|
||||
tarball = "https://download.gnome.org/sources/gtksourceview/5.16/gtksourceview-5.16.0.tar.xz"
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
# etiqueta falsa — la deuda que el frente static-audit ya persiguió una vez.
|
||||
name = "dav1d"
|
||||
version = "1.5.4"
|
||||
license = "BSD-2-Clause"
|
||||
|
||||
[source]
|
||||
tarball = "https://code.videolan.org/videolan/dav1d/-/archive/1.5.4/dav1d-1.5.4.tar.gz"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# runtime = libz.so (zlib-shared). Sin CLI ni samples.
|
||||
name = "exiv2"
|
||||
version = "0.28.5"
|
||||
license = "GPL-2.0-or-later"
|
||||
|
||||
[source]
|
||||
tarball = "https://github.com/Exiv2/exiv2/archive/refs/tags/v0.28.5.tar.gz"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Autotools. Parche conocido: intltool 0.51 usa `\${\` en regex Perl que perl 5.26+ rechaza ⇒ sed fix.
|
||||
name = "intltool"
|
||||
version = "0.51.0"
|
||||
license = "GPL-2.0-or-later"
|
||||
|
||||
[source]
|
||||
tarball = "https://launchpad.net/intltool/trunk/0.51.0/+download/intltool-0.51.0.tar.gz"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# flex/bison para el parser de filtros BPF. Backend PF_PACKET en Linux (sin libnl para minimizar).
|
||||
name = "libpcap"
|
||||
version = "1.10.5"
|
||||
license = "BSD-3-Clause"
|
||||
[source]
|
||||
tarball = "https://www.tcpdump.org/release/libpcap-1.10.5.tar.xz"
|
||||
sha256 = "84fa89ac6d303028c1c5b754abff77224f45eca0a94eb1a34ff0aa9ceece3925"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# como .so dinámicos (bzip2-shared/xz-shared aportan el PIC); openssl para el cifrado AES del ZIP.
|
||||
name = "libzip"
|
||||
version = "1.11.3"
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
[source]
|
||||
tarball = "https://libzip.org/download/libzip-1.11.3.tar.xz"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# Dep de build de libinput (que kwin exige ≥1.28). Autotools.
|
||||
name = "mtdev"
|
||||
version = "1.1.6"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
tarball = "https://bitmath.org/code/mtdev/mtdev-1.1.6.tar.gz"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# json-c 0.18 — lib C base (de-Alpinizada, Etapa G). compiler=zig-cc (migrado de gcc, matar-gcc 2026-07-16), configure-split, estático.
|
||||
name = "json-c"
|
||||
version = "0.18"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
tarball = "https://s3.amazonaws.com/json-c_releases/releases/json-c-0.18.tar.gz"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# De-Alpinizada: compiler=gcc, configure-split, estático.
|
||||
name = "libsodium"
|
||||
version = "1.0.22"
|
||||
license = "ISC"
|
||||
[source]
|
||||
tarball = "https://download.libsodium.org/libsodium/releases/libsodium-1.0.22.tar.gz"
|
||||
sha256 = "adbdd8f16149e81ac6078a03aca6fc03b592b89ef7b5ed83841c086191be3349"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# De-Alpinizada: compiler=zig-cc (migrado de gcc, matar-gcc 2026-07-16), configure-split, crypto vía openssl del corpus, estático.
|
||||
name = "libssh2"
|
||||
version = "1.11.1"
|
||||
license = "BSD-3-Clause"
|
||||
[source]
|
||||
tarball = "https://www.libssh2.org/download/libssh2-1.11.1.tar.gz"
|
||||
sha256 = "d9ec76cbe34db98eec3539fe2c899d26b0c837cb3eb466a56b0f109cabf658f7"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
# desde fuente primero (el monstruo). Test primero, decidir después.
|
||||
name = "mesa-llvmpipe"
|
||||
version = "24.0.9"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
tarball = "https://archive.mesa3d.org/mesa-24.0.9.tar.xz"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# TODO idéntico a mesa.toml; ver su encabezado para el detalle de cada uno.
|
||||
name = "mesa-swrast"
|
||||
version = "24.0.9"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
tarball = "https://archive.mesa3d.org/mesa-24.0.9.tar.xz"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# mtdev 1.1.6 — protocolo multitouch (slots). Dep de build de libinput. Autotools, sin deps.
|
||||
name = "mtdev"
|
||||
version = "1.1.6"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
tarball = "https://bitmath.org/code/mtdev/mtdev-1.1.6.tar.gz"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
name = "nano"
|
||||
version = "9.0"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[source]
|
||||
tarball = "https://www.nano-editor.org/dist/v9/nano-9.0.tar.xz"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# sha256 del tarball (el wrapper lo calcula), y adaptar build/install del shell de abuild.
|
||||
name = "scdoc"
|
||||
version = "1.11.5"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
tarball = "https://git.sr.ht/~sircmpwn/scdoc/archive/1.11.5.tar.gz"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# queremos en el rootfs arje). Sin man-pages (evita scdoc) ni ejemplos. zig 0.13.0 (patrón musl).
|
||||
name = "seatd"
|
||||
version = "0.9.3"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
tarball = "https://git.sr.ht/~kennylevinsen/seatd/archive/0.9.3.tar.gz"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# socat 1.8.1.2 — relay de sockets (C, autotools). Deps openssl+readline. De-Alpinizada: gcc, configure-split.
|
||||
name = "socat"
|
||||
version = "1.8.1.2"
|
||||
license = "GPL-2.0-or-later"
|
||||
[source]
|
||||
tarball = "http://www.dest-unreach.org/socat/download/socat-1.8.1.2.tar.gz"
|
||||
sha256 = "daeb9eed37a99424cd14877208706e93745c91cb86fb917a355635f4df5c8499"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# esqueleto de un editor de código real para el distro.
|
||||
name = "sourceview-hello"
|
||||
version = "5.16.0"
|
||||
license = "LGPL-2.1-or-later"
|
||||
|
||||
[source]
|
||||
tarball = "https://download.gnome.org/sources/gtksourceview/5.16/gtksourceview-5.16.0.tar.xz"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# dep de fcft/foot. Sólo instala tllist.h + tllist.pc. meson, sin binario.
|
||||
name = "tllist"
|
||||
version = "1.1.0"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
tarball = "https://codeberg.org/dnkl/tllist/archive/1.1.0.tar.gz"
|
||||
|
||||
Executable
+410
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
"""licencias-tarball.py — deduce el SPDX de una receta desde SU PROPIO tarball pineado, offline.
|
||||
|
||||
── QUÉ AÑADE SOBRE LOS HERMANOS ────────────────────────────────────────────────────────────────
|
||||
`licencias.sh --sembrar` escribe desde una tabla curada a mano, y `licencias-desambiguar.sh`
|
||||
resuelve `-only` vs `-or-later` preguntándole a la búsqueda de código de GitHub. Los dos dejan
|
||||
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.
|
||||
|
||||
── 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
|
||||
normativo. No hay nada que interpretar.
|
||||
2. Un único fichero en `LICENSES/` (convención REUSE, que usa todo KDE): el nombre del fichero
|
||||
ES el identificador SPDX.
|
||||
3. El texto de `COPYING`/`LICENSE` + la CONCESIÓN buscada en las cabeceras de los fuentes.
|
||||
|
||||
── POR QUÉ EL PASO 3 SON DOS PASOS Y NO UNO ────────────────────────────────────────────────────
|
||||
El COPYING de la GPL es IDÉNTICO para `-only` y `-or-later`: es el texto de la licencia, no la
|
||||
concesión. Peor: su apéndice «cómo aplicar la licencia» contiene literalmente «or (at your option)
|
||||
any later version», así que buscar esa frase en el COPYING da SIEMPRE positivo y parece evidencia
|
||||
siendo plantilla. La concesión vive en las cabeceras de los fuentes y en el README — por eso se
|
||||
busca ahí y se EXCLUYEN explícitamente COPYING/LICENSE/LICENCE. Es la misma regla que ya fijó
|
||||
`licencias-desambiguar.sh`; acá se aplica sobre el árbol pineado en vez de sobre GitHub.
|
||||
|
||||
── SE GUARDA LA CITA ───────────────────────────────────────────────────────────────────────────
|
||||
Cada veredicto sale con el fichero y la frase que lo decidió. Una licencia es una afirmación legal:
|
||||
quien la revise tiene que poder auditarla sin repetir el trabajo. Lo que no alcanza para un
|
||||
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`.
|
||||
|
||||
Uso: scripts/licencias-tarball.py informe + escribe docs/licencias-evidencia.tsv
|
||||
scripts/licencias-tarball.py <receta> una sola, con el detalle a la vista
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import glob
|
||||
import tarfile
|
||||
import tomllib
|
||||
|
||||
RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CACHE = os.path.join(RAIZ, "work", "tarballs")
|
||||
SALIDA = os.path.join(RAIZ, "docs", "licencias-evidencia.tsv")
|
||||
|
||||
# Cada licencia se reconoce por una frase que no aparece en las demás. El texto se NORMALIZA
|
||||
# (espacios colapsados) antes de buscar, porque el mismo texto viene con saltos de línea distintos
|
||||
# según quién lo empaquete.
|
||||
#
|
||||
# ⚠ No se elige «la primera que pegue». Si un fichero contiene VARIAS licencias —el LICENCE de
|
||||
# OpenSSH es un compendio de cuatro— cualquier elección sería una afirmación falsa, así que se
|
||||
# devuelven todas y decide un humano. Las únicas colapsables son las que se contienen entre sí:
|
||||
# el texto de BSD-3 incluye el de BSD-2, y el de 0BSD se parece al de ISC sin su cláusula.
|
||||
FAMILIAS = [
|
||||
("AGPL-3.0", "GNU AFFERO GENERAL PUBLIC LICENSE"),
|
||||
("LGPL-3.0", "GNU LESSER GENERAL PUBLIC LICENSE Version 3"),
|
||||
("LGPL-2.1", "GNU LESSER GENERAL PUBLIC LICENSE Version 2.1"),
|
||||
("LGPL-2.0", "GNU LIBRARY GENERAL PUBLIC LICENSE"),
|
||||
("GPL-3.0", "GNU GENERAL PUBLIC LICENSE Version 3"),
|
||||
("GPL-2.0", "GNU GENERAL PUBLIC LICENSE Version 2, June 1991"),
|
||||
("Apache-2.0", "Apache License Version 2.0"),
|
||||
("MPL-2.0", "Mozilla Public License Version 2.0"),
|
||||
("CC0-1.0", "CC0 1.0 Universal"),
|
||||
("0BSD", "Permission to use, copy, modify, and/or distribute this software for any purpose "
|
||||
"with or without fee is hereby granted. THE SOFTWARE IS PROVIDED"),
|
||||
("ISC", "Permission to use, copy, modify, and/or distribute this software for any purpose "
|
||||
"with or without fee is hereby granted, provided that the above copyright notice"),
|
||||
("Zlib", "altered source versions must be plainly marked as such"),
|
||||
("MIT", "without limitation the rights to use, copy, modify, merge, publish"),
|
||||
("BSD-4-Clause", "All advertising materials mentioning features or use of this software"),
|
||||
("BSD-3-Clause", "endorse or promote products derived"),
|
||||
("BSD-2-Clause", "Redistribution and use in source and binary forms"),
|
||||
]
|
||||
# `mayor: [las que absorbe]` — un texto que contiene al otro no son dos licencias, es una.
|
||||
ABSORBE = {
|
||||
"BSD-4-Clause": ["BSD-3-Clause", "BSD-2-Clause"],
|
||||
"BSD-3-Clause": ["BSD-2-Clause"],
|
||||
"0BSD": ["ISC"],
|
||||
"LGPL-3.0": ["GPL-3.0"],
|
||||
"LGPL-2.1": ["GPL-2.0"],
|
||||
"AGPL-3.0": ["GPL-3.0"],
|
||||
}
|
||||
# Excepciones que cambian el identificador y son invisibles al texto base de la licencia.
|
||||
EXCEPCIONES = [("Apache-2.0", "LLVM Exceptions", "Apache-2.0 WITH LLVM-exception")]
|
||||
# Las familias GNU son las únicas donde el TEXTO no dice la concesión: `-only` y `-or-later`
|
||||
# comparten COPYING palabra por palabra. Se resuelven aparte, en las cabeceras de los fuentes.
|
||||
AMBIGUAS = {"AGPL-3.0", "LGPL-3.0", "LGPL-2.1", "LGPL-2.0", "GPL-3.0", "GPL-2.0"}
|
||||
|
||||
NOMBRES_LICENCIA = re.compile(r"^(COPYING|LICEN[CS]E)([.-].*)?$", re.I)
|
||||
# Ficheros donde SÍ vive la concesión. Se excluyen los de arriba a propósito (ver cabecera).
|
||||
EXT_FUENTE = (
|
||||
".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".py", ".rs", ".go", ".sh",
|
||||
".am", ".ac", ".build", ".txt", ".md", ".in", ".pl", ".java", ".m4",
|
||||
)
|
||||
FRASE_LATER = re.compile(
|
||||
r"(?:any|either)\s+later\s+version|(?:GPL|GPLv?[23])\+|version\s+[23](?:\.\d)?\s+or\s+(?:any\s+)?later",
|
||||
re.I,
|
||||
)
|
||||
FRASE_ONLY = re.compile(
|
||||
r"version\s+[23](?:\.\d)?\s+only|only\s+version\s+[23]|SPDX-License-Identifier:\s*\S*GPL-[23]\.0-only",
|
||||
re.I,
|
||||
)
|
||||
def spdx_conocidos():
|
||||
"""Los identificadores cuyo TEXTO tenemos en `licenses/` (los baja licencias-textos.sh).
|
||||
|
||||
Es la lista blanca correcta y no una regex, por una razón que no es de comodidad: la obligación
|
||||
al distribuir binarios es acompañarlos del TEXTO de la licencia. Un identificador del que no
|
||||
tenemos el texto es un identificador que no podemos cumplir — así que declararlo no adelanta
|
||||
nada y sí crea una afirmación que no se sostiene. Si aparece uno nuevo, el paso siguiente es
|
||||
bajar su texto, no relajar la validación.
|
||||
|
||||
Esto es lo que atrapa el «GPL2+» que meson deja escribir: no es un SPDX, es taquigrafía.
|
||||
"""
|
||||
d = os.path.join(RAIZ, "licenses")
|
||||
if not os.path.isdir(d):
|
||||
return set()
|
||||
return {os.path.splitext(f)[0] for f in os.listdir(d) if f.endswith(".txt")}
|
||||
|
||||
|
||||
CONOCIDOS = spdx_conocidos()
|
||||
PEGAMENTO = {"AND", "OR", "WITH"}
|
||||
|
||||
|
||||
def spdx_valido(expr):
|
||||
"""Toda pieza de la expresión tiene que ser un identificador del que tengamos el texto."""
|
||||
if not CONOCIDOS:
|
||||
return True # sin `licenses/` no hay con qué validar; no se inventa un veredicto
|
||||
piezas = [x.strip("()") for x in expr.split() if x.upper() not in PEGAMENTO]
|
||||
return bool(piezas) and all(x in CONOCIDOS for x in piezas)
|
||||
|
||||
SPDX_HDR = re.compile(rb"SPDX-License-Identifier:\s*([A-Za-z0-9 .+()-]+)")
|
||||
|
||||
# Plantilla de autotools/libtool: viene con el generador, NO la escribió el proyecto, y casi toda
|
||||
# es GPL-2.0-or-later. `nano` (GPL-3.0) salía «-or-later» citando el «either version 2» de su
|
||||
# `aclocal.m4`: una concesión que el proyecto nunca hizo. La cita tiene que ser del autor.
|
||||
BOILERPLATE = re.compile(
|
||||
r"^(m4/|build-aux/|autom4te|aclocal\.m4$|configure$|configure\.ac$|ltmain\.sh$|"
|
||||
r"config\.(guess|sub|rpath)$|install-sh$|missing$|depcomp$|compile$|test-driver$)"
|
||||
)
|
||||
# «either version 3 of the License» → 3. Sirve para exigir que la concesión hable de la MISMA
|
||||
# versión que el COPYING: un veredicto GPL-3.0 justificado con una frase que dice «version 2» es
|
||||
# incoherente, y era exactamente el caso de nano.
|
||||
VERSION_EN_FRASE = re.compile(r"version\s+([23])(?:\.\d)?|GPL-?v?([23])\+|GPL-([23])\.0", re.I)
|
||||
|
||||
MAX_BYTES = 512 * 1024 # un fichero de fuente más grande que esto no es una cabecera de licencia
|
||||
MAX_MIEMBROS = 6000 # cota para no recorrer árboles gigantes enteros
|
||||
|
||||
|
||||
def recetas_sin_licencia():
|
||||
out = []
|
||||
for f in sorted(glob.glob(os.path.join(RAIZ, "recipes", "*.toml")) +
|
||||
glob.glob(os.path.join(RAIZ, "recipes", "*", "*.toml"))):
|
||||
try:
|
||||
d = tomllib.load(open(f, "rb"))
|
||||
except Exception:
|
||||
continue
|
||||
if "name" in d and not d.get("license"):
|
||||
out.append((f, d))
|
||||
return out
|
||||
|
||||
|
||||
def sin_prefijo(nombre):
|
||||
"""`kcompletion-6.27.0/src/x.c` → `src/x.c`. El tar trae un directorio raíz."""
|
||||
return nombre.split("/", 1)[1] if "/" in nombre else ""
|
||||
|
||||
|
||||
def una_linea(txt, tope=160):
|
||||
"""La cita se recorta a UNA línea: lleva texto de un fuente y sus saltos partían la fila del TSV
|
||||
en varias, que es un fichero corrupto con aspecto de fichero bueno."""
|
||||
t = " ".join(str(txt).split()).replace("\t", " ")
|
||||
return t if len(t) <= tope else t[:tope - 1] + "…"
|
||||
|
||||
|
||||
def concuerda(cita, mayor):
|
||||
"""¿La cita habla de la misma versión que el COPYING? Si no, no justifica nada."""
|
||||
if not cita:
|
||||
return False
|
||||
m = VERSION_EN_FRASE.search(cita)
|
||||
if not m:
|
||||
return True # una cabecera SPDX sin número (p. ej. «GPL-2.0-only») ya es explícita
|
||||
return (m.group(1) or m.group(2) or m.group(3)) == mayor
|
||||
|
||||
|
||||
def identificar(texto):
|
||||
"""Todas las licencias presentes en un texto, ya colapsadas las que se contienen."""
|
||||
# Se quita el prefijo de comentario de cada línea antes de colapsar: el COPYING de libssh2 va
|
||||
# dentro de un bloque `/* … */` y sus `*` partían la frase «endorse or * promote products»,
|
||||
# que es justo la marca de la cláusula 3 ⇒ se declaraba BSD-2 un texto BSD-3.
|
||||
t = " ".join(re.sub(r"^[\s*#/]+", "", l) for l in texto.split("\n"))
|
||||
t = " ".join(t.split())
|
||||
hits = [spdx for spdx, marca in FAMILIAS if " ".join(marca.split()) in t]
|
||||
for mayor, menores in ABSORBE.items():
|
||||
if mayor in hits:
|
||||
hits = [h for h in hits if h not in menores]
|
||||
for base, marca, con_excepcion in EXCEPCIONES:
|
||||
if hits == [base] and marca in t:
|
||||
hits = [con_excepcion]
|
||||
return hits
|
||||
|
||||
|
||||
def declaracion_del_autor(ruta, texto):
|
||||
"""El SPDX que el propio autor escribe en su fichero de build. Evidencia de primera."""
|
||||
base = os.path.basename(ruta)
|
||||
if base == "Cargo.toml":
|
||||
m = re.search(r'^\s*license\s*=\s*"([^"]+)"', texto, re.M)
|
||||
if m:
|
||||
return m.group(1), f"{ruta}: license = \"{m.group(1)}\""
|
||||
if base == "meson.build":
|
||||
m = re.search(r"license\s*:\s*\[?\s*'([^']+)'", texto)
|
||||
if m:
|
||||
return m.group(1), f"{ruta}: license: '{m.group(1)}'"
|
||||
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 = {}
|
||||
|
||||
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
|
||||
base = os.path.basename(rel)
|
||||
|
||||
if rel.startswith("LICENSES/"):
|
||||
reuse.append(base)
|
||||
continue
|
||||
|
||||
# ⚠ SÓLO la raíz del árbol. `pigz` trae `zopfli/COPYING` (Apache-2.0) y el paquete NO es
|
||||
# Apache: un COPYING de un subdirectorio es la licencia de una PIEZA EMPAQUETADA, no la
|
||||
# del paquete. Tomar el primero que aparezca convierte una dep vendorizada en el
|
||||
# veredicto del contenedor.
|
||||
if "/" in rel:
|
||||
es_licencia_raiz = False
|
||||
else:
|
||||
es_licencia_raiz = NOMBRES_LICENCIA.match(base) is not None
|
||||
|
||||
interesa_autor = rel in ("Cargo.toml", "meson.build")
|
||||
es_licencia = es_licencia_raiz
|
||||
es_fuente = rel.lower().endswith(EXT_FUENTE) or base in ("README", "AUTHORS", "NEWS")
|
||||
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:
|
||||
autor = (lic, cita)
|
||||
|
||||
if es_licencia:
|
||||
# `COPYING` a secas gana a `COPYING.OpenSSL`: el sufijado suele ser una EXCEPCIÓN o
|
||||
# una licencia añadida, no la del paquete. socat es GPL-2.0 y su `COPYING.OpenSSL`
|
||||
# es BSD — quedarse con el primero que aparezca lo declaraba BSD.
|
||||
exacto = re.fullmatch(r"(COPYING|LICEN[CS]E)", base, re.I) is not None
|
||||
candidatos.append((0 if exacto else 1, rel, datos.decode("utf-8", "replace")))
|
||||
continue # NUNCA se busca la concesión dentro del propio COPYING
|
||||
|
||||
if es_fuente and BOILERPLATE.match(rel):
|
||||
es_fuente = False
|
||||
|
||||
if es_fuente:
|
||||
h = SPDX_HDR.search(datos)
|
||||
if h:
|
||||
ident = h.group(1).decode().strip()
|
||||
spdx_headers[ident] = spdx_headers.get(ident, 0) + 1
|
||||
texto = datos.decode("utf-8", "replace")
|
||||
if cita_later is None:
|
||||
mm = FRASE_LATER.search(texto)
|
||||
if mm:
|
||||
cita_later = f"{rel}: …{texto[max(0, mm.start()-60):mm.end()+20].strip()}…"
|
||||
if cita_only is None:
|
||||
mm = FRASE_ONLY.search(texto)
|
||||
if mm:
|
||||
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
|
||||
|
||||
# 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
|
||||
# basura con cara de dato. Se devuelve como pendiente, con lo que el autor puso a la vista.
|
||||
if autor:
|
||||
if spdx_valido(autor[0]):
|
||||
return autor
|
||||
return None, (f"el autor declara «{autor[0]}», que no es un SPDX del que tengamos texto "
|
||||
f"en licenses/ ({autor[1]})")
|
||||
|
||||
# 2. REUSE con un único fichero de licencia: el nombre es el SPDX.
|
||||
únicos = sorted(set(os.path.splitext(x)[0] for x in reuse))
|
||||
if len(únicos) == 1:
|
||||
return únicos[0], f"LICENSES/{reuse[0]} (único, convención REUSE)"
|
||||
|
||||
# 3. Texto de la licencia + concesión en las cabeceras.
|
||||
if familia:
|
||||
if familia not in AMBIGUAS:
|
||||
return familia, f"{familia_fichero} (texto de {familia})"
|
||||
mayor = familia.rsplit("-", 1)[1][0] # «GPL-3.0» → «3»
|
||||
if not concuerda(cita_later, mayor):
|
||||
cita_later = None
|
||||
if not concuerda(cita_only, mayor):
|
||||
cita_only = None
|
||||
if cita_later and not cita_only:
|
||||
return f"{familia}-or-later", f"{familia_fichero} + concesión en {cita_later}"
|
||||
if cita_only and not cita_later:
|
||||
return f"{familia}-only", f"{familia_fichero} + concesión en {cita_only}"
|
||||
pista = f"{familia} ambigua"
|
||||
if cita_later and cita_only:
|
||||
pista += " · el árbol dice las DOS cosas, decide un humano"
|
||||
else:
|
||||
pista += " · sin concesión fuera del COPYING"
|
||||
return None, pista
|
||||
|
||||
if varias:
|
||||
return None, (f"{familia_fichero} mezcla {len(varias)} licencias ("
|
||||
+ ", ".join(varias) + "): la expresión la compone un humano")
|
||||
if únicos:
|
||||
return None, "REUSE con varias licencias: " + ",".join(únicos)
|
||||
if spdx_headers:
|
||||
top = sorted(spdx_headers.items(), key=lambda x: -x[1])[:3]
|
||||
return None, "sólo cabeceras SPDX: " + ", ".join(f"{k}×{v}" for k, v in top)
|
||||
return None, "sin COPYING ni declaración en el árbol"
|
||||
|
||||
|
||||
def main():
|
||||
objetivo = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
filas, pendientes, sin_tar = [], [], []
|
||||
|
||||
for f, d in recetas_sin_licencia():
|
||||
nombre = d["name"]
|
||||
if objetivo and nombre != objetivo and os.path.basename(f) != objetivo:
|
||||
continue
|
||||
sha = d.get("source", {}).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):
|
||||
sin_tar.append((nombre, "tarball no cacheado"))
|
||||
continue
|
||||
spdx, cita = analizar(tar_path)
|
||||
if spdx:
|
||||
filas.append((nombre, spdx, una_linea(cita)))
|
||||
else:
|
||||
pendientes.append((nombre, una_linea(cita)))
|
||||
|
||||
if objetivo:
|
||||
for n, s, c in filas:
|
||||
print(f"{n}\t{s}\n evidencia: {c}")
|
||||
for n, c in pendientes + sin_tar:
|
||||
print(f"{n}\tPENDIENTE — {una_linea(c)}")
|
||||
return
|
||||
|
||||
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")
|
||||
|
||||
print(f"==> resueltas con evidencia del tarball: {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)}")
|
||||
for n, c in pendientes:
|
||||
print(f" {n:28s} {c}")
|
||||
print(f"\n==> sin tarball que mirar: {len(sin_tar)}")
|
||||
for n, c in sin_tar:
|
||||
print(f" {n:28s} {c}")
|
||||
print(f"\n==> escrito {os.path.relpath(SALIDA, RAIZ)} · NADA se tocó en recipes/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user