From a196cef2073272f831f4257a4e0cdfbda7453f37 Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 21 Mar 2026 17:18:47 +0400 Subject: [PATCH] =?UTF-8?q?=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B0=20=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=84=D0=B5=D1=81=D1=82=D0=B0=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BB=D0=B0=D1=83=D0=BD=D1=87=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- build_manifest.py | 162 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 build_manifest.py diff --git a/.gitignore b/.gitignore index 2eea525..ac8ded1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.env \ No newline at end of file +.env +__pycache__/ +manifest.json \ No newline at end of file diff --git a/build_manifest.py b/build_manifest.py new file mode 100644 index 0000000..01a1c4e --- /dev/null +++ b/build_manifest.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +import hashlib +import json +import os +import sys +from pathlib import Path +from typing import Optional + + +IGNORED_TOP_LEVEL_DIRS = { + "Cache", + "Errors", + "Logs", + "Screenshots", + "WTF", +} + +ENV_VAR_NAME = "WOW_HOME" + + +def sha256_bytes(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def sha256_file(path: Path, chunk_size: int = 1024 * 1024) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + while chunk := f.read(chunk_size): + h.update(chunk) + return h.hexdigest() + + +def is_ignored(rel_path: Path) -> bool: + if not rel_path.parts: + return False + return rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS + + +def load_dotenv(env_path: Path) -> dict[str, str]: + values: dict[str, str] = {} + + if not env_path.exists(): + return values + + for raw_line in env_path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + + if not line or line.startswith("#") or "=" not in line: + continue + + key, value = line.split("=", 1) + key = key.strip() + value = value.strip() + + if key.startswith("export "): + key = key.removeprefix("export ").strip() + + if not key: + continue + + if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: + value = value[1:-1] + + values[key] = value + + return values + + +def resolve_root(explicit_dir: Optional[str]) -> Optional[Path]: + if explicit_dir: + return Path(explicit_dir).resolve() + + env_value = os.environ.get(ENV_VAR_NAME) + if env_value: + return Path(env_value).expanduser().resolve() + + project_root = Path(__file__).resolve().parent + dotenv_values = load_dotenv(project_root / ".env") + env_value = dotenv_values.get(ENV_VAR_NAME) + + if env_value: + os.environ.setdefault(ENV_VAR_NAME, env_value) + return Path(env_value).expanduser().resolve() + + return None + + +def compute_build_hash(files: list[dict]) -> str: + lines = [ + f'{item["path"]}:{item["size"]}:{item["sha256"]}' + for item in sorted(files, key=lambda x: x["path"]) + ] + payload = "\n".join(lines).encode("utf-8") + return sha256_bytes(payload) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Collect files into manifest.json") + parser.add_argument( + "--dir", + help=f"Directory to scan. Defaults to {ENV_VAR_NAME} from env or .env", + ) + parser.add_argument("--output", default="manifest.json", help="Output file") + args = parser.parse_args() + + root = resolve_root(args.dir) + output = Path(args.output).resolve() + + if root is None: + print( + f"ERROR: directory is not set. Pass --dir or define {ENV_VAR_NAME} in env/.env", + file=sys.stderr, + ) + return 1 + + if not root.exists() or not root.is_dir(): + print(f"ERROR: directory not found: {root}", file=sys.stderr) + return 1 + + files = [] + + for file_path in sorted(root.rglob("*")): + if not file_path.is_file(): + continue + + if file_path.resolve() == output: + continue + + rel_path_obj = file_path.relative_to(root) + + if is_ignored(rel_path_obj): + continue + + rel_path = rel_path_obj.as_posix() + files.append({ + "path": rel_path, + "size": file_path.stat().st_size, + "sha256": sha256_file(file_path), + }) + + build_hash = compute_build_hash(files) + + manifest = { + "buildHash": build_hash, + "files": files + } + + with output.open("w", encoding="utf-8", newline="\n") as f: + json.dump(manifest, f, ensure_ascii=False, indent=2) + f.write("\n") + + print(f"OK: written {output}") + print(f"Files: {len(files)}") + print(f"Build hash: {build_hash}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())