Fallback DEV PBO packaging without pboProject
Deploy DEV Workshop / deploy-dev-workshop (push) Failing after 4m30s
Deploy DEV Workshop / deploy-dev-workshop (push) Failing after 4m30s
This commit is contained in:
+163
-14
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user