Fallback DEV PBO packaging without pboProject
Deploy DEV Workshop / deploy-dev-workshop (push) Failing after 4m30s

This commit is contained in:
Valmo Trindade
2026-07-14 20:55:45 +00:00
parent a47dc3fa03
commit f3d9d5f20a
+163 -14
View File
@@ -14,8 +14,10 @@ import json
import os
import re
import shutil
import struct
import subprocess
import sys
import time
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any
@@ -32,6 +34,29 @@ DEFAULT_STAGE_DIR = Path("dist/dev/@braf_dev")
METADATA_FILES = ("mod.cpp", "meta.cpp")
MOD_ASSET_FIELDS = ("picture", "logo", "logoOver", "logoSmall")
IGNORED_DIRS = {".git", "__pycache__"}
CASE_ALIAS_EXTENSIONS = (
"bisurf",
"csv",
"fsm",
"hpp",
"jpg",
"ogg",
"paa",
"p3d",
"png",
"rvmat",
"rtm",
"sqf",
"txt",
"wss",
"xml",
)
CASE_ALIAS_RE = re.compile(
rb"[A-Za-z0-9_ ./\\-]+\.("
+ b"|".join(ext.encode("ascii") for ext in CASE_ALIAS_EXTENSIONS)
+ rb")",
re.IGNORECASE,
)
def parse_args() -> argparse.Namespace:
@@ -154,8 +179,132 @@ def remove_cached_addon(cache_addons_dir: Path, addon: str) -> None:
path.unlink()
def rel_symlink(src: Path, dst: Path) -> None:
target = os.path.relpath(src, start=dst.parent)
dst.symlink_to(target, target_is_directory=src.is_dir())
def create_source_symlink_tree(src: Path, dst: Path) -> dict[str, Path]:
lower_to_source: dict[str, Path] = {}
for path in sorted(src.rglob("*")):
rel = path.relative_to(src)
target = dst / rel
if path.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
if any(part in IGNORED_DIRS for part in rel.parts):
continue
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists() or target.is_symlink():
target.unlink()
rel_symlink(path, target)
lower_to_source[rel.as_posix().lower()] = path
return lower_to_source
def extract_case_alias_refs(addon: str, file_path: Path) -> set[str]:
refs: set[str] = set()
try:
data = file_path.read_bytes()
except OSError:
return refs
addon_prefixes = (
f"braf/{addon}/",
f"{addon}/",
)
for match in CASE_ALIAS_RE.finditer(data):
value = match.group(0).decode("latin-1", errors="ignore")
value = value.strip(" \t\r\n\"'()[]{};,")
value = value.replace("\\", "/").lstrip("/")
lower_value = value.lower()
rel = None
for prefix in addon_prefixes:
index = lower_value.find(prefix)
if index != -1:
rel = value[index + len(prefix) :]
break
if rel is None and "/" in value:
rel = value
elif rel is None:
continue
rel_path = Path(rel)
if rel_path.is_absolute() or ".." in rel_path.parts:
continue
refs.add(rel_path.as_posix())
return refs
def add_case_aliases(addon: str, src: Path, dst: Path, lower_to_source: dict[str, Path]) -> None:
wanted_refs = {rel.lower(): rel for rel in lower_to_source}
for file_path in sorted(path for path in src.rglob("*") if path.is_file()):
for ref in extract_case_alias_refs(addon, file_path):
wanted_refs.setdefault(ref.lower(), ref)
for lower_ref, requested_ref in sorted(wanted_refs.items()):
source = lower_to_source.get(lower_ref)
if source is None:
continue
alias = dst / requested_ref
if alias.exists() or alias.is_symlink():
continue
alias.parent.mkdir(parents=True, exist_ok=True)
rel_symlink(source, alias)
def raw_pbo_files(source: Path) -> list[tuple[str, Path, int]]:
files: list[tuple[str, Path, int]] = []
for file_path in sorted(
(path for path in source.rglob("*") if path.is_file()),
key=lambda path: path.relative_to(source).as_posix().lower(),
):
rel = file_path.relative_to(source)
if any(part in IGNORED_DIRS for part in rel.parts):
continue
files.append((rel.as_posix().replace("/", "\\"), file_path, file_path.stat().st_size))
return files
def write_raw_pbo(addon: str, source: Path, output_path: Path) -> None:
files = raw_pbo_files(source)
output_path.parent.mkdir(parents=True, exist_ok=True)
timestamp = int(time.time())
sha = hashlib.sha1()
with output_path.open("wb") as handle:
def write(data: bytes) -> None:
handle.write(data)
sha.update(data)
write(b"\0")
write(struct.pack("<IIIII", 0x56657273, 0, 0, 0, 0))
for key, value in (
("prefix", f"braf\\{addon}"),
("version", "raw-dev-ci"),
):
write(key.encode("utf-8") + b"\0")
write(value.encode("utf-8") + b"\0")
write(b"\0")
for rel, _file_path, size in files:
write(rel.encode("utf-8") + b"\0")
write(struct.pack("<IIIII", 0, size, 0, timestamp, size))
write(b"\0")
write(struct.pack("<IIIII", 0, 0, 0, 0, 0))
for _rel, file_path, _size in files:
with file_path.open("rb") as input_file:
for chunk in iter(lambda: input_file.read(1024 * 1024), b""):
write(chunk)
handle.write(sha.digest())
def prepare_mikero_source_root(root: Path, cache_dir: Path, addons: list[str]) -> Path:
"""Create a P:/braf-like source root using symlinks.
"""Create a P:/braf-like source root with case aliases for Linux Mikero.
The historic local build path was P:/braf/<addon>. Keeping the parent folder
named "braf" avoids changing Mikero's inferred addon prefix in CI.
@@ -174,14 +323,14 @@ def prepare_mikero_source_root(root: Path, cache_dir: Path, addons: list[str]) -
for addon in addons:
dst = source_root / addon
src = root / addon
if dst.is_symlink() and dst.resolve() == src.resolve():
continue
if dst.exists() or dst.is_symlink():
if dst.is_dir() and not dst.is_symlink():
shutil.rmtree(dst)
else:
dst.unlink()
dst.symlink_to(src, target_is_directory=True)
dst.mkdir(parents=True)
lower_to_source = create_source_symlink_tree(src, dst)
add_case_aliases(addon, src, dst, lower_to_source)
return source_root
@@ -193,7 +342,6 @@ def run_pboproject(addon: str, source_root: Path, output_dir: Path) -> None:
output_dir.mkdir(parents=True, exist_ok=True)
pboproject = shutil.which("pboproject") or shutil.which("pboProject")
makepbo = shutil.which("makepbo") or shutil.which("MakePbo")
if pboproject:
cmd = [
@@ -203,16 +351,15 @@ def run_pboproject(addon: str, source_root: Path, output_dir: Path) -> None:
f"+M={output_dir}",
"+O" if addon in obfuschatedList else "-O",
]
elif makepbo:
cmd = [
makepbo,
"-P",
f"+@=braf\\{addon}",
str(addon_source),
str(output_dir / f"{addon}.pbo"),
]
else:
raise RuntimeError("Neither pboProject nor makepbo was found in PATH")
source = repo_root() / addon
fallback = output_dir / f"{addon}.pbo"
print(
"Mikero pboProject is not available; writing raw DEV PBO "
f"for {addon} at {fallback}"
)
write_raw_pbo(addon, source, fallback)
return
print(f"::group::Building {addon}")
print(" ".join(cmd))
@@ -320,6 +467,8 @@ def main() -> int:
source_root = Path(args.source_root).resolve()
elif args.dry_run:
source_root = (cache_dir / "mikero-source" / "braf").resolve()
elif not (shutil.which("pboproject") or shutil.which("pboProject")):
source_root = root
else:
source_root_addons = requested_addons if requested_addons is not None else addons
source_root = prepare_mikero_source_root(root, cache_dir, source_root_addons).resolve()