сборка манифеста для лаунчера
This commit is contained in:
@@ -1 +1,3 @@
|
|||||||
.env
|
.env
|
||||||
|
__pycache__/
|
||||||
|
manifest.json
|
||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user