mirror of
https://github.com/Pixelplanet/Pixel-VR-Player.git
synced 2026-10-20 13:00:46 +02:00
201 lines
7.1 KiB
Python
201 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Deploy Pixel VR Player to a Steam Frame that is in Developer Mode.
|
|
|
|
This copies the aarch64 binary, the compiled SPIR-V shaders and the direct-run
|
|
helper into ``~/devkit-game/PixelVRPlayer`` on the headset and registers it as a
|
|
Non-Steam "Devkit Game" that runs *directly on the host* (no Steam Linux Runtime
|
|
container), which is how a native SteamOS/ARM64 OpenXR app must launch so the
|
|
SteamVR runtime and compositor are wired up.
|
|
|
|
Prerequisites on the headset:
|
|
* Developer Mode enabled and paired with the SteamOS Devkit Client
|
|
(this installs ``~/devkit-utils`` and authorises the ``devkit_rsa`` key).
|
|
|
|
Examples:
|
|
python3 tools/deploy.py # copy + register only
|
|
python3 tools/deploy.py --launch # copy, register and start it
|
|
python3 tools/deploy.py --launch -- /home/steamos/Videos/clip_360_tb.mp4
|
|
python3 tools/deploy.py --host steamos@192.168.1.50 --launch
|
|
|
|
Environment overrides: FRAME_HOST, FRAME_SSH_KEY.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
GAME = "PixelVRPlayer"
|
|
REMOTE_DIR = f"devkit-game/{GAME}" # relative to the remote $HOME
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def default_devkit_key() -> str | None:
|
|
"""Locate the SteamOS Devkit Client SSH key for the current OS."""
|
|
if sys.platform == "win32":
|
|
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
candidates = [base / "steamos-devkit" / "steamos-devkit" / "devkit_rsa"]
|
|
elif sys.platform == "darwin":
|
|
base = Path.home() / "Library" / "Application Support"
|
|
candidates = [base / "steamos-devkit" / "devkit_rsa"]
|
|
else:
|
|
base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
candidates = [base / "steamos-devkit" / "devkit_rsa"]
|
|
for key in candidates:
|
|
if key.is_file():
|
|
return str(key)
|
|
return None
|
|
|
|
|
|
def find_build_outputs() -> tuple[Path, Path]:
|
|
"""Return (binary, shader_dir), preferring the aarch64 build."""
|
|
for build in ("build-aarch64", "build"):
|
|
binary = PROJECT_ROOT / build / "bin" / "pixelvr"
|
|
if binary.is_file():
|
|
return binary, PROJECT_ROOT / build / "shaders"
|
|
raise FileNotFoundError(
|
|
"No pixelvr binary found. Build first, e.g.: ./tools/build.sh --aarch64"
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Deploy Pixel VR Player to a Steam Frame.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"--host",
|
|
default=os.environ.get("FRAME_HOST", "steamos@frame"),
|
|
help="SSH target (default: steamos@frame or $FRAME_HOST).",
|
|
)
|
|
parser.add_argument(
|
|
"--key",
|
|
default=os.environ.get("FRAME_SSH_KEY") or default_devkit_key(),
|
|
help="SSH private key (default: SteamOS Devkit devkit_rsa).",
|
|
)
|
|
parser.add_argument(
|
|
"--launch",
|
|
action="store_true",
|
|
help="Start the app on the headset after deploying.",
|
|
)
|
|
args, player_args = parser.parse_known_args()
|
|
if player_args and player_args[0] == "--":
|
|
player_args = player_args[1:]
|
|
|
|
binary, shader_dir = find_build_outputs()
|
|
shaders = sorted(shader_dir.glob("*.spv"))
|
|
if not shaders:
|
|
print(f"warning: no compiled shaders in {shader_dir}", file=sys.stderr)
|
|
|
|
host = args.host if "@" in args.host else f"steamos@{args.host}"
|
|
ssh_opts = ["-o", "ConnectTimeout=10", "-o", "BatchMode=yes"]
|
|
if args.key:
|
|
ssh_opts += ["-i", str(Path(args.key).expanduser())]
|
|
|
|
def ssh(command: str) -> str:
|
|
result = subprocess.run(
|
|
["ssh", *ssh_opts, host, command],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
timeout=180,
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
def scp(sources: list[str], dest: str) -> None:
|
|
subprocess.run(
|
|
["scp", *ssh_opts, *sources, f"{host}:{dest}"],
|
|
check=True,
|
|
timeout=300,
|
|
)
|
|
|
|
# 1. Verify Developer Mode, ensure the target dir, refuse to clobber a run.
|
|
print(f"Connecting to {host} ...")
|
|
remote_dir = ssh(
|
|
"set -eu; "
|
|
'test -x "$HOME/devkit-utils/steam-client-create-shortcut" || '
|
|
"{ echo 'error: ~/devkit-utils missing - enable Developer Mode and pair "
|
|
"the SteamOS Devkit Client first.' >&2; exit 1; }; "
|
|
f'target="$HOME/{REMOTE_DIR}"; '
|
|
'if command -v fuser >/dev/null 2>&1 && fuser "$target/pixelvr" '
|
|
">/dev/null 2>&1; then echo 'error: pixelvr is running - stop it in Steam "
|
|
"before deploying.' >&2; exit 1; fi; "
|
|
'mkdir -p "$target/shaders"; printf %s "$target"'
|
|
)
|
|
|
|
# 2. Copy binary, shaders and the direct-run helper.
|
|
print("Copying binary ...")
|
|
scp([str(binary)], f"{REMOTE_DIR}/pixelvr")
|
|
if shaders:
|
|
print(f"Copying {len(shaders)} shader(s) ...")
|
|
scp([str(p) for p in shaders], f"{REMOTE_DIR}/shaders/")
|
|
launch_helper = PROJECT_ROOT / "tools" / "launch.sh"
|
|
if launch_helper.is_file():
|
|
scp([str(launch_helper)], f"{REMOTE_DIR}/launch.sh")
|
|
|
|
# 3. Register the Non-Steam shortcut (runs on the host, not in Sniper).
|
|
request = {
|
|
"gameid": GAME,
|
|
"directory": remote_dir,
|
|
"argv": ["./pixelvr", *player_args],
|
|
"settings": {"steam_play": "0", "compat_tool": ""},
|
|
}
|
|
print("Registering the Non-Steam shortcut ...")
|
|
raw = ssh(
|
|
f"chmod u+x {shlex.quote(f'{REMOTE_DIR}/pixelvr')} "
|
|
f"{shlex.quote(f'{REMOTE_DIR}/launch.sh')} 2>/dev/null; "
|
|
+ shlex.join(
|
|
[
|
|
"python3",
|
|
"-B",
|
|
"devkit-utils/steam-client-create-shortcut",
|
|
"--parms",
|
|
json.dumps(request),
|
|
]
|
|
)
|
|
)
|
|
try:
|
|
response = json.loads(raw) if raw else {}
|
|
except json.JSONDecodeError:
|
|
response = {"raw": raw}
|
|
if isinstance(response, dict) and response.get("error"):
|
|
raise RuntimeError(f"shortcut registration failed: {response['error']}")
|
|
|
|
# 4. Optionally launch it in the headset.
|
|
if args.launch:
|
|
print("Launching in the headset ...")
|
|
ssh(
|
|
shlex.join(
|
|
[
|
|
"python3",
|
|
"-B",
|
|
"devkit-utils/steam-devkit-rpc",
|
|
"run-game",
|
|
f"gameid={GAME}",
|
|
]
|
|
)
|
|
)
|
|
|
|
print()
|
|
print(f"Deployed to {host}:~/{REMOTE_DIR}")
|
|
if player_args:
|
|
print(" argv:", " ".join(["./pixelvr", *player_args]))
|
|
print(f" Launch from Steam > Library > Non-Steam > 'Devkit Game: {GAME}'")
|
|
print(f" Or over SSH: ssh {host} '{REMOTE_DIR}/launch.sh <video>'")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except subprocess.CalledProcessError as exc:
|
|
print(f"error: command failed ({exc.returncode}): {exc}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except (FileNotFoundError, RuntimeError) as exc:
|
|
print(f"error: {exc}", file=sys.stderr)
|
|
sys.exit(1)
|