cl-ds / src /cl_macros /ext /source_fetcher.py
j14i's picture
977 CL macro transformation examples: CL-native pipeline with SBCL verification
d69fc90 verified
"""Fetch library sources — clone from GitHub or resolve local Quicklisp path."""
from __future__ import annotations
import subprocess
from pathlib import Path
from cl_macros.ext.library_index import LibraryInfo
REPOS_DIR = Path("data/repos")
class SourceFetcher:
def __init__(self, repos_dir: Path | None = None):
self.repos_dir = repos_dir or REPOS_DIR
def fetch(self, lib: LibraryInfo) -> Path:
"""Clone or update library, return path to source root."""
dest = self.repos_dir / lib.name
dest.parent.mkdir(parents=True, exist_ok=True)
if dest.exists() and (dest / ".git").exists():
self._update(dest)
else:
self._clone(lib.repo, dest)
return dest
def get_source_files(self, lib: LibraryInfo) -> list[Path]:
"""Return all .lisp source files from a library."""
root = self.fetch(lib)
src_root = root / lib.cl_source_dir if lib.cl_source_dir else root
if not src_root.exists():
src_root = root
return sorted(
p for p in src_root.rglob("*.lisp")
if not p.name.endswith("-test.lisp") and "t/" not in str(p.relative_to(root))
or self._is_main_test_dir(p, root)
)
def get_test_files(self, lib: LibraryInfo) -> list[Path]:
"""Return test .lisp files from a library."""
root = self.fetch(lib)
test_files = list(root.rglob("t/*.lisp")) + list(root.rglob("tests/*.lisp"))
return sorted(set(test_files))
def get_all_lisp_files(self, lib: LibraryInfo) -> list[Path]:
"""Return all .lisp files (source + test)."""
return sorted(set(self.get_source_files(lib) + self.get_test_files(lib)))
def _clone(self, url: str, dest: Path) -> None:
try:
subprocess.run(
["git", "clone", "--depth", "1", url, str(dest)],
check=True,
capture_output=True,
timeout=120,
)
except subprocess.CalledProcessError as e:
raise RuntimeError(
f"Failed to clone {url}: {e.stderr.decode() if e.stderr else e}"
) from e
def _update(self, dest: Path) -> None:
try:
subprocess.run(
["git", "-C", str(dest), "pull", "--ff-only"],
check=True,
capture_output=True,
timeout=60,
)
except subprocess.CalledProcessError:
pass # Stale clone is better than nothing
@staticmethod
def _is_main_test_dir(p: Path, root: Path) -> bool:
"""Allow test files if they're in the main test directory."""
rel = p.relative_to(root)
parts = rel.parts
return len(parts) > 0 and parts[0] == "t"