#!/usr/bin/env python3 """ vendor.py — minimal but extensible vendoring tool for GitHub files Examples: # Fetch and record to manifest vendor.py --github user/repo path/to/file.txt # Fetch from specific commit vendor.py --github user/repo path/to/file.txt --commit abc1234 # Update all vendored files vendor.py update """ import argparse import os import sys import json import urllib.request from pathlib import Path DEFAULT_VENDOR_DIR = "vendor" MANIFEST_FILE = "vendor.json" def load_manifest(): if Path(MANIFEST_FILE).is_file(): with open(MANIFEST_FILE, "r", encoding="utf-8") as f: return json.load(f) return [] def save_manifest(entries): with open(MANIFEST_FILE, "w", encoding="utf-8") as f: json.dump(entries, f, indent=2) print(f"Manifest saved to {MANIFEST_FILE}") def fetch_github_file(repo, file_path, ref, out_path): """Fetch a file from GitHub and save to out_path.""" raw_url = f"https://raw.githubusercontent.com/{repo}/{ref}/{file_path}" print(f"Fetching: {raw_url}") try: with urllib.request.urlopen(raw_url) as resp: data = resp.read() except Exception as e: print(f"Error: Failed to fetch from {raw_url}\n{e}", file=sys.stderr) sys.exit(1) os.makedirs(os.path.dirname(out_path), exist_ok=True) with open(out_path, "wb") as f: f.write(data) print(f"Saved to: {out_path}") def add_to_manifest(provider, repo, file_path, ref, out_path): manifest = load_manifest() entry = { "provider": provider, "repo": repo, "file_path": file_path, "ref": ref, "out_path": out_path } # Avoid duplicates if entry not in manifest: manifest.append(entry) save_manifest(manifest) def update_all(): manifest = load_manifest() if not manifest: print("No vendored files found in manifest.") return for entry in manifest: if entry["provider"] == "github": fetch_github_file( entry["repo"], entry["file_path"], entry["ref"], entry["out_path"] ) else: print(f"Unknown provider: {entry['provider']}", file=sys.stderr) def parse_args(): parser = argparse.ArgumentParser( description="Vendor single files from GitHub (with manifest tracking)." ) subparsers = parser.add_subparsers(dest="command") # Fetch command fetch_parser = subparsers.add_parser( "fetch", help="Fetch a file and record in manifest." ) fetch_parser.add_argument( "--github", nargs=2, metavar=("USER/REPO", "FILE_PATH"), help="Fetch from GitHub repository (example: user/repo path/to/file.txt).", required=True ) fetch_parser.add_argument( "--branch", help="Branch to fetch from (default: master)." ) fetch_parser.add_argument( "--tag", help="Tag to fetch from." ) fetch_parser.add_argument( "--commit", help="Commit SHA to fetch from." ) fetch_parser.add_argument( "--out", help=f"Output path (default: {DEFAULT_VENDOR_DIR}/)." ) # Update command subparsers.add_parser("update", help="Update all files in manifest.") return parser.parse_args() def main(): args = parse_args() if args.command == "fetch": repo, file_path = args.github # Pick ref in priority order ref = args.commit or args.tag or args.branch or "master" out_path = args.out or os.path.join( DEFAULT_VENDOR_DIR, os.path.basename(file_path) ) fetch_github_file(repo, file_path, ref, out_path) add_to_manifest("github", repo, file_path, ref, out_path) elif args.command == "update": update_all() else: print("No command specified. Use -h for help.", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()