velai / scripts /build_release.py
cansik's picture
Upload folder via script
df13642 verified
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import shutil
from pathlib import Path
from gitignore_parser import handle_negation, rule_from_pattern
def _load_gitignore_rules(gitignore_path: Path) -> list:
"""
Parse a single .gitignore into gitignore_parser IgnoreRule objects.
We intentionally use gitignore_parser's internal rule representation so we can
combine multiple .gitignore files (root + subfolders) while keeping correct
precedence (later rules override earlier ones).
"""
base_dir = gitignore_path.parent
try:
lines = gitignore_path.read_text(encoding="utf-8").splitlines()
except OSError:
return []
rules = []
for raw in lines:
line = raw.strip()
if not line or line.startswith("#"):
continue
rules.append(rule_from_pattern(line, base_path=str(base_dir)))
return rules
def _is_ignored(abs_path: Path, rules: list) -> bool:
# gitignore_parser expects paths as strings; absolute paths are safest here.
return bool(handle_negation(str(abs_path), rules))
def _copy_repo_respecting_gitignores(repo_root: Path, out_dir: Path) -> None:
repo_root = repo_root.resolve()
out_dir = out_dir.resolve()
# If out_dir is inside repo_root, compute its rel path so we can prune traversal into it.
out_rel: Path | None = None
try:
out_rel = out_dir.relative_to(repo_root)
except ValueError:
out_rel = None
# Map directory -> cumulative rules that apply inside that directory.
rules_for_dir: dict[Path, list] = {}
for current_dir, dirnames, filenames in os.walk(repo_root, topdown=True):
current_path = Path(current_dir)
try:
rel_dir = current_path.resolve().relative_to(repo_root)
except ValueError:
continue
# Hard excludes
if rel_dir == Path(".git") or Path(".git") in rel_dir.parents:
dirnames[:] = []
continue
if out_rel is not None and (rel_dir == out_rel or out_rel in rel_dir.parents):
dirnames[:] = []
continue
# Build cumulative rules for this directory: parent rules + local .gitignore rules
parent_rel = rel_dir.parent if rel_dir != Path(".") else None
parent_rules = rules_for_dir.get(parent_rel, []) if parent_rel is not None else []
local_rules = []
local_gitignore = current_path / ".gitignore"
if local_gitignore.is_file():
local_rules = _load_gitignore_rules(local_gitignore)
current_rules = [*parent_rules, *local_rules]
rules_for_dir[rel_dir] = current_rules
# Filter directories in-place to control traversal
kept_dirs: list[str] = []
for d in dirnames:
rel = rel_dir / d
if rel == Path(".git") or Path(".git") in rel.parents:
continue
if out_rel is not None and (rel == out_rel or out_rel in rel.parents):
continue
abs_path = (repo_root / rel).resolve()
if _is_ignored(abs_path, current_rules):
continue
kept_dirs.append(d)
dirnames[:] = kept_dirs
# Ensure destination dir exists
(out_dir / rel_dir).mkdir(parents=True, exist_ok=True)
# Copy files
for f in filenames:
rel_file = rel_dir / f
abs_file = (repo_root / rel_file).resolve()
if _is_ignored(abs_file, current_rules):
continue
src = repo_root / rel_file
dst = out_dir / rel_file
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def main() -> None:
parser = argparse.ArgumentParser(
description="Build a release directory by copying all files except those ignored by .gitignore files."
)
parser.add_argument("--out-dir", default="out", help="Output directory.")
parser.add_argument("--clean", action="store_true", help="Delete output directory before building.")
args = parser.parse_args()
repo_root = Path.cwd()
out_dir = (repo_root / args.out_dir).resolve()
if args.clean and out_dir.exists():
shutil.rmtree(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
_copy_repo_respecting_gitignores(repo_root=repo_root, out_dir=out_dir)
print(f"Release directory built at: {out_dir}")
if __name__ == "__main__":
main()