from __future__ import annotations
import argparse
import os
import re
import shutil
import subprocess
import sys
import sysconfig
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
MANIFEST = REPO / "bindings" / "python" / "Cargo.toml"
PACKAGE = REPO / "python" / "macrame"
CDYLIB = {
"win32": "_macrame.dll",
"darwin": "lib_macrame.dylib",
}.get(sys.platform, "lib_macrame.so")
def extension_suffix() -> str:
if sys.platform == "win32":
return ".pyd"
return sysconfig.get_config_var("EXT_SUFFIX") or ".so"
def declared_version() -> str:
text = MANIFEST.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.M)
if not match:
raise SystemExit(f"no version in {MANIFEST}")
return match.group(1)
def newest_source_mtime() -> tuple[float, Path]:
newest = (0.0, REPO)
for root in (REPO / "src", REPO / "bindings" / "python" / "src"):
for path in root.rglob("*.rs"):
stamp = path.stat().st_mtime
if stamp > newest[0]:
newest = (stamp, path)
for manifest in (REPO / "Cargo.toml", MANIFEST):
stamp = manifest.stat().st_mtime
if stamp > newest[0]:
newest = (stamp, manifest)
return newest
def build() -> None:
cmd = [
"cargo",
"build",
"--release",
"-p",
"macrame-py",
"--features",
"extension-module",
]
print("$ " + " ".join(cmd), flush=True)
proc = subprocess.run(cmd, cwd=REPO)
if proc.returncode != 0:
raise SystemExit(f"cargo build failed (exit {proc.returncode})")
def install() -> Path:
built = REPO / "target" / "release" / CDYLIB
if not built.exists():
raise SystemExit(
f"cargo reported success and {built} does not exist. If this "
f"platform names its cdylib something else, CDYLIB in this file is "
f"what needs to know."
)
dest = PACKAGE / f"_macrame{extension_suffix()}"
shutil.copyfile(built, dest)
os.utime(dest, None)
return dest
def verify(dest: Path) -> None:
expected = declared_version()
proc = subprocess.run(
[sys.executable, "-c", "import macrame; print(macrame.__version__)"],
cwd=REPO,
capture_output=True,
text=True,
env={**os.environ, "PYTHONPATH": str(REPO / "python")},
)
if proc.returncode != 0:
raise SystemExit(
f"the extension was installed to {dest} and does not import:\n"
f"{proc.stdout}{proc.stderr}"
)
got = proc.stdout.strip()
if got != expected:
raise SystemExit(
f"installed {dest} reports {got!r}, and "
f"bindings/python/Cargo.toml declares {expected!r}. The build did "
f"not use the manifest this script read, which should not be "
f"possible — check for a second checkout on PYTHONPATH."
)
print(f"ok: macrame {got} at {dest.relative_to(REPO)}")
def staleness() -> str | None:
dest = PACKAGE / f"_macrame{extension_suffix()}"
fix = "python scripts/build_python_ext.py"
if not dest.exists():
return (
f"no built extension at {dest.relative_to(REPO)}: the suite would "
f"import nothing, or something else's. Build it:\n {fix}"
)
newest, source = newest_source_mtime()
if dest.stat().st_mtime < newest:
minutes = round((newest - dest.stat().st_mtime) / 60.0)
age = "a minute" if minutes == 1 else f"{minutes} minutes"
return (
f"{dest.relative_to(REPO)} was built {age} before "
f"{source.relative_to(REPO)} was last edited, so the suite would "
f"measure code that is not in the tree. Rebuild it:\n {fix}"
)
return None
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument(
"--check",
action="store_true",
help="do not build; report whether the installed extension is current",
)
args = parser.parse_args()
if args.check:
stale = staleness()
if stale:
print(stale, file=sys.stderr)
return 1
print("ok: the installed extension is current")
return 0
build()
dest = install()
verify(dest)
return 0
if __name__ == "__main__":
sys.exit(main())