|
| 1 | +# |
| 2 | +# PySceneDetect: Python-Based Video Scene Detector |
| 3 | +# --------------------------------------------------------------- |
| 4 | +# [ Site: http://www.bcastell.com/projects/PySceneDetect/ ] |
| 5 | +# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] |
| 6 | +# [ Documentation: http://www.scenedetect.com/docs/ ] |
| 7 | +# |
| 8 | +# Copyright (C) 2026 Brandon Castellano <http://www.bcastell.com>. |
| 9 | +# |
| 10 | +"""Builds all three PySceneDetect distributions into dist/: |
| 11 | +
|
| 12 | + - scenedetect-core: minimal dependencies (numpy only, no OpenCV variant declared, |
| 13 | + no console script) - built from the repo root pyproject.toml |
| 14 | + - scenedetect / scenedetect-headless: the same code plus an OpenCV variant, the |
| 15 | + CLI dependencies, and the `scenedetect` console script |
| 16 | +
|
| 17 | +All three are code-carrying packages built from the repo root, so they share the |
| 18 | +same source, readme, and dynamic version. The scenedetect / scenedetect-headless |
| 19 | +variants are produced by temporarily swapping packaging/variants/pyproject-<name>.toml |
| 20 | +into the repo root (restored afterwards, even on failure). |
| 21 | +
|
| 22 | +Requires `build` (pip install build). Fails if dist/ ends up with any wheel/sdist |
| 23 | +besides the six expected artifacts, so clear stale build artifacts from dist/ first. |
| 24 | +(Other dist/ contents are ignored - e.g. dist/logo/ is tracked website assets.) |
| 25 | +""" |
| 26 | + |
| 27 | +import ast |
| 28 | +import subprocess |
| 29 | +import sys |
| 30 | +from pathlib import Path |
| 31 | + |
| 32 | +ROOT = Path(__file__).resolve().parent.parent |
| 33 | +DIST = ROOT / "dist" |
| 34 | +PYPROJECT = ROOT / "pyproject.toml" |
| 35 | +VARIANTS = ("scenedetect", "scenedetect-headless") |
| 36 | + |
| 37 | + |
| 38 | +def get_version() -> str: |
| 39 | + """Parse scenedetect.__version__ without importing (avoids the cv2 guard), |
| 40 | + normalized per PEP 440 (e.g. 0.7.1-dev0 -> 0.7.1.dev0).""" |
| 41 | + source = (ROOT / "scenedetect" / "__init__.py").read_text(encoding="utf-8") |
| 42 | + for node in ast.parse(source).body: |
| 43 | + if isinstance(node, ast.Assign) and any( |
| 44 | + getattr(target, "id", None) == "__version__" for target in node.targets |
| 45 | + ): |
| 46 | + assert isinstance(node.value, ast.Constant) |
| 47 | + return str(node.value.value).replace("-", ".") |
| 48 | + raise SystemExit("Could not find __version__ in scenedetect/__init__.py") |
| 49 | + |
| 50 | + |
| 51 | +def build() -> None: |
| 52 | + subprocess.check_call([sys.executable, "-m", "build", "--outdir", str(DIST), str(ROOT)]) |
| 53 | + |
| 54 | + |
| 55 | +def main() -> None: |
| 56 | + version = get_version() |
| 57 | + |
| 58 | + original = PYPROJECT.read_text(encoding="utf-8") |
| 59 | + if 'name = "scenedetect-core"' not in original: |
| 60 | + raise SystemExit( |
| 61 | + "pyproject.toml is not the scenedetect-core baseline - likely left over " |
| 62 | + "from an interrupted build. Restore it (e.g. `git checkout pyproject.toml`) " |
| 63 | + "and re-run." |
| 64 | + ) |
| 65 | + |
| 66 | + build() # scenedetect-core from the unmodified repo root. |
| 67 | + try: |
| 68 | + for name in VARIANTS: |
| 69 | + variant = (ROOT / "packaging" / "variants" / f"pyproject-{name}.toml").read_text( |
| 70 | + encoding="utf-8" |
| 71 | + ) |
| 72 | + assert f'name = "{name}"' in variant, f"unexpected package name in variant {name}" |
| 73 | + PYPROJECT.write_text(variant, encoding="utf-8") |
| 74 | + build() |
| 75 | + finally: |
| 76 | + PYPROJECT.write_text(original, encoding="utf-8") |
| 77 | + |
| 78 | + expected = set() |
| 79 | + for name in ("scenedetect-core", *VARIANTS): |
| 80 | + normalized = name.replace("-", "_") |
| 81 | + expected.add(f"{normalized}-{version}.tar.gz") |
| 82 | + expected.add(f"{normalized}-{version}-py3-none-any.whl") |
| 83 | + # Only validate build artifacts: dist/ also holds tracked files (e.g. dist/logo/). |
| 84 | + actual = { |
| 85 | + path.name |
| 86 | + for path in DIST.iterdir() |
| 87 | + if path.is_file() and (path.name.endswith(".whl") or path.name.endswith(".tar.gz")) |
| 88 | + } |
| 89 | + if actual != expected: |
| 90 | + raise SystemExit( |
| 91 | + f"dist/ mismatch (stale files or failed build?)\n" |
| 92 | + f" missing: {sorted(expected - actual)}\n" |
| 93 | + f" unexpected: {sorted(actual - expected)}" |
| 94 | + ) |
| 95 | + print(f"Built {len(expected)} artifacts for version {version}:") |
| 96 | + for filename in sorted(expected): |
| 97 | + print(f" dist/{filename}") |
| 98 | + |
| 99 | + |
| 100 | +if __name__ == "__main__": |
| 101 | + main() |
0 commit comments