vimPluginsUpdater: generate plugin licenses

Fetch GitHub SPDX license metadata during plugin updates and emit meta.license for generated vim plugin derivations.

Fall back to lib.licenses.unfree when no SPDX license is detected, and round-trip existing license metadata through get-plugins.nix so partial updates do not drop it.
This commit is contained in:
Austin Horstman
2026-04-26 12:27:25 -05:00
parent 9f1639ef52
commit 544cc616d8
3 changed files with 76 additions and 0 deletions
@@ -17,6 +17,7 @@ let
parse = _name: value: {
inherit (value) pname version;
homePage = value.meta.homepage;
license = value.meta.license.spdxId or null;
checksum =
if hasChecksum value then
{
@@ -19,6 +19,7 @@
#
import inspect
import json
import logging
import os
import textwrap
@@ -40,6 +41,15 @@ HEADER = "# GENERATED by ./pkgs/applications/editors/vim/plugins/utils/update.py
NIXPKGS_NVIMTREESITTER_FOLDER = "pkgs/applications/editors/vim/plugins/nvim-treesitter"
SPDX_LICENSE_NORMALIZATION = {
"AGPL-3.0": "AGPL-3.0-only",
"GPL-2.0": "GPL-2.0-only",
"GPL-3.0": "GPL-3.0-only",
"LGPL-2.0": "LGPL-2.0-only",
"LGPL-2.1": "LGPL-2.1-only",
"LGPL-3.0": "LGPL-3.0-only",
}
class VimEditor(nixpkgs_plugin_update.Editor):
nvim_treesitter_updated = False
@@ -112,11 +122,22 @@ class VimEditor(nixpkgs_plugin_update.Editor):
content = f" {plugin.normalized_name} = "
src_nix = repo.as_nix(plugin)
license_spdx_id = plugin.license
if license_spdx_id is not None:
license_spdx_id = SPDX_LICENSE_NORMALIZATION.get(
license_spdx_id, license_spdx_id
)
license_nix = (
f" meta.license = lib.meta.getLicenseFromSpdxId {json.dumps(license_spdx_id)};\n"
if license_spdx_id
else " meta.license = lib.licenses.unfree;\n"
)
content += """{buildFn} {{
pname = "{plugin.name}";
version = "{plugin.version}";
src = {src_nix};
meta.homepage = "{repo.uri}";
{license_nix}\
meta.hydraPlatforms = [ ];
}};
@@ -125,6 +146,7 @@ class VimEditor(nixpkgs_plugin_update.Editor):
plugin=plugin,
src_nix=src_nix,
repo=repo,
license_nix=license_nix,
)
log.debug(content)
return content
@@ -244,6 +244,9 @@ class Repo:
hash = "{plugin.to_sri_hash()}";
}}"""
def get_license_spdx_id(self, fallback_license: str | None = None) -> str | None:
return fallback_license
class RepoGitHub(Repo):
def __init__(self, owner: str, repo: str, branch: str) -> None:
@@ -460,6 +463,41 @@ class RepoGitHub(Repo):
loaded = json.loads(data)
return loaded["hash"]
def get_license_spdx_id(self, fallback_license: str | None = None) -> str | None:
license_url = f"https://api.github.com/repos/{self.owner}/{self.repo}/license"
log.debug("Fetching license metadata from %s", license_url)
def log_fetch_failure(reason: str) -> str | None:
log.warning(
"Failed to fetch license metadata for %s/%s: %s; reusing %s",
self.owner,
self.repo,
reason,
fallback_license or "no cached license",
)
return fallback_license
try:
req = make_request(license_url, self.token)
with urllib.request.urlopen(req, timeout=10) as response:
data = json.load(response)
except urllib.error.HTTPError as e:
if e.code == 404:
return None
return log_fetch_failure(f"HTTP {e.code}")
except urllib.error.URLError as e:
return log_fetch_failure(str(e))
license_info = data.get("license")
if not isinstance(license_info, dict):
return None
spdx_id = license_info.get("spdx_id")
if not spdx_id or spdx_id == "NOASSERTION":
return None
return spdx_id
def as_nix(self, plugin: "Plugin") -> str:
if plugin.has_submodules:
submodule_attr = "\n fetchSubmodules = true;"
@@ -528,6 +566,7 @@ class Plugin:
date: datetime | None = None
last_tag: str | None = None
tag: str | None = None
license: str | None = None
@property
def normalized_name(self) -> str:
@@ -783,6 +822,7 @@ class Editor:
date,
last_tag=last_tag,
tag=source_tag,
license=attr.get("license"),
)
plugins.append((pdesc, p))
@@ -1112,6 +1152,11 @@ def prefetch_plugin(
)
if cached_plugin is not None:
log.debug(f"Cache hit for {p.name}!")
license_spdx_id = (
cached_plugin.license
or (current_plugin.license if current_plugin else None)
or p.repo.get_license_spdx_id()
)
return (
replace(
cached_plugin,
@@ -1121,6 +1166,7 @@ def prefetch_plugin(
date=date,
last_tag=latest_tag,
tag=source_tag,
license=license_spdx_id,
),
p.repo.redirect,
)
@@ -1132,6 +1178,11 @@ def prefetch_plugin(
if source_tag
else p.repo.prefetch(commit)
)
license_spdx_id = (
current_plugin.license
if current_plugin and current_plugin.license
else p.repo.get_license_spdx_id()
)
return (
Plugin(
@@ -1143,6 +1194,7 @@ def prefetch_plugin(
date=date,
last_tag=latest_tag,
tag=source_tag,
license=license_spdx_id,
),
p.repo.redirect,
)
@@ -1249,6 +1301,7 @@ class Cache:
attr.get("version", ""),
last_tag=attr.get("last_tag"),
tag=attr.get("tag"),
license=attr.get("license"),
)
downloads[cache_key] = p
return downloads