155 lines
4.6 KiB
Python
Executable file
155 lines
4.6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Create a patched build-local copy of the pinned Bluepad32 source."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
class PatchError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def resolve_paths(repo_root: Path | None = None) -> tuple[Path, Path, Path]:
|
|
root = Path.cwd() if repo_root is None else Path(repo_root)
|
|
return (
|
|
root / "external" / "bluepad32",
|
|
root / "patches" / "bluepad32-sdl3-imu.patch",
|
|
root / "build" / "_deps" / "bluepad32-src",
|
|
)
|
|
|
|
|
|
def _run(
|
|
command: list[str],
|
|
*,
|
|
cwd: Path | None = None,
|
|
env: dict[str, str] | None = None,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
command,
|
|
cwd=cwd,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
|
|
def check_paths(source_path: Path, patch_path: Path, output_path: Path) -> None:
|
|
if not source_path.is_dir():
|
|
raise PatchError(f"bluepad32 directory does not exist: {source_path}")
|
|
if not patch_path.is_file():
|
|
raise PatchError(f"patch file does not exist: {patch_path}")
|
|
|
|
repository_check = _run(
|
|
["git", "-C", str(source_path), "rev-parse", "--is-inside-work-tree"]
|
|
)
|
|
if repository_check.returncode != 0:
|
|
raise PatchError(f"Bluepad32 is not a git repository: {source_path}")
|
|
|
|
source = source_path.resolve()
|
|
output = output_path.resolve()
|
|
if output == source or source in output.parents:
|
|
raise PatchError("patched output must be outside the Bluepad32 source tree")
|
|
|
|
|
|
def validate_pristine_source(source_path: Path, patch_path: Path) -> None:
|
|
status = _run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
str(source_path),
|
|
"status",
|
|
"--porcelain",
|
|
"--untracked-files=all",
|
|
]
|
|
)
|
|
if status.returncode != 0:
|
|
detail = status.stderr.strip() or "git status failed"
|
|
raise PatchError(f"Could not inspect Bluepad32 source: {detail}")
|
|
if status.stdout.strip():
|
|
raise PatchError(
|
|
"Bluepad32 source must be pristine; patches are applied only to the build-local copy"
|
|
)
|
|
|
|
check = _run(
|
|
["git", "-C", str(source_path), "apply", "--check", str(patch_path)]
|
|
)
|
|
if check.returncode != 0:
|
|
detail = check.stderr.strip() or "patch does not apply"
|
|
raise PatchError(f"Patch validation failed (source revision may have diverged):\n{detail}")
|
|
|
|
|
|
def _copy_and_patch(source_path: Path, patch_path: Path, output_path: Path) -> None:
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
staging_path = output_path.with_name(f".{output_path.name}.tmp")
|
|
|
|
if staging_path.exists():
|
|
shutil.rmtree(staging_path)
|
|
|
|
try:
|
|
shutil.copytree(
|
|
source_path,
|
|
staging_path,
|
|
symlinks=True,
|
|
ignore=shutil.ignore_patterns(".git"),
|
|
)
|
|
result = _run(
|
|
["git", "apply", "--no-index", str(patch_path.resolve())],
|
|
cwd=staging_path,
|
|
env={
|
|
**os.environ,
|
|
"GIT_CEILING_DIRECTORIES": str(
|
|
staging_path.resolve().parent
|
|
),
|
|
},
|
|
)
|
|
if result.returncode != 0:
|
|
detail = result.stderr.strip() or "git apply failed"
|
|
raise PatchError(f"Could not patch build-local Bluepad32 copy: {detail}")
|
|
|
|
if output_path.exists():
|
|
shutil.rmtree(output_path)
|
|
staging_path.replace(output_path)
|
|
except Exception:
|
|
if staging_path.exists():
|
|
shutil.rmtree(staging_path)
|
|
raise
|
|
|
|
|
|
def prepare_bluepad32(
|
|
source_path: Path | None = None,
|
|
patch_path: Path | None = None,
|
|
output_path: Path | None = None,
|
|
) -> Path:
|
|
default_source, default_patch, default_output = resolve_paths()
|
|
source = Path(source_path or default_source)
|
|
patch = Path(patch_path or default_patch)
|
|
output = Path(output_path or default_output)
|
|
|
|
check_paths(source, patch, output)
|
|
validate_pristine_source(source, patch)
|
|
_copy_and_patch(source, patch, output)
|
|
return output
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--bluepad32", type=Path)
|
|
parser.add_argument("--patch", type=Path)
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
prepare_bluepad32(args.bluepad32, args.patch, args.output)
|
|
except PatchError as exc:
|
|
print(f"error: {exc}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|