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 os
import re import re
import shutil import shutil
import struct
import subprocess import subprocess
import sys import sys
import time
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any from typing import Any
@@ -32,6 +34,29 @@ DEFAULT_STAGE_DIR = Path("dist/dev/@braf_dev")
METADATA_FILES = ("mod.cpp", "meta.cpp") METADATA_FILES = ("mod.cpp", "meta.cpp")
MOD_ASSET_FIELDS = ("picture", "logo", "logoOver", "logoSmall") MOD_ASSET_FIELDS = ("picture", "logo", "logoOver", "logoSmall")
IGNORED_DIRS = {".git", "__pycache__"} 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: def parse_args() -> argparse.Namespace:
@@ -154,8 +179,132 @@ def remove_cached_addon(cache_addons_dir: Path, addon: str) -> None:
path.unlink() 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: 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 The historic local build path was P:/braf/<addon>. Keeping the parent folder
named "braf" avoids changing Mikero's inferred addon prefix in CI. 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: for addon in addons:
dst = source_root / addon dst = source_root / addon
src = root / addon src = root / addon
if dst.is_symlink() and dst.resolve() == src.resolve():
continue
if dst.exists() or dst.is_symlink(): if dst.exists() or dst.is_symlink():
if dst.is_dir() and not dst.is_symlink(): if dst.is_dir() and not dst.is_symlink():
shutil.rmtree(dst) shutil.rmtree(dst)
else: else:
dst.unlink() 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 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) output_dir.mkdir(parents=True, exist_ok=True)
pboproject = shutil.which("pboproject") or shutil.which("pboProject") pboproject = shutil.which("pboproject") or shutil.which("pboProject")
makepbo = shutil.which("makepbo") or shutil.which("MakePbo")
if pboproject: if pboproject:
cmd = [ cmd = [
@@ -203,16 +351,15 @@ def run_pboproject(addon: str, source_root: Path, output_dir: Path) -> None:
f"+M={output_dir}", f"+M={output_dir}",
"+O" if addon in obfuschatedList else "-O", "+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: 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(f"::group::Building {addon}")
print(" ".join(cmd)) print(" ".join(cmd))
@@ -320,6 +467,8 @@ def main() -> int:
source_root = Path(args.source_root).resolve() source_root = Path(args.source_root).resolve()
elif args.dry_run: elif args.dry_run:
source_root = (cache_dir / "mikero-source" / "braf").resolve() source_root = (cache_dir / "mikero-source" / "braf").resolve()
elif not (shutil.which("pboproject") or shutil.which("pboProject")):
source_root = root
else: else:
source_root_addons = requested_addons if requested_addons is not None else addons 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() source_root = prepare_mikero_source_root(root, cache_dir, source_root_addons).resolve()