New build script
This commit is contained in:
parent
237169c606
commit
29505c6635
285
tools/build/main.py
Normal file
285
tools/build/main.py
Normal file
@ -0,0 +1,285 @@
|
||||
from typing import TypedDict
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tomllib
|
||||
import shutil
|
||||
import json
|
||||
import os
|
||||
|
||||
# TODO:
|
||||
# list handouts without solutions
|
||||
# list handouts that are not published
|
||||
# lint: always commit without `nosolutions`
|
||||
# use latexmk
|
||||
|
||||
ROOT: Path = Path(os.getcwd())
|
||||
|
||||
### CONFIGURATION
|
||||
OUT_DIR: Path = ROOT / "output"
|
||||
TYPST_PATH = "typst"
|
||||
XETEX_PATH = "xelatex"
|
||||
### END CONFIGURATION
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(f"[BUILD.PY] {msg}")
|
||||
|
||||
|
||||
log(f"Running in {ROOT}")
|
||||
if not ROOT.is_dir():
|
||||
log("Root is not a directory, cannot continue")
|
||||
exit(1)
|
||||
|
||||
log(f"Output dir is {OUT_DIR}")
|
||||
if OUT_DIR.exists():
|
||||
log("Output dir exists, removing")
|
||||
shutil.rmtree(OUT_DIR)
|
||||
|
||||
OUT_DIR.mkdir(parents=True)
|
||||
|
||||
|
||||
IndexEntry = TypedDict(
|
||||
"IndexEntry",
|
||||
{"title": str, "group": str, "handout_file": str, "solutions_file": str | None},
|
||||
)
|
||||
|
||||
|
||||
MetaToml = TypedDict(
|
||||
"MetaToml", {"title": str, "publish_handout": bool, "publish_solutions": bool}
|
||||
)
|
||||
|
||||
|
||||
def read_meta_toml(file: Path) -> MetaToml:
|
||||
with file.open("rb") as f:
|
||||
base = tomllib.load(f)
|
||||
|
||||
meta = base.get("metadata")
|
||||
if not isinstance(meta, dict):
|
||||
log("Invalid meta.toml: `metadata` should be a table")
|
||||
exit(1)
|
||||
|
||||
title = meta.get("title")
|
||||
if not isinstance(title, str):
|
||||
log("Invalid meta.toml: `title` should be a string")
|
||||
exit(1)
|
||||
|
||||
pub = base.get("publish")
|
||||
if not isinstance(pub, dict):
|
||||
log("Invalid meta.toml: `publish` should be a table")
|
||||
exit(1)
|
||||
|
||||
pub_handout = pub.get("handout")
|
||||
if not isinstance(pub_handout, bool):
|
||||
log("Invalid meta.toml: `publish.handout` should be a boolean")
|
||||
exit(1)
|
||||
|
||||
pub_solutions = pub.get("solutions")
|
||||
if not isinstance(pub_solutions, bool):
|
||||
log("Invalid meta.toml: `publish.solutions` should be a boolean")
|
||||
exit(1)
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"publish_handout": pub_handout,
|
||||
"publish_solutions": pub_solutions,
|
||||
}
|
||||
|
||||
|
||||
def log_error(res):
|
||||
stdout = res.stdout.decode("utf-8").strip()
|
||||
stderr = res.stderr.decode("utf-8").strip()
|
||||
|
||||
log(f"Build failed with code {res.returncode}")
|
||||
|
||||
if stdout == "":
|
||||
log("Stdout: <empty>")
|
||||
else:
|
||||
log("Stdout:")
|
||||
print(stdout)
|
||||
|
||||
if stderr == "":
|
||||
log("Stderr: <empty>")
|
||||
else:
|
||||
log("Stderr:")
|
||||
print(stderr)
|
||||
|
||||
exit(1)
|
||||
|
||||
|
||||
def build_typst(source_dir: Path, out_subdir: Path) -> IndexEntry | None:
|
||||
if not (source_dir / "main.typ").is_file():
|
||||
# log(f"No main.typ, skipping {source_dir}")
|
||||
return None
|
||||
|
||||
meta_path = source_dir / "meta.toml"
|
||||
if not meta_path.is_file():
|
||||
log(f"No meta.toml, skipping {source_dir}")
|
||||
return None
|
||||
meta = read_meta_toml(meta_path)
|
||||
|
||||
# Do nothing if not published
|
||||
if not meta["publish_handout"]:
|
||||
return None
|
||||
|
||||
# Build handout
|
||||
log(f"Building typst (handout) : {source_dir}")
|
||||
|
||||
out = OUT_DIR / out_subdir
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
res = subprocess.run(
|
||||
[
|
||||
TYPST_PATH,
|
||||
"compile",
|
||||
"--ignore-system-fonts",
|
||||
"main.typ",
|
||||
"--input",
|
||||
"show_solutions=false",
|
||||
f"{out}/{meta['title']}.pdf",
|
||||
],
|
||||
cwd=source_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
if res.returncode != 0:
|
||||
log_error(res)
|
||||
|
||||
# Build solutions
|
||||
if meta["publish_solutions"]:
|
||||
log(f"Building typst (solutions): {source_dir}")
|
||||
res = subprocess.run(
|
||||
[
|
||||
TYPST_PATH,
|
||||
"compile",
|
||||
"--ignore-system-fonts",
|
||||
"main.typ",
|
||||
f"{out}/{meta['title']}.sols.pdf",
|
||||
],
|
||||
cwd=source_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
if res.returncode != 0:
|
||||
log_error(res)
|
||||
|
||||
return {
|
||||
"title": meta["title"],
|
||||
"group": str(out_subdir),
|
||||
"handout_file": f"{out_subdir}/{meta['title']}.pdf",
|
||||
"solutions_file": (
|
||||
f"{out_subdir}/{meta['title']}.sols.pdf"
|
||||
if meta["publish_solutions"]
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def build_xetex(source_dir: Path, out_subdir: Path) -> IndexEntry | None:
|
||||
if not (source_dir / "main.tex").is_file():
|
||||
# log(f"No main.tex, skipping {source_dir}")
|
||||
return None
|
||||
|
||||
meta_path = source_dir / "meta.toml"
|
||||
if not meta_path.is_file():
|
||||
log(f"No meta.toml, skipping {source_dir}")
|
||||
return None
|
||||
meta = read_meta_toml(meta_path)
|
||||
|
||||
# Do nothing if not published
|
||||
if not meta["publish_handout"]:
|
||||
return None
|
||||
|
||||
# Build handout
|
||||
log(f"Building xetex (handout) : {source_dir}")
|
||||
|
||||
out = OUT_DIR / out_subdir
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
res = subprocess.run(
|
||||
[
|
||||
XETEX_PATH,
|
||||
"-interaction=batchmode",
|
||||
],
|
||||
input=b"\\def\\argNoSolutions{1}\\input{main.tex}",
|
||||
cwd=source_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
shutil.copy(source_dir / "main.pdf", f"{out}/{meta['title']}.pdf")
|
||||
|
||||
if res.returncode != 0:
|
||||
log_error(res)
|
||||
|
||||
# Build solutions
|
||||
if meta["publish_solutions"]:
|
||||
log(f"Building xetex (solutions): {source_dir}")
|
||||
res = subprocess.run(
|
||||
[
|
||||
XETEX_PATH,
|
||||
"-interaction=batchmode",
|
||||
],
|
||||
input=b"\\def\\argYesSolutions{1}\\input{main.tex}",
|
||||
cwd=source_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
shutil.copy(source_dir / "main.pdf", f"{out}/{meta['title']}.sols.pdf")
|
||||
|
||||
if res.returncode != 0:
|
||||
log_error(res)
|
||||
|
||||
return {
|
||||
"title": meta["title"],
|
||||
"group": str(out_subdir),
|
||||
"handout_file": f"{out_subdir}/{meta['title']}.pdf",
|
||||
"solutions_file": (
|
||||
f"{out_subdir}/{meta['title']}.sols.pdf"
|
||||
if meta["publish_solutions"]
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# Try to build handouts in all subdirs of `base`,
|
||||
# skipping those that do not contain a handout.
|
||||
#
|
||||
# This method does _not_ recurse into subdirectories.
|
||||
def build_dir(base: str, out_sub: str, index: list[IndexEntry]):
|
||||
builders = [build_typst, build_xetex]
|
||||
|
||||
for d_str in os.listdir(base):
|
||||
d = base / Path(d_str)
|
||||
if not d.is_dir():
|
||||
continue
|
||||
|
||||
# Try each builder,
|
||||
# stopping at the first success
|
||||
done = False
|
||||
for builder in builders:
|
||||
res = builder(d, Path(out_sub))
|
||||
if res is not None:
|
||||
# Check for duplicate titles
|
||||
for i in index:
|
||||
if i["title"] == res["title"]:
|
||||
log(f'Duplicate title "{i["title"]}", cannot continue')
|
||||
exit(1)
|
||||
# Save to index and exit
|
||||
index.append(res)
|
||||
done = True
|
||||
if done:
|
||||
break
|
||||
|
||||
return index
|
||||
|
||||
|
||||
index: list[IndexEntry] = []
|
||||
|
||||
index.extend(build_dir("Advanced", "Advanced", index))
|
||||
index.extend(build_dir("Intermediate", "Intermediate", index))
|
||||
|
||||
with open(OUT_DIR / "index.json", "w") as f:
|
||||
f.write(json.dumps(index))
|
17
tools/build/ruff.toml
Normal file
17
tools/build/ruff.toml
Normal file
@ -0,0 +1,17 @@
|
||||
exclude = ["venv"]
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
target-version = "py39"
|
||||
|
||||
[lint]
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = []
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
[format]
|
||||
quote-style = "double"
|
||||
indent-style = "tab"
|
||||
skip-magic-trailing-comma = false
|
||||
line-ending = "lf"
|
Loading…
x
Reference in New Issue
Block a user