Coverage for src/lilbee/providers/fleet/engine_install.py: 100%

47 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-11-06 08:17 +0000

1"""Install advice for a missing engine, in the form that fits how lilbee was installed.""" 

2 

3from __future__ import annotations 

4 

5import sys 

6from enum import StrEnum 

7from pathlib import Path 

8 

9from lilbee._frozen import is_frozen 

10 

11 

12class InstallMethod(StrEnum): 

13 """How this lilbee was put on the machine.""" 

14 

15 UV_TOOL = "uv_tool" 

16 UV_ENV = "uv_env" 

17 PIPX = "pipx" 

18 PIP = "pip" 

19 HOMEBREW = "homebrew" 

20 STANDALONE = "standalone" 

21 UNKNOWN = "unknown" 

22 

23 

24# Hardware label and index name, one line each in the advice. One index per 

25# hardware because the builds are not interchangeable: cpu has every GPU backend 

26# off, metal ships only a macOS arm64 wheel, vulkan only Linux and Windows ones. 

27_INDEXES: tuple[tuple[str, str], ...] = ( 

28 ("NVIDIA (CUDA)", "cu125"), 

29 ("AMD (ROCm)", "rocm"), 

30 ("Apple silicon", "metal"), 

31 ("Other GPUs", "vulkan"), 

32 ("No GPU", "cpu"), 

33) 

34_INDEX_URL = "https://lilbee.sh/{index}/" 

35_COMMANDS: dict[InstallMethod, str] = { 

36 InstallMethod.PIP: "pip install --pre 'lilbee[engine]' --extra-index-url {url}", 

37 InstallMethod.UV_ENV: ( 

38 "uv pip install --prerelease=allow 'lilbee[engine]' --extra-index-url {url}" 

39 ), 

40 InstallMethod.UV_TOOL: ( 

41 "uv tool install --force --prerelease=allow 'lilbee[engine]' --extra-index-url {url}" 

42 ), 

43 InstallMethod.PIPX: ( 

44 "pipx install --force --pip-args='--pre --extra-index-url {url}' 'lilbee[engine]'" 

45 ), 

46} 

47_BUNDLED_FORMS = ( 

48 "Install a self-contained build again; it carries its own engine: " 

49 "'brew reinstall lilbee' (Homebrew), 'npm install -g lilbee' (npm launcher), " 

50 "or the standalone binary from https://github.com/tobocop2/lilbee/releases/latest." 

51) 

52_BRING_YOUR_OWN = ( 

53 "To bring your own engine, set LILBEE_LLAMA_SERVER_PATH to a llama-server binary " 

54 "and put llama-swap / gguf-parser on PATH." 

55) 

56 

57 

58def detect_install_method() -> InstallMethod: 

59 """Tell how lilbee was installed from the interpreter's own location.""" 

60 if is_frozen(): 

61 exe = Path(sys.executable).as_posix().lower() 

62 return ( 

63 InstallMethod.HOMEBREW 

64 if ("/cellar/" in exe or "/homebrew/" in exe) 

65 else (InstallMethod.STANDALONE) 

66 ) 

67 prefix = Path(sys.prefix).as_posix().lower() 

68 if "/uv/tools/" in prefix: 

69 return InstallMethod.UV_TOOL 

70 if "/pipx/venvs/" in prefix: 

71 return InstallMethod.PIPX 

72 if sys.prefix == sys.base_prefix: 

73 return InstallMethod.UNKNOWN 

74 try: 

75 lines = (Path(sys.prefix) / "pyvenv.cfg").read_text(encoding="utf-8").splitlines() 

76 except OSError: 

77 return InstallMethod.PIP 

78 made_by_uv = any(line.partition("=")[0].strip() == "uv" for line in lines) 

79 return InstallMethod.UV_ENV if made_by_uv else InstallMethod.PIP 

80 

81 

82def _index_lines(method: InstallMethod) -> list[str]: 

83 template = _COMMANDS[method] 

84 return [ 

85 f" {label + ':':<15}{template.format(url=_INDEX_URL.format(index=index))}" 

86 for label, index in _INDEXES 

87 ] 

88 

89 

90def engine_install_hint(method: InstallMethod | None = None) -> str: 

91 """The advice for installing the engine, for *method* or the detected install.""" 

92 method = detect_install_method() if method is None else method 

93 if method in (InstallMethod.HOMEBREW, InstallMethod.STANDALONE): 

94 return f"{_BUNDLED_FORMS} {_BRING_YOUR_OWN}" 

95 intro = ( 

96 "The engine is lilbee's 'engine' extra, published on lilbee.sh rather than " 

97 "PyPI, so the index is part of the command:" 

98 ) 

99 if method is InstallMethod.UNKNOWN: 

100 blocks = [ 

101 "With pip:", 

102 *_index_lines(InstallMethod.PIP), 

103 "With uv:", 

104 *_index_lines(InstallMethod.UV_TOOL), 

105 ] 

106 else: 

107 blocks = _index_lines(method) 

108 return "\n".join([intro, *blocks, _BUNDLED_FORMS, _BRING_YOUR_OWN])