#!/usr/bin/env -S uv run # /// script # requires-python = ">=3.10" # dependencies = [ # "pandas", # "openpyxl", # ] # /// import subprocess import os from pathlib import Path from datetime import datetime import pandas as pd import sys def get_git_sha(): """Get the current git SHA.""" try: result = subprocess.run( ["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True, check=True ) return result.stdout.strip() except subprocess.CalledProcessError: return "unknown" def is_gitignored(path, repo_root): """Check if a path is gitignored.""" try: result = subprocess.run( ["git", "check-ignore", str(path)], cwd=repo_root, capture_output=True, timeout=5 ) # Exit code 0 means the path is ignored return result.returncode == 0 except (subprocess.CalledProcessError, subprocess.TimeoutExpired): return False def should_exclude(path, repo_root): """Check if path should be excluded.""" exclude_patterns = ['.venv', 'node_modules', '.next', '__pycache__'] if any(pattern in str(path) for pattern in exclude_patterns): return True return is_gitignored(path, repo_root) def find_python_projects(root_dir): """Find all Python projects with pyproject.toml and uv.lock.""" projects = [] for pyproject in Path(root_dir).rglob("pyproject.toml"): if should_exclude(pyproject, root_dir): continue uv_lock = pyproject.parent / "uv.lock" if uv_lock.exists(): projects.append(pyproject.parent) return projects def find_nodejs_projects(root_dir): """Find all Node.js projects with package.json.""" projects = [] for package_json in Path(root_dir).rglob("package.json"): if should_exclude(package_json, root_dir): continue projects.append(package_json.parent) return projects def check_pip_licenses_installed(project_dir): """Check if pip-licenses is available in the project.""" try: result = subprocess.run( ["uv", "run", "pip-licenses", "--version"], cwd=project_dir, capture_output=True, text=True, timeout=30 ) return result.returncode == 0 except (subprocess.CalledProcessError, subprocess.TimeoutExpired): return False def install_pip_licenses(project_dir): """Install pip-licenses as a dev dependency.""" print(f" Installing pip-licenses in {project_dir.name}...") try: subprocess.run( ["uv", "add", "--dev", "pip-licenses"], cwd=project_dir, check=True, capture_output=True ) return True except subprocess.CalledProcessError as e: print(f" Error installing pip-licenses: {e}") return False def extract_python_licenses(project_dir, output_dir, date_str, git_sha): """Extract licenses from a Python project.""" folder_name = project_dir.name output_file = output_dir / f"{date_str}-{folder_name}-{git_sha}-py-licenses.csv" print(f"Processing Python project: {project_dir.relative_to(Path.cwd())}") # Check if pip-licenses is installed if not check_pip_licenses_installed(project_dir): print(f" pip-licenses not found, installing...") if not install_pip_licenses(project_dir): print(f" Skipping {folder_name} due to installation failure") return None try: result = subprocess.run( ["uv", "run", "pip-licenses", "--format=csv", "--with-urls", "--with-description"], cwd=project_dir, capture_output=True, text=True, check=True, timeout=120 ) output_file.write_text(result.stdout) print(f" ✓ Saved to {output_file.name}") return output_file except subprocess.CalledProcessError as e: print(f" Error extracting Python licenses: {e}") return None except subprocess.TimeoutExpired: print(f" Timeout extracting Python licenses") return None def extract_nodejs_licenses(project_dir, output_dir, date_str, git_sha): """Extract licenses from a Node.js project.""" folder_name = project_dir.name output_file = output_dir / f"{date_str}-{folder_name}-{git_sha}-node-licenses.csv" print(f"Processing Node.js project: {project_dir.relative_to(Path.cwd())}") try: result = subprocess.run( ["pnpx", "license-checker", "--csv"], cwd=project_dir, capture_output=True, text=True, check=True, timeout=120 ) output_file.write_text(result.stdout) print(f" ✓ Saved to {output_file.name}") return output_file except subprocess.CalledProcessError as e: print(f" Error extracting Node.js licenses: {e}") return None except subprocess.TimeoutExpired: print(f" Timeout extracting Node.js licenses") return None def compile_to_excel(csv_files, output_dir, date_str): """Compile all CSV files into a single Excel file with multiple sheets.""" if not csv_files: print("No CSV files to compile") return excel_file = output_dir / f"{date_str}-compiled-licenses.xlsx" print(f"\nCompiling results to {excel_file.name}...") with pd.ExcelWriter(excel_file, engine='openpyxl') as writer: for csv_file in csv_files: try: df = pd.read_csv(csv_file) # Create sheet name from filename (remove date and git sha prefix) sheet_name = csv_file.stem # Truncate sheet name if too long (Excel limit is 31 chars) if len(sheet_name) > 31: sheet_name = sheet_name[:31] df.to_excel(writer, sheet_name=sheet_name, index=False) print(f" ✓ Added sheet: {sheet_name}") except Exception as e: print(f" Error adding {csv_file.name}: {e}") print(f"\n✓ Compilation complete: {excel_file}") def main(): # Setup repo_root = Path.cwd() output_dir = repo_root / "licenses" / "results" output_dir.mkdir(parents=True, exist_ok=True) date_str = datetime.now().strftime("%Y%m%d") git_sha = get_git_sha() print(f"License Audit") print(f"Date: {date_str}") print(f"Git SHA: {git_sha}") print(f"Repository: {repo_root}\n") # Find projects print("Discovering projects...") python_projects = find_python_projects(repo_root) nodejs_projects = find_nodejs_projects(repo_root) print(f"Found {len(python_projects)} Python projects") print(f"Found {len(nodejs_projects)} Node.js projects\n") # Extract licenses csv_files = [] print("=" * 60) print("Extracting Python licenses...") print("=" * 60) for project in python_projects: result = extract_python_licenses(project, output_dir, date_str, git_sha) if result: csv_files.append(result) print("\n" + "=" * 60) print("Extracting Node.js licenses...") print("=" * 60) for project in nodejs_projects: result = extract_nodejs_licenses(project, output_dir, date_str, git_sha) if result: csv_files.append(result) # Compile results print("\n" + "=" * 60) compile_to_excel(csv_files, output_dir, date_str) print(f"\n✓ License audit complete!") print(f" CSV files: {len(csv_files)}") print(f" Results directory: {output_dir}") if __name__ == "__main__": main()