147 lines
4.4 KiB
Python
147 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Build script for CV and résumé.
|
|
|
|
Loads YAML data files, renders Jinja2 LaTeX templates with a LaTeX-safe
|
|
delimiter configuration, and compiles both documents via xelatex.
|
|
|
|
Usage:
|
|
python build.py cv # build CV only
|
|
python build.py resume # build website résumé only
|
|
python build.py resume_ats # build ATS-optimized résumé only
|
|
python build.py all # build cv + resume (default; not ats)
|
|
python build.py all_all # build cv + resume + resume_ats
|
|
"""
|
|
|
|
import sys
|
|
import subprocess
|
|
import shutil
|
|
from pathlib import Path
|
|
from datetime import date
|
|
|
|
import yaml
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
|
|
ROOT = Path(__file__).parent
|
|
DATA_DIR = ROOT / "data"
|
|
TEMPLATE_DIR = ROOT / "templates"
|
|
OUTPUT_DIR = ROOT / "output"
|
|
BUILD_DIR = ROOT / "build"
|
|
|
|
|
|
def load_all_data():
|
|
"""Load every .yml file in data/ and merge into a single dict."""
|
|
data = {}
|
|
for yml_file in sorted(DATA_DIR.glob("*.yml")):
|
|
with open(yml_file) as f:
|
|
loaded = yaml.safe_load(f)
|
|
if not loaded:
|
|
continue
|
|
stem = yml_file.stem
|
|
if stem == "personal":
|
|
# personal.yml's contents become the `personal` key
|
|
data["personal"] = loaded
|
|
elif stem == "skills":
|
|
# skills.yml has multiple top-level keys — merge all directly
|
|
data.update(loaded)
|
|
else:
|
|
# everything else merges directly (education.yml has key
|
|
# 'education', experience.yml has key 'experience', etc.)
|
|
data.update(loaded)
|
|
data["build_date"] = date.today().strftime("%B %Y")
|
|
return data
|
|
|
|
|
|
def make_env():
|
|
"""Jinja2 environment configured for LaTeX-safe delimiters."""
|
|
return Environment(
|
|
loader=FileSystemLoader(str(TEMPLATE_DIR)),
|
|
block_start_string="((*",
|
|
block_end_string="*))",
|
|
variable_start_string="(((",
|
|
variable_end_string=")))",
|
|
comment_start_string="((#",
|
|
comment_end_string="#))",
|
|
trim_blocks=True,
|
|
lstrip_blocks=True,
|
|
autoescape=False,
|
|
)
|
|
|
|
|
|
def render(template_name, data):
|
|
env = make_env()
|
|
template = env.get_template(template_name)
|
|
return template.render(**data)
|
|
|
|
|
|
def compile_tex(tex_path, output_name):
|
|
"""Run xelatex twice (for cross-references) and move the PDF to output/."""
|
|
BUILD_DIR.mkdir(exist_ok=True)
|
|
OUTPUT_DIR.mkdir(exist_ok=True)
|
|
|
|
# Copy shared templates into build dir so \input paths resolve cleanly
|
|
build_template_dir = BUILD_DIR / "templates"
|
|
if build_template_dir.exists():
|
|
shutil.rmtree(build_template_dir)
|
|
shutil.copytree(TEMPLATE_DIR, build_template_dir)
|
|
|
|
# Write .tex into build dir
|
|
tex_in_build = BUILD_DIR / f"{output_name}.tex"
|
|
tex_in_build.write_text(tex_path.read_text())
|
|
|
|
# Compile (twice, as is tradition)
|
|
for _ in range(2):
|
|
result = subprocess.run(
|
|
["xelatex", "-interaction=nonstopmode", "-halt-on-error",
|
|
f"-output-directory={BUILD_DIR}", str(tex_in_build)],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
print(f"ERROR compiling {output_name}.tex:")
|
|
print(result.stdout[-2000:])
|
|
sys.exit(1)
|
|
|
|
# Move PDF to output/
|
|
pdf_src = BUILD_DIR / f"{output_name}.pdf"
|
|
pdf_dst = OUTPUT_DIR / f"{output_name}.pdf"
|
|
shutil.copy(pdf_src, pdf_dst)
|
|
print(f" → {pdf_dst}")
|
|
|
|
|
|
def build_one(name):
|
|
"""Build a single document by name ('cv' or 'resume')."""
|
|
print(f"Building {name}...")
|
|
data = load_all_data()
|
|
tex_source = render(f"{name}.tex.j2", data)
|
|
|
|
# Write rendered .tex into build dir
|
|
BUILD_DIR.mkdir(exist_ok=True)
|
|
tex_path = BUILD_DIR / f"{name}.tex"
|
|
tex_path.write_text(tex_source)
|
|
|
|
compile_tex(tex_path, name)
|
|
|
|
|
|
def main():
|
|
target = sys.argv[1] if len(sys.argv) > 1 else "all"
|
|
if target in ("cv", "resume", "resume_ats"):
|
|
build_one(target)
|
|
elif target == "all":
|
|
build_one("cv")
|
|
build_one("resume")
|
|
elif target == "all_all":
|
|
build_one("cv")
|
|
build_one("resume")
|
|
build_one("resume_ats")
|
|
else:
|
|
print(f"Unknown target: {target}")
|
|
print("Usage: python build.py [cv|resume|resume_ats|all|all_all]")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|