mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 01:00:04 +02:00
fix(installer): honor explicit launcher overrides without duplicate flags
This commit is contained in:
1 parent
938caa39cc
commit
67910aa7d4
3 files changed
+170
-28
No files matched your search
+46
-14
@@ -348,7 +348,7 @@ def select(root, link, target):
|
||||
temp.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def desired_launcher(root, legacy=False, pinned_font=False):
|
||||
def desired_launcher(root, legacy=False, pinned_font=False, prior_launcher=False):
|
||||
import shlex
|
||||
q = lambda path: shlex.quote(str(path))
|
||||
base = root / "current"
|
||||
@@ -374,18 +374,48 @@ def desired_launcher(root, legacy=False, pinned_font=False):
|
||||
model_default = (q(base / "model") if legacy else
|
||||
'${CONFIG_MODEL:-' + q(base / "model") + '}')
|
||||
check_font = ' --font ' + q(base / "fonts/font.ttf") if legacy or pinned_font else ''
|
||||
return ("#!/bin/sh\n" + MARKER + 'export PYTHONDONTWRITEBYTECODE=1\n'
|
||||
+ f'export FRAMEYAP_INSTALL_ROOT={q(root)}\n'
|
||||
+ f'export LD_LIBRARY_PATH={q(base / "lib")}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\n'
|
||||
+ config
|
||||
+ f'PYTHON=${{FRAMEYAP_PYTHON:-{python_default}}}\n'
|
||||
+ f'MODEL=${{FRAMEYAP_MODEL:-{model_default}}}\n'
|
||||
+ 'if [ "$#" -eq 0 ]; then set -- --run; fi\n'
|
||||
+ 'case "$1" in\n'
|
||||
preamble = ("#!/bin/sh\n" + MARKER + 'export PYTHONDONTWRITEBYTECODE=1\n'
|
||||
+ f'export FRAMEYAP_INSTALL_ROOT={q(root)}\n'
|
||||
+ f'export LD_LIBRARY_PATH={q(base / "lib")}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\n'
|
||||
+ config
|
||||
+ f'PYTHON=${{FRAMEYAP_PYTHON:-{python_default}}}\n'
|
||||
+ f'MODEL=${{FRAMEYAP_MODEL:-{model_default}}}\n'
|
||||
+ 'if [ "$#" -eq 0 ]; then set -- --run; fi\n'
|
||||
+ 'case "$1" in\n')
|
||||
# These are only for byte-for-byte migration of earlier managed wrappers.
|
||||
if legacy or pinned_font or prior_launcher:
|
||||
return (preamble
|
||||
+ ' --run)\n'
|
||||
+ ' if [ -n "${GAMESCOPE_SOCKET:-}" ]; then set -- "$@" --socket "$GAMESCOPE_SOCKET"; fi\n'
|
||||
+ f" shift; exec {q(base / 'bin/frameyap')} --run {args} --python \"$PYTHON\" --model \"$MODEL\" \"$@\";;\n"
|
||||
+ f" --check-overlay|--check-controls) mode=$1; shift; exec {q(base / 'bin/frameyap')} \"$mode\" --assets {q(base / 'assets')}{check_font} \"$@\";;\n"
|
||||
+ f" *) exec {q(base / 'bin/frameyap')} \"$@\";;\n"
|
||||
+ 'esac\n').encode()
|
||||
# Scan the original argv without shifting or evaluating it. Prepend each
|
||||
# absent default in reverse order, preserving explicit options (including
|
||||
# duplicates) for the binary's strict parser to handle.
|
||||
return (preamble
|
||||
+ ' --run)\n'
|
||||
+ ' if [ -n "${GAMESCOPE_SOCKET:-}" ]; then set -- "$@" --socket "$GAMESCOPE_SOCKET"; fi\n'
|
||||
+ f" shift; exec {q(base / 'bin/frameyap')} --run {args} --python \"$PYTHON\" --model \"$MODEL\" \"$@\";;\n"
|
||||
+ f" --check-overlay|--check-controls) mode=$1; shift; exec {q(base / 'bin/frameyap')} \"$mode\" --assets {q(base / 'assets')}{check_font} \"$@\";;\n"
|
||||
+ ' shift\n'
|
||||
+ ' has_assets= has_worker= has_python= has_model= has_socket=\n'
|
||||
+ ' for arg in "$@"; do\n'
|
||||
+ ' case "$arg" in\n'
|
||||
+ ' --assets) has_assets=1;;\n'
|
||||
+ ' --worker) has_worker=1;;\n'
|
||||
+ ' --python) has_python=1;;\n'
|
||||
+ ' --model) has_model=1;;\n'
|
||||
+ ' --socket) has_socket=1;;\n'
|
||||
+ ' esac\n'
|
||||
+ ' done\n'
|
||||
+ ' if [ -z "$has_socket" ] && [ -n "${GAMESCOPE_SOCKET:-}" ]; then\n'
|
||||
+ ' set -- --socket "$GAMESCOPE_SOCKET" "$@"\n'
|
||||
+ ' fi\n'
|
||||
+ ' if [ -z "$has_model" ]; then set -- --model "$MODEL" "$@"; fi\n'
|
||||
+ ' if [ -z "$has_python" ]; then set -- --python "$PYTHON" "$@"; fi\n'
|
||||
+ f' if [ -z "$has_worker" ]; then set -- --worker {q(base / "python/frameyap/worker.py")} "$@"; fi\n'
|
||||
+ f' if [ -z "$has_assets" ]; then set -- --assets {q(base / "assets")} "$@"; fi\n'
|
||||
+ f" exec {q(base / 'bin/frameyap')} --run \"$@\";;\n"
|
||||
+ f" --check-overlay|--check-controls) mode=$1; shift; exec {q(base / 'bin/frameyap')} \"$mode\" --assets {q(base / 'assets')} \"$@\";;\n"
|
||||
+ f" *) exec {q(base / 'bin/frameyap')} \"$@\";;\n"
|
||||
+ 'esac\n').encode()
|
||||
|
||||
@@ -425,7 +455,8 @@ def check_wrappers(root, launcher):
|
||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||
# Accept only exact earlier managed scripts for migration.
|
||||
check_owned_file(launcher, desired_launcher(root),
|
||||
(desired_launcher(root, pinned_font=True), desired_launcher(root, legacy=True)))
|
||||
(desired_launcher(root, prior_launcher=True), desired_launcher(root, pinned_font=True),
|
||||
desired_launcher(root, legacy=True)))
|
||||
check_owned_file(manifest, desired)
|
||||
check_owned_file(desktop_path(root), desired_desktop(launcher))
|
||||
return manifest, desired
|
||||
@@ -764,7 +795,8 @@ def uninstall(root, launcher):
|
||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||
check_owned_file(manifest, desired)
|
||||
check_owned_file(launcher, desired_launcher(root),
|
||||
(desired_launcher(root, pinned_font=True), desired_launcher(root, legacy=True)))
|
||||
(desired_launcher(root, prior_launcher=True), desired_launcher(root, pinned_font=True),
|
||||
desired_launcher(root, legacy=True)))
|
||||
desktop = desktop_path(root)
|
||||
check_owned_file(desktop, desired_desktop(launcher))
|
||||
versions = root / "versions"
|
||||
|
||||
+46
-14
@@ -328,7 +328,7 @@ def select(root, link, target):
|
||||
temp.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def desired_launcher(root, legacy=False, pinned_font=False):
|
||||
def desired_launcher(root, legacy=False, pinned_font=False, prior_launcher=False):
|
||||
import shlex
|
||||
q = lambda path: shlex.quote(str(path))
|
||||
base = root / "current"
|
||||
@@ -354,18 +354,48 @@ def desired_launcher(root, legacy=False, pinned_font=False):
|
||||
model_default = (q(base / "model") if legacy else
|
||||
'${CONFIG_MODEL:-' + q(base / "model") + '}')
|
||||
check_font = ' --font ' + q(base / "fonts/font.ttf") if legacy or pinned_font else ''
|
||||
return ("#!/bin/sh\n" + MARKER + 'export PYTHONDONTWRITEBYTECODE=1\n'
|
||||
+ f'export FRAMEYAP_INSTALL_ROOT={q(root)}\n'
|
||||
+ f'export LD_LIBRARY_PATH={q(base / "lib")}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\n'
|
||||
+ config
|
||||
+ f'PYTHON=${{FRAMEYAP_PYTHON:-{python_default}}}\n'
|
||||
+ f'MODEL=${{FRAMEYAP_MODEL:-{model_default}}}\n'
|
||||
+ 'if [ "$#" -eq 0 ]; then set -- --run; fi\n'
|
||||
+ 'case "$1" in\n'
|
||||
preamble = ("#!/bin/sh\n" + MARKER + 'export PYTHONDONTWRITEBYTECODE=1\n'
|
||||
+ f'export FRAMEYAP_INSTALL_ROOT={q(root)}\n'
|
||||
+ f'export LD_LIBRARY_PATH={q(base / "lib")}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\n'
|
||||
+ config
|
||||
+ f'PYTHON=${{FRAMEYAP_PYTHON:-{python_default}}}\n'
|
||||
+ f'MODEL=${{FRAMEYAP_MODEL:-{model_default}}}\n'
|
||||
+ 'if [ "$#" -eq 0 ]; then set -- --run; fi\n'
|
||||
+ 'case "$1" in\n')
|
||||
# These are only for byte-for-byte migration of earlier managed wrappers.
|
||||
if legacy or pinned_font or prior_launcher:
|
||||
return (preamble
|
||||
+ ' --run)\n'
|
||||
+ ' if [ -n "${GAMESCOPE_SOCKET:-}" ]; then set -- "$@" --socket "$GAMESCOPE_SOCKET"; fi\n'
|
||||
+ f" shift; exec {q(base / 'bin/frameyap')} --run {args} --python \"$PYTHON\" --model \"$MODEL\" \"$@\";;\n"
|
||||
+ f" --check-overlay|--check-controls) mode=$1; shift; exec {q(base / 'bin/frameyap')} \"$mode\" --assets {q(base / 'assets')}{check_font} \"$@\";;\n"
|
||||
+ f" *) exec {q(base / 'bin/frameyap')} \"$@\";;\n"
|
||||
+ 'esac\n').encode()
|
||||
# Scan the original argv without shifting or evaluating it. Prepend each
|
||||
# absent default in reverse order, preserving explicit options (including
|
||||
# duplicates) for the binary's strict parser to handle.
|
||||
return (preamble
|
||||
+ ' --run)\n'
|
||||
+ ' if [ -n "${GAMESCOPE_SOCKET:-}" ]; then set -- "$@" --socket "$GAMESCOPE_SOCKET"; fi\n'
|
||||
+ f" shift; exec {q(base / 'bin/frameyap')} --run {args} --python \"$PYTHON\" --model \"$MODEL\" \"$@\";;\n"
|
||||
+ f" --check-overlay|--check-controls) mode=$1; shift; exec {q(base / 'bin/frameyap')} \"$mode\" --assets {q(base / 'assets')}{check_font} \"$@\";;\n"
|
||||
+ ' shift\n'
|
||||
+ ' has_assets= has_worker= has_python= has_model= has_socket=\n'
|
||||
+ ' for arg in "$@"; do\n'
|
||||
+ ' case "$arg" in\n'
|
||||
+ ' --assets) has_assets=1;;\n'
|
||||
+ ' --worker) has_worker=1;;\n'
|
||||
+ ' --python) has_python=1;;\n'
|
||||
+ ' --model) has_model=1;;\n'
|
||||
+ ' --socket) has_socket=1;;\n'
|
||||
+ ' esac\n'
|
||||
+ ' done\n'
|
||||
+ ' if [ -z "$has_socket" ] && [ -n "${GAMESCOPE_SOCKET:-}" ]; then\n'
|
||||
+ ' set -- --socket "$GAMESCOPE_SOCKET" "$@"\n'
|
||||
+ ' fi\n'
|
||||
+ ' if [ -z "$has_model" ]; then set -- --model "$MODEL" "$@"; fi\n'
|
||||
+ ' if [ -z "$has_python" ]; then set -- --python "$PYTHON" "$@"; fi\n'
|
||||
+ f' if [ -z "$has_worker" ]; then set -- --worker {q(base / "python/frameyap/worker.py")} "$@"; fi\n'
|
||||
+ f' if [ -z "$has_assets" ]; then set -- --assets {q(base / "assets")} "$@"; fi\n'
|
||||
+ f" exec {q(base / 'bin/frameyap')} --run \"$@\";;\n"
|
||||
+ f" --check-overlay|--check-controls) mode=$1; shift; exec {q(base / 'bin/frameyap')} \"$mode\" --assets {q(base / 'assets')} \"$@\";;\n"
|
||||
+ f" *) exec {q(base / 'bin/frameyap')} \"$@\";;\n"
|
||||
+ 'esac\n').encode()
|
||||
|
||||
@@ -405,7 +435,8 @@ def check_wrappers(root, launcher):
|
||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||
# Accept only exact earlier managed scripts for migration.
|
||||
check_owned_file(launcher, desired_launcher(root),
|
||||
(desired_launcher(root, pinned_font=True), desired_launcher(root, legacy=True)))
|
||||
(desired_launcher(root, prior_launcher=True), desired_launcher(root, pinned_font=True),
|
||||
desired_launcher(root, legacy=True)))
|
||||
check_owned_file(manifest, desired)
|
||||
check_owned_file(desktop_path(root), desired_desktop(launcher))
|
||||
return manifest, desired
|
||||
@@ -744,7 +775,8 @@ def uninstall(root, launcher):
|
||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||
check_owned_file(manifest, desired)
|
||||
check_owned_file(launcher, desired_launcher(root),
|
||||
(desired_launcher(root, pinned_font=True), desired_launcher(root, legacy=True)))
|
||||
(desired_launcher(root, prior_launcher=True), desired_launcher(root, pinned_font=True),
|
||||
desired_launcher(root, legacy=True)))
|
||||
desktop = desktop_path(root)
|
||||
check_owned_file(desktop, desired_desktop(launcher))
|
||||
versions = root / "versions"
|
||||
|
||||
@@ -438,6 +438,84 @@ class InstallTests(unittest.TestCase):
|
||||
self.install("0.1.202609241530", a, h, "--without-model")
|
||||
self.assertTrue((self.data / "frameyap/versions/0.1.202609241530/model/weights.bin").exists())
|
||||
|
||||
def test_launcher_run_defaults_and_explicit_overrides_preserve_argv(self):
|
||||
native = self.stage / "bin/frameyap"
|
||||
native.write_text('#!/usr/bin/env python3\nimport json, sys\nprint(json.dumps(sys.argv[1:]))\n')
|
||||
native.chmod(0o755)
|
||||
a, h = self.package("0.1.202609241530")
|
||||
self.install("0.1.202609241530", a, h)
|
||||
root = self.data / "frameyap/current"
|
||||
launcher = self.home / ".local/bin/frameyap"
|
||||
base_env = {**os.environ, "GAMESCOPE_SOCKET": "", "FRAMEYAP_PYTHON": "", "FRAMEYAP_MODEL": ""}
|
||||
|
||||
def run(*args, env=base_env):
|
||||
result = subprocess.run([str(launcher), *args], env=env, capture_output=True, text=True, check=True)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
defaults = ["--assets", str(root / "assets"), "--worker", str(root / "python/frameyap/worker.py"),
|
||||
"--python", str(root / "runtime/bin/python3"), "--model", str(root / "model")]
|
||||
self.assertEqual(run(), ["--run", *defaults])
|
||||
marker = self.base / "not-executed"
|
||||
literal = f"$(touch {marker})"
|
||||
config = self.home / ".config/frameyap/paths.conf"
|
||||
config.parent.mkdir(parents=True, exist_ok=True)
|
||||
config.write_text(f"python=/opt/approved python/{literal}\n"
|
||||
"model=/opt/local model; touch not-executed\n")
|
||||
config_defaults = [*defaults[:4], "--python", f"/opt/approved python/{literal}",
|
||||
"--model", "/opt/local model; touch not-executed"]
|
||||
self.assertEqual(run("--run"), ["--run", *config_defaults])
|
||||
self.assertFalse(marker.exists())
|
||||
env_defaults = [*defaults[:4], "--python", f"/env/python $HOME; {literal}",
|
||||
"--model", "/env/model * 'quoted'"]
|
||||
env = {**base_env, "FRAMEYAP_PYTHON": env_defaults[5], "FRAMEYAP_MODEL": env_defaults[7],
|
||||
"GAMESCOPE_SOCKET": "env-socket $HOME"}
|
||||
self.assertEqual(run(env=env), ["--run", *env_defaults, "--socket", "env-socket $HOME"])
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
# Each explicit flag wins independently, even when environment/config supplies defaults.
|
||||
for flag, value in (("--assets", f"/other assets/{literal}"),
|
||||
("--worker", "/other worker; touch not-executed"),
|
||||
("--python", "/other python $HOME"),
|
||||
("--model", "/other model *"),
|
||||
("--socket", f"explicit socket; {literal}")):
|
||||
with self.subTest(flag=flag):
|
||||
argv = run("--run", flag, value, env=env)
|
||||
self.assertEqual(argv.count(flag), 1)
|
||||
self.assertEqual(argv[argv.index(flag) + 1], value)
|
||||
if flag == "--socket":
|
||||
self.assertNotIn("env-socket $HOME", argv)
|
||||
else:
|
||||
self.assertIn("env-socket $HOME", argv)
|
||||
explicit = ["--model", "/my model", "--worker", "/my worker", "--assets", "/my assets",
|
||||
"--python", "/my python", "--socket", "my socket", "--head"]
|
||||
self.assertEqual(run("--run", *explicit, env=env), ["--run", *explicit])
|
||||
self.assertEqual(run("--run", "--python", "/one", "--python", "/two"),
|
||||
["--run", *defaults[:4], "--model", "/opt/local model; touch not-executed",
|
||||
"--python", "/one", "--python", "/two"])
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
def test_prior_managed_launcher_migration_and_modified_refusal(self):
|
||||
a, h = self.package("0.1.202609241530")
|
||||
self.install("0.1.202609241530", a, h)
|
||||
root = self.data / "frameyap"
|
||||
launcher = self.home / ".local/bin/frameyap"
|
||||
old = installer.desired_launcher(root, prior_launcher=True)
|
||||
self.assertIn(b'shift; exec ', old)
|
||||
self.assertIn(b'--python "$PYTHON" --model "$MODEL" "$@"', old)
|
||||
launcher.write_bytes(old)
|
||||
self.install("0.1.202609241530", a, h)
|
||||
self.assertEqual(launcher.read_bytes(), installer.desired_launcher(root))
|
||||
launcher.write_bytes(old + b'# user change\n')
|
||||
with self.assertRaisesRegex(ValueError, "foreign file"):
|
||||
self.install("0.1.202609241530", a, h)
|
||||
with self.assertRaisesRegex(ValueError, "foreign file"):
|
||||
installer.main(["--uninstall", "--unregistered"])
|
||||
self.assertEqual(launcher.read_bytes(), old + b'# user change\n')
|
||||
launcher.write_bytes(old)
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
installer.main(["--uninstall", "--unregistered"])
|
||||
self.assertFalse(launcher.exists())
|
||||
|
||||
def test_no_model_reinstall_preserves_provisioned_model_and_uninstall(self):
|
||||
a, h = self.package("0.1.202609241530")
|
||||
self.install("0.1.202609241530", a, h, "--without-model")
|
||||
|
||||
Reference in new issue
Block a user