mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 03:00:04 +02:00
installer: add explicit --install-runtime for the pinned CPU Python runtime (D4)
Creates a user-local venv under $XDG_DATA_HOME/frameyap/runtimes from the invoking python3 (3.10-3.13), installs CPU torch==2.8.0 from PyTorch's CPU index and moondream==2.4.0 from PyPI (binary wheels only, pip --isolated, Torch constrained), verifies imports and no CUDA, then sets python= in paths.conf and removes superseded managed runtimes. Requires --yes; has a read-only --print-plan/--json. A failure removes only the new venv. Offline tests mock pip; a real run passed on x86-64 Python 3.12. ARM64 Frame install is still pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70ea838a59
commit
e5b998eb6c
9 files changed
+334
-38
No files matched your search
@@ -1113,6 +1113,92 @@ with patch.object(module, "check_host"), patch.object(module.urllib.request, "ur
|
||||
self.assertIn("mismatched", json.loads(output.getvalue())["message"])
|
||||
fetch.assert_not_called()
|
||||
|
||||
def test_runtime_install_is_explicit_cpu_pinned_and_updates_only_python_path(self):
|
||||
# subprocess is mocked: no venv, pip or network is touched.
|
||||
root = self.data / "frameyap"
|
||||
output = io.StringIO()
|
||||
with contextlib.redirect_stdout(output):
|
||||
self.assertEqual(installer.cli(["--install-runtime", "--print-plan", "--json"]), 0)
|
||||
plan = json.loads(output.getvalue())
|
||||
self.assertEqual((plan["operation"], plan["network"]), ("install-runtime", True))
|
||||
self.assertIn("torch==2.8.0 from https://download.pytorch.org/whl/cpu", plan["packages"])
|
||||
self.assertFalse(root.exists())
|
||||
for extra in (["--version", "0.1.202609241530"], ["--backend", "other"], ["--without-model"]):
|
||||
output = io.StringIO()
|
||||
with contextlib.redirect_stdout(output):
|
||||
self.assertEqual(installer.cli(["--install-runtime", "--json"] + extra), 2)
|
||||
output = io.StringIO()
|
||||
with contextlib.redirect_stdout(output), patch.object(installer.subprocess, "run") as run:
|
||||
self.assertEqual(installer.cli(["--install-runtime", "--json"]), 1)
|
||||
run.assert_not_called()
|
||||
self.assertIn("--yes", json.loads(output.getvalue())["message"])
|
||||
self.assertFalse((root / "runtimes").exists())
|
||||
|
||||
config = self.home / ".config/frameyap/paths.conf"
|
||||
config.parent.mkdir(parents=True)
|
||||
config.write_text("# mine\npython=/opt/old/bin/python3\nmodel=/opt/model\n")
|
||||
old = root / "runtimes/cpu-20260101000000"
|
||||
old.mkdir(parents=True)
|
||||
foreign = root / "runtimes/keep-me"
|
||||
foreign.mkdir()
|
||||
commands = []
|
||||
|
||||
def fake(command, **kwargs):
|
||||
commands.append(command)
|
||||
if command[1:3] == ["-m", "venv"]:
|
||||
python = Path(command[3]) / "bin/python3"
|
||||
python.parent.mkdir(parents=True)
|
||||
python.write_text("")
|
||||
return SimpleNamespace(returncode=0)
|
||||
|
||||
with patch.object(installer, "RUNTIME_PYTHON_RANGE", ((3, 0), (3, 1))), \
|
||||
patch.object(installer.subprocess, "run") as run:
|
||||
with contextlib.redirect_stdout(io.StringIO()) as unsupported:
|
||||
self.assertEqual(installer.cli(["--install-runtime", "--yes", "--json"]), 1)
|
||||
self.assertIn("Python 3.10-3.13", json.loads(unsupported.getvalue())["message"])
|
||||
run.assert_not_called()
|
||||
self.env_range = patch.object(installer, "RUNTIME_PYTHON_RANGE", ((3, 0), (4, 0)))
|
||||
self.env_range.start()
|
||||
self.addCleanup(self.env_range.stop)
|
||||
output = io.StringIO()
|
||||
with contextlib.redirect_stdout(output), patch.object(installer.subprocess, "run", side_effect=fake):
|
||||
self.assertEqual(installer.cli(["--install-runtime", "--yes", "--json"]), 0)
|
||||
events = [json.loads(line) for line in output.getvalue().splitlines()]
|
||||
self.assertEqual([e.get("step") for e in events if e["event"] == "runtime_step"],
|
||||
["create virtual environment", "install CPU Torch", "install moondream/Kestrel",
|
||||
"verify CPU runtime imports"])
|
||||
self.assertEqual(events[-1]["event"], "complete")
|
||||
torch, packages = commands[1], commands[2]
|
||||
for command in (torch, packages):
|
||||
self.assertIn("--isolated", command)
|
||||
self.assertIn("--only-binary=:all:", command)
|
||||
self.assertEqual(torch[-3:], ["--index-url", "https://download.pytorch.org/whl/cpu", "torch==2.8.0"])
|
||||
self.assertEqual(packages[-1], "moondream==2.4.0")
|
||||
self.assertIn("--constraint", packages)
|
||||
runtimes = [item for item in (root / "runtimes").iterdir() if item.name.startswith("cpu-")]
|
||||
self.assertEqual(len(runtimes), 1)
|
||||
self.assertNotEqual(runtimes[0], old)
|
||||
self.assertTrue(foreign.is_dir())
|
||||
self.assertEqual(config.read_text(),
|
||||
f"# mine\nmodel=/opt/model\npython={runtimes[0]}/bin/python3\n")
|
||||
|
||||
# A failed pip step removes only its own new venv and leaves paths.conf alone.
|
||||
before = config.read_text()
|
||||
|
||||
def failing(command, **kwargs):
|
||||
if "moondream==2.4.0" in command:
|
||||
raise subprocess.CalledProcessError(1, command)
|
||||
return fake(command, **kwargs)
|
||||
|
||||
output = io.StringIO()
|
||||
with contextlib.redirect_stdout(output), patch.object(installer.subprocess, "run", side_effect=failing), \
|
||||
patch.object(installer, "datetime") as clock:
|
||||
clock.now.return_value.strftime.return_value = "cpu-20991231235959"
|
||||
self.assertEqual(installer.cli(["--install-runtime", "--yes", "--json"]), 1)
|
||||
self.assertFalse((root / "runtimes/cpu-20991231235959").exists())
|
||||
self.assertTrue(runtimes[0].is_dir())
|
||||
self.assertEqual(config.read_text(), before)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user