#!/usr/bin/env python3 """Build and run an isolated local platform + fake OneOS, without external services. Requires installed Go/Node dependencies. No install, SSH, migration, cloud CLI, external fetch or production configuration is used. Ctrl-C stops both children. """ import argparse import json import os from pathlib import Path import shutil import signal import subprocess import tempfile import time import urllib.request ROOT = Path(__file__).resolve().parents[1] def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--skip-web-build", action="store_true") args = parser.parse_args() run = Path(tempfile.mkdtemp(prefix="vehicle-oneos-local-")) os.chmod(run, 0o700) # Build tools need installed paths/cache, but no inherited service credentials. clean = {key: os.environ[key] for key in ("PATH", "HOME", "TMPDIR") if key in os.environ} clean.update({"GOPROXY": "off", "GOSUMDB": "off", "GOTOOLCHAIN": "local", "NO_UPDATE_NOTIFIER": "1"}) children, logs = [], [] try: for name, package in (("fixture", "./cmd/oneos-local-fixture"), ("platform", "./cmd/platform-api")): subprocess.run([shutil.which("go"), "build", "-o", str(run / name), package], cwd=ROOT / "apps/api", env=clean, check=True) if not args.skip_web_build: with (run / "web-build.log").open("w") as build_log: subprocess.run([shutil.which("pnpm"), "build"], cwd=ROOT / "apps/web", env=clean, stdout=build_log, stderr=subprocess.STDOUT, check=True) if not (ROOT / "apps/web/dist/index.html").is_file(): raise RuntimeError("local frontend build is missing") config_file = run / "fixture.json" fixture_log = (run / "fixture.log").open("w"); logs.append(fixture_log) children.append(subprocess.Popen([str(run / "fixture"), "-config-file", str(config_file)], env=clean, stdout=fixture_log, stderr=subprocess.STDOUT)) deadline = time.monotonic() + 10 while not config_file.exists(): if children[-1].poll() is not None or time.monotonic() > deadline: raise RuntimeError("local fixture failed to start; see fixture.log") time.sleep(0.1) fixture = json.loads(config_file.read_text()) api_env = dict(clean) api_env.update({ "HTTP_ADDR": "127.0.0.1:20400", "DATA_MODE": "mock", "AUTH_MODE": "enforce", "STATIC_DIR": str(ROOT / "apps/web/dist"), "EXPORT_DIR": str(run / "exports"), "ONEOS_LOCAL_ONLY": "true", "ONEOS_AUTH_ENABLED": "true", "ONEOS_PASSWORD_ENABLED": "true", "ONEOS_PROFILE_BASE_URL": fixture["baseURL"], "ONEOS_PROFILE_ISSUER": fixture["issuer"], "ONEOS_PROFILE_ALLOWED_TENANT": fixture["tenantId"], "ONEOS_PUBLIC_ORIGIN": fixture["publicOrigin"], "ONEOS_PASSWORD_CLIENT_ID": fixture["clientId"], "ONEOS_PASSWORD_RSA_PUBLIC_KEY": fixture["publicKey"], "ONEOS_LOGIN_TICKET_EXCHANGE_PATH": "/auth/inner/v1/sso/vehicle-platform/token-exchange", "ONEOS_LOGIN_TICKET_SERVICE_TOKEN": fixture["serviceToken"], "ONEOS_VEHICLE_SCOPE_PATH": "/asset/vehicle-platform/scope", }) api_log = (run / "platform.log").open("w"); logs.append(api_log) children.append(subprocess.Popen([str(run / "platform")], env=api_env, stdout=api_log, stderr=subprocess.STDOUT)) opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) deadline = time.monotonic() + 10 while True: try: with opener.open("http://127.0.0.1:20400/api/v2/auth/providers", timeout=1) as resp: providers = json.load(resp)["data"] if not providers["oneos"]["passwordEnabled"] or not providers["oneos"]["ssoEnabled"]: raise RuntimeError("local shared-token authentication not ready") break except (OSError, ValueError, KeyError): if children[-1].poll() is not None or time.monotonic() > deadline: raise RuntimeError("local platform failed to start; see platform.log") time.sleep(0.1) print(json.dumps({"platform": fixture["publicOrigin"], "oneosFixture": fixture["baseURL"], "runDirectory": str(run), "fixtureConfig": str(config_file), "localOnly": True}), flush=True) while all(child.poll() is None for child in children): time.sleep(0.5) raise RuntimeError("one of the local child processes exited") except KeyboardInterrupt: pass finally: for child in reversed(children): if child.poll() is None: child.terminate() for child in children: try: child.wait(timeout=5) except subprocess.TimeoutExpired: child.kill(); child.wait() for handle in logs: handle.close() # Keep evidence and fixtures for local inspection; directory is mode 0700. print(f"Local processes stopped. Local evidence: {run}", flush=True) if __name__ == "__main__": signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt())) main()