da15bda6d2
---ci--- phase: 3 milestone: v0.2 status: complete requirements: covered: [REQ-21, REQ-22, REQ-23] partial: [] ---/ci---
126 lines
4.9 KiB
Python
126 lines
4.9 KiB
Python
# coreci-broker CLI boot shim; staged by scripts/build_cli.py as the root
|
|
# __main__.py of the coreci-broker-vX.Y.Z.pyz zipapp (the documented mode —
|
|
# running this file unzipped from a source tree is a guarded fallback).
|
|
#
|
|
# Boot sequence: (1) --version prints _cli_version.VERSION and exits 0 before
|
|
# any broker import; (2) the state dir is resolved exactly like the broker
|
|
# resolves it (BROKER_ENV_PATH-anchored; relative default paths stay
|
|
# CWD-relative — the broker's path contract is inherited unchanged);
|
|
# (3) bundled assets are materialized from the running archive
|
|
# (zipfile.ZipFile(sys.path[0])) into
|
|
# <state>/.coreci_broker_assets/<VERSION>/{ui,static} — ui/ receives the
|
|
# bundled coreci_broker_ui/ pages, static/ receives the bundled
|
|
# coreci_broker_client.py — via a temp dir plus os.replace (versioned dirs;
|
|
# same-version overwrite self-repairs; other versions are never cleaned);
|
|
# (4) BROKER_UI_ROOT/BROKER_STATIC_ROOT are pointed at the materialized
|
|
# dirs only when the key is absent from both the process env and the user
|
|
# .env (user wins); (5) coreci_broker.run_standalone() boots the broker.
|
|
# Guard: when sys.path[0] is a directory (or empty) this shim is running
|
|
# unzipped from a source tree — materialization is skipped and the broker's
|
|
# repo-relative defaults apply.
|
|
#
|
|
# Private-API coupling (grill G-018, tracked in P3-02): this shim imports
|
|
# coreci_broker._parse_env_file and coreci_broker.run_standalone —
|
|
# module-level but private-surfaced broker APIs (coreci_broker.py is frozen
|
|
# at 1390 lines). Any future broker refactor that removes or renames them
|
|
# breaks packaged boots loudly (ImportError/AttributeError); that breakage
|
|
# is accepted — the fix is to update this shim and re-run the v0.2 packaged
|
|
# acceptance (tests/test_cli_packaging.py).
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
|
|
ASSET_DIR_NAME = ".coreci_broker_assets"
|
|
|
|
|
|
def _version_requested():
|
|
return "--version" in sys.argv[1:]
|
|
|
|
|
|
def _resolve_state_dir():
|
|
env_path = os.environ.get("BROKER_ENV_PATH") or ".env"
|
|
return os.path.dirname(os.path.abspath(env_path)) or os.getcwd()
|
|
|
|
|
|
def _safe_relative(name):
|
|
if not name or "\x00" in name or "\\" in name or name.startswith("/"):
|
|
return None
|
|
parts = name.split("/")
|
|
if ".." in parts or any(part.startswith(".") for part in parts):
|
|
return None
|
|
return name
|
|
|
|
|
|
def _write_asset(target_dir, relative, payload):
|
|
target = os.path.join(target_dir, relative)
|
|
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
with open(target, "wb") as handle:
|
|
handle.write(payload)
|
|
|
|
|
|
def _materialize_assets(archive_path, state_dir):
|
|
import _cli_version
|
|
|
|
base_dir = os.path.join(state_dir, ASSET_DIR_NAME, _cli_version.VERSION)
|
|
parent_dir = os.path.dirname(base_dir)
|
|
os.makedirs(parent_dir, exist_ok=True)
|
|
tmp_dir = tempfile.mkdtemp(dir=parent_dir, prefix=".tmp-")
|
|
try:
|
|
ui_dir = os.path.join(tmp_dir, "ui")
|
|
static_dir = os.path.join(tmp_dir, "static")
|
|
os.makedirs(ui_dir)
|
|
os.makedirs(static_dir)
|
|
with zipfile.ZipFile(archive_path) as archive:
|
|
for name in archive.namelist():
|
|
if name == "coreci_broker_client.py":
|
|
_write_asset(static_dir, name, archive.read(name))
|
|
elif name.startswith("coreci_broker_ui/") and not name.endswith("/"):
|
|
relative = _safe_relative(name[len("coreci_broker_ui/"):])
|
|
if relative is not None:
|
|
_write_asset(ui_dir, relative, archive.read(name))
|
|
if os.path.isdir(base_dir):
|
|
stale_dir = base_dir + ".stale"
|
|
shutil.rmtree(stale_dir, ignore_errors=True)
|
|
os.replace(base_dir, stale_dir)
|
|
try:
|
|
os.replace(tmp_dir, base_dir)
|
|
except OSError:
|
|
os.replace(stale_dir, base_dir)
|
|
raise
|
|
shutil.rmtree(stale_dir, ignore_errors=True)
|
|
else:
|
|
os.replace(tmp_dir, base_dir)
|
|
finally:
|
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
return {"ui": os.path.join(base_dir, "ui"), "static": os.path.join(base_dir, "static")}
|
|
|
|
|
|
def _apply_asset_roots(roots, env_path):
|
|
import coreci_broker
|
|
|
|
file_values = dict(coreci_broker._parse_env_file(env_path))
|
|
for key, name in (("BROKER_UI_ROOT", "ui"), ("BROKER_STATIC_ROOT", "static")):
|
|
if key not in os.environ and key not in file_values:
|
|
os.environ[key] = roots[name]
|
|
|
|
|
|
def main():
|
|
if _version_requested():
|
|
import _cli_version
|
|
print(_cli_version.VERSION)
|
|
return 0
|
|
archive_path = sys.path[0]
|
|
if archive_path and not os.path.isdir(archive_path):
|
|
env_path = os.environ.get("BROKER_ENV_PATH") or ".env"
|
|
roots = _materialize_assets(archive_path, _resolve_state_dir())
|
|
_apply_asset_roots(roots, env_path)
|
|
import coreci_broker
|
|
coreci_broker.run_standalone()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main()) |