Skip to content

Instantly share code, notes, and snippets.

@drbh
Created August 8, 2025 15:37
Show Gist options
  • Save drbh/790d6a147d6a852887448242bedec47f to your computer and use it in GitHub Desktop.
Save drbh/790d6a147d6a852887448242bedec47f to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
import json
import subprocess
import sys
from typing import Dict, Set, Tuple
def run_nix_eval(commit: str) -> Dict[str, str]:
"""Run nix eval command with the specified commit and return parsed JSON."""
cmd = [
"nix", "eval",
".#packages.x86_64-linux.python3Packages",
"--override-input", "nixpkgs", f"github:nixos/nixpkgs/{commit}",
"--json"
]
print(f"Running: {' '.join(cmd)}")
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return json.loads(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Error running nix eval: {e}")
print(f"stderr: {e.stderr}")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error parsing JSON output: {e}")
sys.exit(1)
def extract_version(store_path: str) -> str:
"""Extract version from a nix store path."""
parts = store_path.split('-')
version_parts = []
found_python = False
for part in parts:
if part.startswith('python'):
found_python = True
continue
if found_python and part[0].isdigit():
version_parts.append(part)
return '-'.join(version_parts) if version_parts else "unknown"
def print_table(headers, rows):
"""Print a formatted table."""
if not rows:
return
# Calculate column widths
col_widths = [len(h) for h in headers]
for row in rows:
for i, cell in enumerate(row):
col_widths[i] = max(col_widths[i], len(str(cell)))
# Print header
header_line = " | ".join(h.ljust(w) for h, w in zip(headers, col_widths))
print(header_line)
print("-+-".join("-" * w for w in col_widths))
# Print rows
for row in rows:
print(" | ".join(str(cell).ljust(w) for cell, w in zip(row, col_widths)))
def compare_packages(old_commit: str, new_commit: str) -> None:
"""Compare packages between two commits."""
print(f"Comparing packages between:")
print(f" Old: {old_commit}")
print(f" New: {new_commit}")
print()
old_packages = run_nix_eval(old_commit)
new_packages = run_nix_eval(new_commit)
old_keys = set(old_packages.keys())
new_keys = set(new_packages.keys())
added = new_keys - old_keys
removed = old_keys - new_keys
common = old_keys & new_keys
changed = []
unchanged = []
for pkg in common:
old_path = old_packages[pkg]
new_path = new_packages[pkg]
if old_path != new_path:
old_version = extract_version(old_path)
new_version = extract_version(new_path)
changed.append((pkg, old_version, new_version, old_path, new_path))
else:
unchanged.append(pkg)
print(f"\nSummary:")
print(f" Total packages in old: {len(old_packages)}")
print(f" Total packages in new: {len(new_packages)}")
print(f" Added: {len(added)}")
print(f" Removed: {len(removed)}")
print(f" Changed: {len(changed)}")
print(f" Unchanged: {len(unchanged)}")
if added:
print(f"\nAdded packages ({len(added)}):")
rows = []
for pkg in sorted(added):
version = extract_version(new_packages[pkg])
rows.append([pkg, version])
print_table(["Package", "Version"], rows)
if removed:
print(f"\nRemoved packages ({len(removed)}):")
rows = []
for pkg in sorted(removed):
version = extract_version(old_packages[pkg])
rows.append([pkg, version])
print_table(["Package", "Version"], rows)
if changed:
print(f"\nChanged packages ({len(changed)}):")
rows = []
for pkg, old_ver, new_ver, old_path, new_path in sorted(changed):
# Determine if version increased (+), decreased (-), or changed (blank)
diff = ""
if old_ver != "unknown" and new_ver != "unknown":
# Simple heuristic: if new version starts with higher number, it's likely upgraded
old_parts = old_ver.split('.')
new_parts = new_ver.split('.')
try:
if len(old_parts) > 0 and len(new_parts) > 0:
old_major = int(''.join(c for c in old_parts[0] if c.isdigit()) or '0')
new_major = int(''.join(c for c in new_parts[0] if c.isdigit()) or '0')
if new_major > old_major:
diff = "+"
elif new_major < old_major:
diff = "-"
elif len(old_parts) > 1 and len(new_parts) > 1:
old_minor = int(''.join(c for c in old_parts[1] if c.isdigit()) or '0')
new_minor = int(''.join(c for c in new_parts[1] if c.isdigit()) or '0')
if new_minor > old_minor:
diff = "+"
elif new_minor < old_minor:
diff = "-"
except:
# If parsing fails, leave blank
pass
rows.append([pkg, old_ver, new_ver, diff])
print_table(["Package", "Old Version", "New Version", ""], rows)
if "--verbose" in sys.argv:
print(f"\nDetailed paths for changed packages:")
rows = []
for pkg, old_ver, new_ver, old_path, new_path in sorted(changed):
rows.append([pkg, "old", old_path])
rows.append(["", "new", new_path])
print_table(["Package", "Type", "Path"], rows)
if unchanged and "--show-unchanged" in sys.argv:
print(f"\nUnchanged packages ({len(unchanged)}):")
rows = []
for pkg in sorted(unchanged):
version = extract_version(old_packages[pkg])
rows.append([pkg, version])
print_table(["Package", "Version"], rows)
def main():
old_commit = "d38025438a6ee456758dc03188ca6873a415463b"
new_commit = "679aa6b2219d272ae97f00adbc3092581680243"
if len(sys.argv) > 2:
old_commit = sys.argv[1]
new_commit = sys.argv[2]
compare_packages(old_commit, new_commit)
if __name__ == "__main__":
print("Nix Package Comparison Tool")
print("Usage: python compare_nix_packages.py [old_commit] [new_commit] [--verbose] [--show-unchanged]")
print()
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment