Skip to content

Commit

Permalink
Add setup routine for launcher
Browse files Browse the repository at this point in the history
- On every run of the launcher, we check the state of ~/.local/share/ULWGL and the configuration file, ULWGL_VERSION.json, stored in /usr/share/ULWGL_VERSION.json  to determine whether an update for the runtime tools or a new install needs to be performed.
  • Loading branch information
R1kaB3rN committed Feb 23, 2024
1 parent ac684f4 commit 90759c3
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 6 deletions.
11 changes: 11 additions & 0 deletions ULWGL_VERSION.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"ulwgl": {
"versions": {
"launcher": "0.1-RC3",
"runner": "0.1-RC3",
"runtime_platform": "sniper_platform_0.20240125.75305",
"reaper": "1.0",
"pressure_vessel": "v0.20240212.0"
}
}
}
6 changes: 4 additions & 2 deletions ulwgl_consts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from enum import Enum
from logging import INFO, WARNING, DEBUG, ERROR

SIMPLE_FORMAT = "%(levelname)s: %(message)s"
SIMPLE_FORMAT: str = "%(levelname)s: %(message)s"

DEBUG_FORMAT = "%(levelname)s [%(module)s.%(funcName)s:%(lineno)s]:%(message)s"
DEBUG_FORMAT: str = "%(levelname)s [%(module)s.%(funcName)s:%(lineno)s]:%(message)s"

CONFIG: str = "ULWGL_VERSION.json"


class Level(Enum):
Expand Down
17 changes: 15 additions & 2 deletions ulwgl_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import subprocess
from ulwgl_dl_util import get_ulwgl_proton
from ulwgl_consts import Level
from ulwgl_util import msg
from ulwgl_util import msg, setup_ulwgl
from ulwgl_log import log, console_handler, debug_formatter

verbs: Set[str] = {
Expand Down Expand Up @@ -270,12 +270,20 @@ def main() -> int: # noqa: D103
"ULWGL_ID": "",
}
command: List[str] = []
args: Union[Namespace, Tuple[str, List[str]]] = parse_args()
opts: List[str] = None
# Expected files in this dir: pressure vessel, launcher files, runtime platform, runner, config
# root: Path = Path("/usr/share/ULWGL")
root: Path = Path(__file__).parent
# Expects this dir to be in sync with root
# On update, files will be selectively updated
local: Path = Path.home().joinpath(".local/share/ULWGL")
args: Union[Namespace, Tuple[str, List[str]]] = parse_args()

if "ULWGL_LOG" in os.environ:
set_log()

setup_ulwgl(root, local)

if isinstance(args, Namespace) and getattr(args, "config", None):
set_env_toml(env, args)
else:
Expand Down Expand Up @@ -310,3 +318,8 @@ def main() -> int: # noqa: D103
except Exception as e: # noqa: BLE001
print_exception(e)
sys.exit(1)
finally:
# Cleanup .ref file on every exit
file: Path = Path.home().joinpath(".local/share/ULWGL").joinpath(".ref")
if file.is_file():
file.unlink()
256 changes: 254 additions & 2 deletions ulwgl_util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from ulwgl_consts import Color, Level
from typing import Any
import os
from errno import EXDEV, ENOSYS, EINVAL
from ulwgl_consts import Color, Level, CONFIG
from typing import Any, Callable
from json import load, dump
from shutil import rmtree, copyfile, copytree
from ulwgl_log import log
from pathlib import Path
from sys import stderr


def msg(msg: Any, level: Level):
Expand All @@ -18,3 +25,248 @@ def msg(msg: Any, level: Level):
log = f"{Color.BOLD.value}{Color.DEBUG.value}{msg}{Color.RESET.value}"

return log


def setup_ulwgl(root: Path, local: Path) -> None:
"""Copy the launcher and its tools to ~/.local/share/ULWGL.
Paramaters are expected to be /usr/share/ULWGL and ~/.local/share/ULWGL respectively
Performs full copies of tools on new installs and selectively on new updates
The tools that will be copied are: Pressure Vessel, Reaper, SteamRT, ULWLG launcher and the ULWGL-Runner
"""
log.debug(msg(f"Root: {root}", Level.DEBUG))
log.debug(msg(f"Local: {local}", Level.DEBUG))

# Config saved in /usr/share/ULWGL
json_root: Any = None
# Config saved in .local/share/ULWGL
json_local: Any = None
cp: Callable[Path, Path] = None

if hasattr(os, "copy_file_range"):
log.debug(msg("CoW filesystem detected", Level.DEBUG))
cp = copyfile_reflink
else:
cp = copyfile

json_root = _get_json(root, CONFIG)

# New install
# Be lazy and don't implement fallback mechanisms
if not any(local.iterdir()):
log.debug(msg("New install detected", Level.DEBUG))

local.mkdir(parents=True, exist_ok=True)

# Config
print(f"Copying {CONFIG} -> {local} ...", file=stderr)
cp(root.joinpath(CONFIG), local.joinpath(CONFIG))

# Pressure vessel
print(f"Copying pressure vessel -> {local} ...", file=stderr)
copy_tree(root.joinpath("pressure-vessel"), local.joinpath("pressure-vessel"))

# Reaper
# print(f"Copying reaper -> {local}", file=stderr)
# copytree(root.joinpath("reaper").as_posix(), local.joinpath("reaper"), symlinks=True)

# Runtime platform
print(f"Copying runtime -> {local} ...", file=stderr)
copy_tree(
root.joinpath(json_root["ulwgl"]["versions"]["runtime_platform"]),
local.joinpath(json_root["ulwgl"]["versions"]["runtime_platform"]),
)

# Launcher files
for file in root.glob("*.py"):
if not file.name.startswith("ulwgl_test"):
print(f"Copying {file} -> {local} ...", file=stderr)
cp(file, local.joinpath(file.name))

for file in root.glob("run*"):
cp(file, local.joinpath(file.name))

cp(root.joinpath("ULWGL"), local.joinpath("ULWGL"))

local.joinpath("ulwgl-run").symlink_to("ulwgl_run.py")

# Runner
print(f"Copying ULWGL-Runner -> {local} ...", file=stderr)
copytree(
root.joinpath("ULWGL-Runner").as_posix(),
local.joinpath("ULWGL-Runner").as_posix(),
symlinks=True,
)

print("Completed.")

return

# Existing installs
log.debug(msg("Existing install detected", Level.DEBUG))
json_local = _get_json(local, CONFIG)

# Attempt to copy only the updated versions
# Compare the local to the root config
# Be lazy and just trust the integrity of local
for key, val in json_root["ulwgl"]["versions"].items():
if (
key == "reaper"
and val != json_local["ulwgl"]["versions"]["reaper"]
or not local.joinpath("reaper").is_dir()
):
# Reaper
# print(f"New version for {key}\nUpdating to {val} ...", file=stderr)

# if local.joinpath(key).is_dir():
# rmtree(local.joinpath(key).as_posix())

# copy_tree(root.joinpath("reaper"), local.joinpath("reaper"))

# json_local["ulwgl"]["versions"]["reaper"] = val
# print("Completed.", file=stderr)
pass
if (
key == "pressure_vessel"
and val != json_local["ulwgl"]["versions"]["pressure_vessel"]
or not local.joinpath("pressure-vessel").is_dir()
):
# Pressure Vessel
print(f"New version for {key}\nUpdating to {val} ...", file=stderr)

if local.joinpath("pressure-vessel").is_dir():
rmtree(local.joinpath("pressure-vessel").as_posix())

copy_tree(
root.joinpath("pressure-vessel"), local.joinpath("pressure-vessel")
)

json_local["ulwgl"]["versions"]["pressure_vessel"] = val
print("Completed.", file=stderr)
if key == "runtime_platform" and (
val != (json_local["ulwgl"]["versions"]["runtime_platform"])
or not local.joinpath(val).is_dir()
):
# Runtime
# The value of the rt corresponds to the dir name
rt: str = json_local["ulwgl"]["versions"]["runtime_platform"]

print(f"New version for {rt}\nUpdate to {val} ...", file=stderr)
if local.joinpath(rt).is_dir():
rmtree(local.joinpath(rt).as_posix())

copy_tree(root.joinpath(val), local.joinpath(val))

# The auto-generated files need to be copied as well
print("Updating run files ...", file=stderr)
for file in local.glob("run*"):
file.unlink(missing_ok=True)
cp(root.joinpath(file.name), local.joinpath(file.name))

json_local["ulwgl"]["versions"]["runtime_platform"] = val
print("Completed.", file=stderr)
if key == "launcher" and val != json_local["ulwgl"]["versions"]["launcher"]:
# Launcher
# We do not selectively update launcher files
print(f"New version for {key}\nUpdating to {val} ...", file=stderr)

# Assume that the launcher files are in /usr/share/ULWGL
for file in root.glob("*.py"):
if not file.name.startswith("ulwgl_test"):
print(f"Copying {file} -> {local} ...", file=stderr)
local.joinpath(file.name).unlink(missing_ok=True)
cp(file, local.joinpath(file.name))

# Handle the symbolic link to ulwgl_run separately
local.joinpath("ulwgl-run").unlink(missing_ok=True)
local.joinpath("ulwgl-run").symlink_to("ulwgl_run.py")

json_local["ulwgl"]["versions"]["launcher"] = val
print("Completed.", file=stderr)
if (
key == "runner"
and val != json_local["ulwgl"]["versions"]["runner"]
or not local.joinpath("ULWGL-Runner").is_dir()
):
# Runner
print(f"New version for {key}\nUpdating to {val} ...", file=stderr)

if local.joinpath(val).is_dir():
rmtree(local.joinpath(val).as_posix())

# TODO
# Prefer recreating the symbolic link within this dir
copytree(
root.joinpath("ULWGL-Runner").as_posix(),
local.joinpath("ULWGL-Runner").as_posix(),
symlinks=True,
)

json_local["ulwgl"]["versions"]["runner"] = val
print("Completed.", file=stderr)

# Finally, update the local config file
with local.joinpath(CONFIG).open(mode="w") as file:
dump(json_local, file, indent=4)


def _get_json(path: Path, config: str) -> Any:
"""Check the state of the configuration file in the given path."""
json: Any = None

log.debug(msg(f"JSON: {path.joinpath(config)}", level=Level.DEBUG))

# The file in /usr/share/ULWGL should always exist
# TODO Account for multiple root paths because of Flatpak
if not path.joinpath(config).is_file():
err: str = f"File not found: {config}\nPlease reinstall the package to recover configuration file"
raise FileNotFoundError(err)

with path.joinpath(config).open(mode="r") as file:
json = load(file)

if not json or ("ulwgl" not in json and "versions" not in json["ulwgl"]):
err: str = f"Failed to load {config} or failed to find valid keys in: {config}\nPlease reinstall the package"
raise ValueError(err)

return json


def copyfile_reflink(src: Path, dst: Path):
"""Create CoW copies of a file to a destination.
Implementation is from Proton
"""
dst.parent.mkdir(parents=True, exist_ok=True)

with src.open(mode="rb", buffering=0) as source:
bytes_to_copy: int = os.fstat(source.fileno()).st_size

try:
with dst.open(mode="wb", buffering=0) as dest:
while bytes_to_copy > 0:
bytes_to_copy -= os.copy_file_range(
source.fileno(), dest.fileno(), bytes_to_copy
)
except OSError as e:
if e.errno not in (EXDEV, ENOSYS, EINVAL):
raise
if e.errno == ENOSYS:
# Fallback to normal copy
copyfile(src.as_posix(), dst.joinpath(src.name).as_posix())

dst.chmod(src.stat().st_mode)


def copy_tree(src: Path, dest: Path) -> None:
"""Copy the directory tree from a source to a destination.
Implements a recursive solution
"""
for file in src.iterdir():
dest_file: Path = dest.joinpath(file.name)

if file.is_dir():
copy_tree(file, dest_file)
else:
copyfile_reflink(file, dest_file)

0 comments on commit 90759c3

Please sign in to comment.