Commit 74e924b

HPCesia <me@hpcesia.com>
2026-06-12 05:05:51
refactor: rewrite gen-readme script in python
Assisted-by: opencode/deepseek-v4-pro
1 parent 0669f1b
scripts/gen-readme-config.json
@@ -0,0 +1,7 @@
+{
+  "expand_attrs": {
+    "helixPlugins": {
+      "heading": true
+    }
+  }
+}
\ No newline at end of file
scripts/gen-readme.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+"""Generate a package table and inject it into README.md."""
+
+import json
+import os
+import subprocess
+import sys
+from pathlib import Path
+from tempfile import NamedTemporaryFile
+
+REPO_DIR = Path(__file__).resolve().parent.parent
+README = REPO_DIR / "README.md"
+SYSTEM = os.environ.get("NIX_SYSTEM", "x86_64-linux")
+CONFIG_PATH = Path(__file__).resolve().parent / "gen-readme-config.json"
+
+
+def load_config():
+    default = {"expand_attrs": {}}
+    if CONFIG_PATH.exists():
+        with open(CONFIG_PATH) as f:
+            cfg = json.load(f)
+        raw = cfg.get("expand_attrs", {})
+        if isinstance(raw, list):
+            default["expand_attrs"] = {k: {"heading": True} for k in raw}
+        elif isinstance(raw, dict):
+            default["expand_attrs"] = {
+                k: (v if isinstance(v, dict) else {"heading": v})
+                for k, v in raw.items()
+            }
+    return default
+
+
+NIX_EXPR = """
+let
+  flake = builtins.getFlake (toString {repo_dir});
+  pkgs = flake.legacyPackages."{system}";
+
+  reserved = [
+    "lib" "nixosModules" "overlays"
+    "homeModules" "darwinModules" "flakeModules"
+    "callPackage" "newScope" "overrideScope" "packages"
+    "override" "overrideDerivation"
+  ];
+
+  resolveLic = lic:
+    let
+      go = l:
+        if l == null then {{ shortName = null; spdxId = null; url = null; }}
+        else if builtins.isList l then go (builtins.head l)
+        else if builtins.isString l then {{ shortName = l; spdxId = null; url = null; }}
+        else {{
+          shortName = l.shortName or null;
+          spdxId = l.spdxId or null;
+          url = l.url or null;
+        }};
+    in go lic;
+
+  getLic = meta:
+    resolveLic (meta.license or meta.licence or null);
+
+  isDer = x: builtins.isAttrs x && x ? type && x.type == "derivation";
+  isScope = x: builtins.isAttrs x && x ? callPackage && x ? newScope && !isDer x;
+
+  reservedSet = builtins.listToAttrs (builtins.map (n: {{ name = n; value = true; }}) reserved);
+
+  collect = prefix: attrs:
+    let
+      names = builtins.filter (n: !(reservedSet.${{n}} or false)) (builtins.attrNames attrs);
+    in
+      builtins.concatMap (name:
+        let
+          v = attrs.${{name}};
+          fullPrefix = if prefix == "" then name else "${{prefix}}.${{name}}";
+          pkgInfo = {{
+            path = fullPrefix;
+            pname = v.pname or v.name or "";
+            version = v.version or "";
+            homepage = v.meta.homepage or "";
+            description = v.meta.description or "";
+            license = getLic v.meta;
+          }};
+        in
+          if isDer v then [ pkgInfo ]
+          else if isScope v then collect fullPrefix v
+          else []
+      ) names;
+in
+  collect "" pkgs
+"""
+
+
+def eval_packages():
+    expr = NIX_EXPR.format(repo_dir=json.dumps(str(REPO_DIR)), system=SYSTEM)
+    result = subprocess.run(
+        ["nix", "eval", "--impure", "--expr", expr, "--json"],
+        capture_output=True,
+        text=True,
+        cwd=REPO_DIR,
+    )
+    if result.returncode != 0:
+        print(f"nix eval failed: {result.stderr}", file=sys.stderr)
+        sys.exit(1)
+    return json.loads(result.stdout)
+
+
+def fmt_license(lic):
+    spdx = lic.get("spdxId")
+    url = lic.get("url")
+    short = lic.get("shortName")
+    if spdx:
+        if url and len(url) > 0:
+            return f"[{spdx}]({url})"
+        return spdx
+    if short == "unfree":
+        return "**Unfree**"
+    if short:
+        if url and len(url) > 0:
+            return f"[{short}]({url})"
+        return short
+    return "Not specified"
+
+
+def fmt_name(pname, homepage):
+    if pname and homepage:
+        return f"[{pname}]({homepage})"
+    return pname
+
+
+def fmt_version(version):
+    if version:
+        return f"`{version}`"
+    return "-"
+
+
+def fmt_row(item):
+    path = item["path"]
+    pname = item.get("pname", "")
+    homepage = item.get("homepage", "")
+    version = item.get("version", "")
+    license_info = item.get("license", {})
+    description = item.get("description", "")
+    return f"| `{path}` | {fmt_name(pname, homepage)} | {fmt_version(version)} | {fmt_license(license_info)} | {description} |"
+
+
+HEADER = "| Path | Name | Version | License | Description |"
+SEPARATOR = "| --- | --- | --- | --- | --- |"
+
+
+def mk_table(items):
+    lines = [HEADER, SEPARATOR]
+    lines.extend(fmt_row(item) for item in items)
+    lines.append("")
+    return "\n".join(lines)
+
+
+def group_packages(pkgs):
+    groups = {}
+    for item in pkgs:
+        first_seg = item["path"].split(".")[0]
+        groups.setdefault(first_seg, []).append(item)
+    return groups
+
+
+def generate_markdown(pkgs, config):
+    expand_attrs = config["expand_attrs"]  # {attr_name: {heading: bool}}
+
+    groups = group_packages(pkgs)
+    common_items = []
+    named_sections = []  # List of (section_name, items)
+
+    for group_key, items in groups.items():
+        count = len(items)
+        desc_set = {item.get("description", "") for item in items}
+        desc_count = len(desc_set)
+
+        if group_key in expand_attrs:
+            if expand_attrs[group_key].get("heading", True):
+                named_sections.append((group_key, items))
+            else:
+                common_items.extend(items)
+        elif count == 1:
+            common_items.append(items[0])
+        elif desc_count <= 1:
+            common_items.append(
+                {
+                    "path": group_key,
+                    "pname": group_key,
+                    "version": "",
+                    "homepage": items[0].get("homepage", ""),
+                    "description": items[0].get("description", ""),
+                    "license": items[0].get("license", {}),
+                }
+            )
+        else:
+            named_sections.append((group_key, items))
+
+    output_parts = []
+    if common_items:
+        output_parts.append("### Common\n")
+        output_parts.append(mk_table(common_items))
+
+    for section_name, items in named_sections:
+        output_parts.append(f"### {section_name}\n")
+        output_parts.append(mk_table(items))
+
+    return "\n".join(output_parts)
+
+
+BEGIN_MARKER = "<!-- BEGIN_PACKAGE_TABLE -->"
+END_MARKER = "<!-- END_PACKAGE_TABLE -->"
+
+
+def inject_into_readme(table_md):
+    if README.exists():
+        content = README.read_text()
+    else:
+        content = ""
+
+    if BEGIN_MARKER in content and END_MARKER in content:
+        before = content.split(BEGIN_MARKER)[0]
+        after = content.split(END_MARKER, 1)[1]
+        # Preserve trailing content after END_MARKER marker properly
+        if after.startswith("\n"):
+            after = after[1:]
+        new_content = f"{before}{BEGIN_MARKER}\n{table_md}\n{END_MARKER}\n{after}"
+    else:
+        new_content = f"{content}\n\n{BEGIN_MARKER}\n{table_md}\n{END_MARKER}\n"
+
+    README.write_text(new_content)
+
+
+def main():
+    print("==> Evaluating package metadata via nix...")
+    pkgs = eval_packages()
+    print(f"==> Found {len(pkgs)} derivations")
+
+    config = load_config()
+
+    print("==> Generating markdown tables...")
+    table_md = generate_markdown(pkgs, config)
+
+    print("==> Injecting table into README...")
+    inject_into_readme(table_md)
+
+    print(f"==> Done. README updated at {README}")
+
+
+if __name__ == "__main__":
+    main()
scripts/gen-readme.sh
@@ -1,173 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
-README="$REPO_DIR/README.md"
-TEMP_TABLE="${TMPDIR:-/tmp}/nur-packages-readme-table.md"
-SYSTEM="${NIX_SYSTEM:-x86_64-linux}"
-
-echo "==> Evaluating package metadata via nix..."
-
-json=$(nix eval --impure --expr "
-let
-  flake = builtins.getFlake (toString $REPO_DIR);
-  pkgs = flake.legacyPackages.\"${SYSTEM}\";
-
-  reserved = [
-    \"lib\" \"nixosModules\" \"overlays\"
-    \"homeModules\" \"darwinModules\" \"flakeModules\"
-    \"callPackage\" \"newScope\" \"overrideScope\" \"packages\"
-    \"override\" \"overrideDerivation\"
-  ];
-
-  resolveLic = lic:
-    let
-      go = l:
-        if l == null then { shortName = null; spdxId = null; url = null; }
-        else if builtins.isList l then go (builtins.head l)
-        else if builtins.isString l then { shortName = l; spdxId = null; url = null; }
-        else {
-          shortName = l.shortName or null;
-          spdxId = l.spdxId or null;
-          url = l.url or null;
-        };
-    in go lic;
-
-  getLic = meta:
-    resolveLic (meta.license or meta.licence or null);
-
-  isDer = x: builtins.isAttrs x && x ? type && x.type == \"derivation\";
-  isScope = x: builtins.isAttrs x && x ? callPackage && x ? newScope && !isDer x;
-
-  reservedSet = builtins.listToAttrs (builtins.map (n: { name = n; value = true; }) reserved);
-
-  collect = prefix: attrs:
-    let
-      names = builtins.filter (n: !(reservedSet.\${n} or false)) (builtins.attrNames attrs);
-    in
-      builtins.concatMap (name:
-        let
-          v = attrs.\${name};
-          fullPrefix = if prefix == \"\" then name else \"\${prefix}.\${name}\";
-          pkgInfo = {
-            path = fullPrefix;
-            pname = v.pname or v.name or \"\";
-            version = v.version or \"\";
-            homepage = v.meta.homepage or \"\";
-            description = v.meta.description or \"\";
-            license = getLic v.meta;
-          };
-        in
-          if isDer v then [ pkgInfo ]
-          else if isScope v then collect fullPrefix v
-          else []
-      ) names;
-in
-  collect \"\" pkgs
-" --json 2>/dev/null)
-
-echo "==> Found $(echo "$json" | jq length) derivations"
-
-echo "==> Generating markdown tables..."
-
-echo "$json" | jq -r '
-
-  def fmt_license:
-    if .spdxId then
-      if .url and (.url | length > 0) then "[\(.spdxId)](\(.url))"
-      else .spdxId end
-    elif .shortName then
-      if .shortName == "unfree" then "**Unfree**"
-      elif .url and (.url | length > 0) then "[\(.shortName)](\(.url))"
-      else .shortName end
-    else "Not specified"
-    end;
-
-  def fmt_name($pname; $homepage):
-    if ($pname | length > 0) and ($homepage | length > 0) then "[\($pname)](\($homepage))"
-    else $pname end;
-
-  def fmt_version:
-    if . and (. | length > 0) then "`\(.)`" else "-" end;
-
-  def fmt_row($item):
-    "| `\($item.path)` | \(fmt_name($item.pname; $item.homepage)) | \($item.version | fmt_version) | \($item.license | fmt_license) | \($item.description) |";
-
-  def mk_table($items):
-    "| Path | Name | Version | License | Description |",
-    "| --- | --- | --- | --- | --- |",
-    ($items[] | fmt_row(.)),
-    "";
-
-  # Group by first path segment
-  (reduce .[] as $item ({}; .[$item.path | split(".")[0]] += [$item])
-  | to_entries
-  | map(
-      .key as $group_key
-      | .value as $items
-      | ($items | length) as $count
-      | ($items | map(.description) | unique | length) as $desc_count
-      | if $count == 1 then
-          { section: "Common", items: $items }
-        elif $desc_count <= 1 then
-          {
-            section: "Common",
-            items: [{
-              path: $group_key,
-              pname: $group_key,
-              version: "-",
-              homepage: $items[0].homepage,
-              description: $items[0].description,
-              license: $items[0].license
-            }]
-          }
-        else
-          { section: $group_key, items: $items }
-        end
-      )
-  ) as $groups
-
-  # Combine all Common items
-  | ($groups | map(select(.section == "Common") | .items[]) ) as $common
-  | ($groups | map(select(.section != "Common"))) as $named
-
-  # Generate output
-  | (if ($common | length > 0) then
-       "### Common\n", mk_table($common)
-     else empty end),
-    ($named[] |
-       "### \(.section)\n", mk_table(.items))
-' > "$TEMP_TABLE"
-
-echo "==> Injecting table into README..."
-
-if grep -q '<!-- BEGIN_PACKAGE_TABLE -->' "$README" 2>/dev/null; then
-  awk -v tmp="$TEMP_TABLE" '
-    BEGIN          { printing = 1 }
-    /<!-- BEGIN_PACKAGE_TABLE -->/ {
-      print
-      while ((getline line < tmp) > 0) print line
-      close(tmp)
-      printing = 0
-      next
-    }
-    /<!-- END_PACKAGE_TABLE -->/ {
-      printing = 1
-      print
-      next
-    }
-    printing       { print }
-  ' "$README" > "${README}.tmp" && mv "${README}.tmp" "$README"
-else
-  {
-    head -n -0 "$README"
-    echo ""
-    echo "<!-- BEGIN_PACKAGE_TABLE -->"
-    cat "$TEMP_TABLE"
-    echo "<!-- END_PACKAGE_TABLE -->"
-  } > "${README}.tmp" && mv "${README}.tmp" "$README"
-fi
-
-rm -f "$TEMP_TABLE"
-
-echo "==> Done. README updated at $README"
justfile
@@ -1,4 +1,4 @@
 
 # Generate latest packages list and write to README.md
 gen-readme:
-    bash ./scripts/gen-readme.sh
+    python3 ./scripts/gen-readme.py
README.md
@@ -19,7 +19,7 @@
 | `nocturne` | [nocturne](https://github.com/Jeffser/Nocturne) | `1.2.2` | [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) | An Adwaita Music Player / Library Manager  |
 | `particle-music` | [particle-music](https://github.com/AfalpHy/ParticleMusic) | `2.2.1` | [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) | A cross-platform local music player based on Flutter |
 | `shimmie2` | [shimmie2](https://github.com/shish/shimmie2) | `2.12.2` | [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) | An easy-to-install community image gallery (aka booru) |
-| `spritz-wine-bin` | [spritz-wine-bin](https://github.com/NelloKudo/spritz-wine) | `-` | [MIT](https://spdx.org/licenses/MIT.html) | Spritz-Wine builds for some games  |
+| `spritz-wine-bin` | [spritz-wine-bin](https://github.com/NelloKudo/spritz-wine) | - | [MIT](https://spdx.org/licenses/MIT.html) | Spritz-Wine builds for some games  |
 
 ### helixPlugins