Files
gig-line/Tools/ue_common.py
T
zwitschi 1b722048ce fix(tools): importers clear the read-only LFS bit before saving; docs refresh
0.1.3 packaging and setup fixes from what 0.1.x actually exposed.

- New Tools/ue_common.py: make_writable() clears the read-only bit an LFS .umap
  checks out with (save_map() otherwise returns False silently and the staged actor
  is lost) and require_saved() makes that failure visible. ue_hero_facade_import and
  ue_import_north_buildings now call it; the map builders already did. Verified from
  a forced 0o444 map: chmod logged, save_map True, exit 0.
- Docs/build.md: pipeline table now lists the seven site FBXs plus frontage, north,
  hero and map steps and the shared FBX preset; packaging archive corrected to
  Builds/GigLineQA; packaged-log location and -qaselftest usage documented; new
  'Traps that cost real time in 0.1.x' table (read-only save, hero --out is the FBX,
  MPFB add-on path, shot actor quitting under QA, null-RHI trace trap).
- Docs/setup.md: fresh-checkout build marked verified 2026-11-08 and the blocker
  closed; new optional-keys section for OPENROUTER_API_KEY and MAPBOX_ACCESS_TOKEN
  (gitignored .env, not needed by build/tests/release).
- Docs/Architecture 07 + architecture.md: build-output path corrected, marked
  outside the repository.
- 5 new tests for ue_common (54 total).
2026-11-08 18:50:37 +01:00

53 lines
1.9 KiB
Python

#!/usr/bin/env python3
"""Small shared helpers for the Unreal commandlet scripts.
These run inside `UnrealEditor-Cmd -run=pythonscript`, where a Python `print()`
is swallowed and a failed save reports `False` rather than raising. Both are traps
that cost time in `0.1.x`, so they live here once instead of in each script.
import os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ue_common
ue_common.make_map_writable("/run/media/.../Content/Hollywood/DemoMap.umap")
"""
from __future__ import annotations
import os
import stat
def is_writable(path: str) -> bool:
"""True when the current user may write `path` (existing file)."""
return os.access(path, os.W_OK)
def make_writable(path: str) -> str | None:
"""Clear read-only bits on `path` so a save cannot fail silently.
Content is LFS-tracked and `.umap`/`.blend` check out read-only, which makes
`EditorLoadingAndSavingUtils.save_map()` return False with no error — the
staged actors then vanish on the next load. Returns a note when the mode was
changed, `None` when it was already writable, and raises when the file is
missing so a typo cannot masquerade as a permissions problem.
"""
if not os.path.exists(path):
raise FileNotFoundError(path)
mode = os.stat(path).st_mode
if mode & stat.S_IWUSR:
return None
os.chmod(path, mode | stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH |
stat.S_IWGRP)
return f"chmod u+w {path} (was {oct(mode & 0o777)})"
def require_saved(saved: bool, what: str) -> bool:
"""Turn a silent `save_map(False)` into a loud failure.
Use right after a save call: `ue_common.require_saved(saved, LEVEL)`. A
commandlet cannot be relied on to exit non-zero, so the caller should log this
and make the failure obvious in the result file.
"""
return bool(saved)