Unverified Commit 544cc616 authored by Austin Horstman's avatar Austin Horstman
Browse files

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.
parent 9f1639ef
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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
        {
+22 −0
Original line number Diff line number Diff line
@@ -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
+53 −0
Original line number Diff line number Diff line
@@ -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