b11146 ships ubuntu-cuda-13.4-arm64 (+ cudart), so llamacpp-cuda gains linux-arm64, auto resolves NVIDIA arm64 Linux to CUDA, and the desktop's localModels flag ships on Linux arm64 too. Every upstream Linux llama.cpp build (CPU included) links the system libgomp, which minimal hosts (WSL, containers) lack, and a normal install never touches the system package manager. Each Linux engine now pins Ubuntu 24.04's libgomp1 .deb (glibc 2.38, the floor upstream already sets) and stages libgomp.so.1 beside llama-server, where the $ORIGIN RUNPATH finds it; the artifact mirror covers the pool URL after 24.04's EOL. DebPackage's ar+tar unpack becomes pm.package.unpack_deb and reads zstd data.tar (Ubuntu's default) with the stdlib codec PM's 3.14 runtime has. Pre-PM engines on Linux carry only llama.cpp's own archives, so adoption maps their manifests to those and records them as outdated (usable now, update offered) instead of failing to match. Verified on an RTX Spark N1X (WSL, Ubuntu 26.04, no system libgomp): ensure_engine() for cuda, cpu and vulkan through PM's real download/stage/ verify path; ldd resolves libgomp from the entry; llama-server serves completions (CUDA0 at 445 tok/s). linux-x64 cpu/vulkan staged through the same path with no NEEDED library missing beyond glibc/libstdc++/OpenSSL 3.
203 lines
8.4 KiB
Python
203 lines
8.4 KiB
Python
"""A pre-PM engine under runtimes/llamacpp/ moves into PM's store instead of forcing first-run setup."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
import pm
|
|
from pm import paths
|
|
from pm.lock import Facts, Lockfile
|
|
|
|
# The engine archive, cudart (CUDA only), and the libgomp .deb (Linux only), in PM's archive order.
|
|
PINNED = ("a" * 64, "c" * 64, "e" * 64)
|
|
|
|
|
|
@pytest.fixture(params=["cpu", "cuda"])
|
|
def legacy_env(request, tmp_path, monkeypatch):
|
|
backend = request.param
|
|
home = tmp_path / "home"
|
|
store = home / "tools"
|
|
lock_path = tmp_path / "lock.json"
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
monkeypatch.setattr(paths, "store_root", lambda: store)
|
|
monkeypatch.setattr(paths, "lockfile_path", lambda: lock_path)
|
|
target = pm.current_target()
|
|
package = pm.get_package(f"llamacpp-{backend}")
|
|
if package.missing_reason(target):
|
|
pytest.skip(f"{package.name} has no build for {target}")
|
|
urls = package.fetch_urls("10964", target)
|
|
lock = Lockfile(lock_path)
|
|
lock.set_pin(package.name, "10964", {target: [{"url": u, "sha256": s} for u, s in zip(urls, PINNED)]})
|
|
lock.save()
|
|
# verify() runs `llama-server --version`; the fixture binary is not executable.
|
|
monkeypatch.setattr(type(package), "verify", lambda self, entry, target: "")
|
|
from hermes_cli.local_runtime import binaries
|
|
|
|
monkeypatch.setattr(binaries, "_LEFT_IN_PLACE", set())
|
|
return home, store, package, target, backend, list(PINNED[:len(urls)])
|
|
|
|
|
|
def legacy_digests(package, target, pinned):
|
|
"""What a pre-PM manifest records: llama.cpp's own release assets, never PM's extra archives."""
|
|
return pinned[:len(package._asset_names("10964", target))]
|
|
|
|
|
|
def legacy_install(home, package, target, backend, tag, digests, *, verified=True):
|
|
install = home / "runtimes" / "llamacpp" / tag / backend
|
|
install.mkdir(parents=True)
|
|
package.binary(install, target).write_bytes(b"engine " + tag.encode())
|
|
(install / "ggml-base.dll").write_bytes(b"dll")
|
|
names = package._asset_names(tag.removeprefix("b"), target)
|
|
manifest = {"tag": tag, "backend": backend, "assets": dict(zip(names, digests))}
|
|
if verified:
|
|
manifest["verified_version"] = f"version: 0.4.1-dev (build {tag[1:]}, commit 0)"
|
|
(install / "manifest.json").write_bytes(json.dumps(manifest).encode("utf-8"))
|
|
return install
|
|
|
|
|
|
def test_matching_legacy_engine_moves_into_the_store_as_current(legacy_env):
|
|
from hermes_cli.local_runtime import binaries
|
|
|
|
home, store, package, target, backend, pinned = legacy_env
|
|
legacy = legacy_digests(package, target, pinned)
|
|
install = legacy_install(home, package, target, backend, "b10964", legacy)
|
|
|
|
engine = binaries.installed_engine(backend)
|
|
|
|
entry = store / package.store_entry("10964", target)
|
|
assert engine == binaries.Engine(backend, "b10964", package.binary(entry, target))
|
|
assert engine.binary.read_bytes() == b"engine b10964"
|
|
assert not install.exists() and not install.parent.exists()
|
|
fact = Facts(store / "facts.json").get(package.name)
|
|
assert fact["version"] == "10964" and fact["artifacts"] == legacy
|
|
# Current exactly when the legacy install carries every archive PM pins.
|
|
assert (binaries.installed_engine(backend, allow_outdated=False) == engine) == (legacy == pinned)
|
|
|
|
|
|
def test_older_legacy_engine_counts_as_installed_but_outdated(legacy_env):
|
|
from hermes_cli.local_runtime import binaries
|
|
|
|
home, _, package, target, backend, pinned = legacy_env
|
|
legacy_install(home, package, target, backend, "b10679",
|
|
["d" * 64, *legacy_digests(package, target, pinned)[1:]])
|
|
|
|
assert binaries.installed_engine(backend).tag == "b10679"
|
|
assert pm.installed_package(package.name) is None # the pane offers the update
|
|
assert binaries.installed_engine(backend, allow_outdated=False) is None
|
|
|
|
|
|
def test_newest_legacy_engine_wins_and_older_stays(legacy_env):
|
|
from hermes_cli.local_runtime import binaries
|
|
|
|
home, _, package, target, backend, pinned = legacy_env
|
|
old = legacy_install(home, package, target, backend, "b10679",
|
|
["d" * 64, *legacy_digests(package, target, pinned)[1:]])
|
|
legacy_install(home, package, target, backend, "b10964", legacy_digests(package, target, pinned))
|
|
|
|
assert binaries.installed_engine(backend).tag == "b10964"
|
|
assert old.is_dir()
|
|
|
|
|
|
@pytest.mark.parametrize("damage", ["unverified", "missing_digest", "wrong_backend", "tag_mismatch"])
|
|
def test_unverified_or_damaged_legacy_engine_is_left_alone(legacy_env, damage):
|
|
from hermes_cli.local_runtime import binaries
|
|
|
|
home, store, package, target, backend, pinned = legacy_env
|
|
legacy = legacy_digests(package, target, pinned)
|
|
install = legacy_install(home, package, target, backend, "b10964",
|
|
["not-a-digest", *legacy[1:]] if damage == "missing_digest" else legacy,
|
|
verified=damage != "unverified")
|
|
manifest = json.loads((install / "manifest.json").read_bytes())
|
|
if damage == "wrong_backend":
|
|
manifest["backend"] = "vulkan"
|
|
if damage == "tag_mismatch":
|
|
manifest["tag"] = "b10679"
|
|
(install / "manifest.json").write_bytes(json.dumps(manifest).encode("utf-8"))
|
|
|
|
assert binaries.installed_engine(backend) is None
|
|
assert install.is_dir()
|
|
assert Facts(store / "facts.json").get(package.name) is None
|
|
|
|
|
|
def test_engine_that_fails_verification_stays_and_is_not_retried(legacy_env, monkeypatch):
|
|
from hermes_cli.local_runtime import binaries
|
|
|
|
home, _, package, target, backend, pinned = legacy_env
|
|
install = legacy_install(home, package, target, backend, "b10964", legacy_digests(package, target, pinned))
|
|
probes = []
|
|
|
|
def failing(self, entry, target):
|
|
probes.append(entry)
|
|
return "llama-server --version exited 1"
|
|
|
|
monkeypatch.setattr(type(package), "verify", failing)
|
|
assert binaries.installed_engine(backend) is None
|
|
assert binaries.installed_engine(backend) is None
|
|
assert install.is_dir() and probes == [install]
|
|
|
|
|
|
def test_existing_pm_engine_is_never_replaced(legacy_env):
|
|
from hermes_cli.local_runtime import binaries
|
|
from pm.store import tree_digest
|
|
|
|
home, store, package, target, backend, pinned = legacy_env
|
|
entry = store / package.store_entry("10964", target)
|
|
entry.mkdir(parents=True)
|
|
package.binary(entry, target).write_bytes(b"pm engine")
|
|
Facts(store / "facts.json").record(package.name, "10964", entry.name, {}, store, target=target,
|
|
artifacts=pinned, digest=tree_digest(entry))
|
|
install = legacy_install(home, package, target, backend, "b10964", legacy_digests(package, target, pinned))
|
|
|
|
assert binaries.installed_engine(backend).binary.read_bytes() == b"pm engine"
|
|
assert install.is_dir()
|
|
|
|
|
|
def test_busy_store_leaves_the_engine_for_a_later_call(legacy_env):
|
|
from hermes_cli.local_runtime import binaries
|
|
from pm.filesystem import lock_fd
|
|
|
|
home, store, package, target, backend, pinned = legacy_env
|
|
install = legacy_install(home, package, target, backend, "b10964", legacy_digests(package, target, pinned))
|
|
store.mkdir(parents=True, exist_ok=True)
|
|
held, release = threading.Event(), threading.Event()
|
|
|
|
def hold():
|
|
# A second open file: msvcrt byte locks and flock both exclude it within one process.
|
|
fd = os.open(store / ".install.lock", os.O_CREAT | os.O_RDWR, 0o600)
|
|
try:
|
|
assert lock_fd(fd, wait=True)
|
|
held.set()
|
|
release.wait(30)
|
|
finally:
|
|
os.close(fd)
|
|
|
|
holder = threading.Thread(target=hold)
|
|
holder.start()
|
|
try:
|
|
assert held.wait(10)
|
|
assert binaries.installed_engine(backend) is None
|
|
assert install.is_dir()
|
|
finally:
|
|
release.set()
|
|
holder.join()
|
|
assert binaries.installed_engine(backend).tag == "b10964"
|
|
|
|
|
|
def test_status_route_reports_the_moved_engine(legacy_env, monkeypatch):
|
|
from hermes_cli.web_routers import local_models as lm
|
|
|
|
home, _, package, target, backend, pinned = legacy_env
|
|
legacy = legacy_digests(package, target, pinned)
|
|
legacy_install(home, package, target, backend, "b10964", legacy)
|
|
monkeypatch.setattr(lm, "_runtime_section", lambda: {"enabled": True, "backend": backend})
|
|
monkeypatch.setattr(lm, "_state_endpoint", lambda: None)
|
|
|
|
status = lm.local_models_status()
|
|
|
|
assert status["runtime_installed"] and status["runtime_backend"] == backend
|
|
assert status["tag"] == "b10964" and status["update_available"] == (legacy != pinned)
|