#!/usr/bin/env python3 """Build or preview Labyricorn with validated remote project content.""" from __future__ import annotations import argparse import json import os from pathlib import Path import shutil import subprocess import sys import uuid from project_sources import ProjectSourceError, sync_projects from feed_generator import prepare_feed_assets IGNORED_NAMES = { ".cache", ".git", ".lektor", "__pycache__", "build", "dist", } def copy_site_source(site_root: Path, workspace: Path) -> None: for source in site_root.iterdir(): if source.name in IGNORED_NAMES: continue destination = workspace / source.name if source.is_dir(): shutil.copytree( source, destination, ignore=shutil.ignore_patterns(*IGNORED_NAMES, "*.pyc"), ) elif source.is_file(): shutil.copy2(source, destination) def git_state(site_root: Path) -> tuple[str, bool]: try: commit = subprocess.run( ["git", "-C", str(site_root), "rev-parse", "HEAD"], check=True, capture_output=True, text=True, ).stdout.strip() dirty = bool( subprocess.run( ["git", "-C", str(site_root), "status", "--porcelain"], check=True, capture_output=True, text=True, ).stdout.strip() ) return commit, dirty except (subprocess.CalledProcessError, OSError): return "unknown", True def find_project_file(workspace: Path) -> Path: project_files = list(workspace.glob("*.lektorproject")) if len(project_files) != 1: raise ProjectSourceError( f"expected exactly one .lektorproject file; found {len(project_files)}" ) return project_files[0] def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--site-root", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--cache-path", type=Path) parser.add_argument("--output-path", type=Path) parser.add_argument("--lektor", default="lektor") parser.add_argument("--serve", action="store_true") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=5000) args = parser.parse_args() if args.serve == bool(args.output_path): parser.error("choose exactly one of --serve or --output-path") site_root = args.site_root.resolve() cache_root = (args.cache_path or site_root / ".cache" / "project-sources").resolve() output_path = args.output_path.resolve() if args.output_path else None try: cache_root.mkdir(parents=True, exist_ok=True) workspace = cache_root / f"labyricorn-build-{uuid.uuid4().hex}" workspace.mkdir() try: copy_site_source(site_root, workspace) manifest = sync_projects(site_root, workspace, cache_root) site_commit, dirty = git_state(site_root) manifest["site_commit"] = site_commit manifest["site_dirty"] = dirty project_file = find_project_file(workspace) prepare_feed_assets(project_file, workspace) if args.serve: command = [ args.lektor, "--project", str(project_file), "server", "--host", args.host, "--port", str(args.port), ] return subprocess.run(command, check=False).returncode assert output_path is not None output_path.mkdir(parents=True, exist_ok=True) subprocess.run( [ args.lektor, "--project", str(project_file), "build", "--output-path", str(output_path), ], check=True, ) (output_path / ".labyricorn-projects.json").write_text( json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) finally: shutil.rmtree(workspace, ignore_errors=True) except (ProjectSourceError, subprocess.CalledProcessError, OSError) as exc: print(f"project-build: ERROR: {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())