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 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()
|
||||||
|
|||||||
Reference in New Issue
Block a user