nixpkgs mirror (for testing)
github.com/NixOS/nixpkgs
nix
1{
2 lib,
3 stdenv,
4 fetchFromGitHub,
5 python3Packages,
6 replaceVars,
7 macmon,
8
9 # pyo3-bindings
10 rustPlatform,
11
12 # dashboard
13 buildNpmPackage,
14 fetchNpmDeps,
15
16 writableTmpDirAsHomeHook,
17
18 nix-update-script,
19}:
20let
21 version = "1.0.67";
22 src = fetchFromGitHub {
23 name = "exo";
24 owner = "exo-explore";
25 repo = "exo";
26 tag = "v${version}";
27 hash = "sha256-hipCiAqCkkyrVcQXEZKbGoVbgjM3hykUcazNPEbT+q8=";
28 };
29
30 pyo3-bindings = python3Packages.buildPythonPackage (finalAttrs: {
31 pname = "exo-pyo3-bindings";
32 inherit version src;
33 pyproject = true;
34
35 buildAndTestSubdir = "rust/exo_pyo3_bindings";
36
37 cargoDeps = rustPlatform.fetchCargoVendor {
38 inherit (finalAttrs) pname src version;
39 hash = "sha256-N7B1WFqPdqeNPZe9hXGyX7F3EbB1spzeKc19BFDDwls=";
40 };
41
42 # Bypass rust nightly features not being available on rust stable
43 env.RUSTC_BOOTSTRAP = 1;
44
45 nativeBuildInputs = [
46 rustPlatform.cargoSetupHook
47 rustPlatform.maturinBuildHook
48 ];
49
50 nativeCheckInputs = with python3Packages; [
51 pytest-asyncio
52 pytestCheckHook
53 ];
54
55 enabledTestPaths = [
56 "rust/exo_pyo3_bindings/tests/"
57 ];
58
59 # RuntimeError
60 # Attempted to create a NULL object
61 doCheck = !stdenv.hostPlatform.isDarwin;
62 });
63
64 dashboard = buildNpmPackage (finalAttrs: {
65 pname = "exo-dashboard";
66 inherit src version;
67
68 sourceRoot = "${finalAttrs.src.name}/dashboard";
69
70 npmDeps = fetchNpmDeps {
71 inherit (finalAttrs)
72 pname
73 version
74 src
75 sourceRoot
76 ;
77 fetcherVersion = 2;
78 hash = "sha256-3ZgE1ysb1OeB4BNszvlrnYcc7gOo7coPfOEQmMHC6E0=";
79 };
80 });
81
82 # exo requires building mlx-lm from its main branch to use the kimi-k2.5 model
83 mlx-lm-unstable = python3Packages.mlx-lm.overridePythonAttrs (old: {
84 version = "0.30.4-unstable-2026-01-27";
85 src = old.src.override {
86 rev = "96699e6dadb13b82b28285bb131a0741997d19ae";
87 tag = null;
88 hash = "sha256-L1ws8XA8VhR18pRuRGbVal/yEfJaFNW8QzS16C1dFpE=";
89 };
90 meta = old.meta // {
91 changelog = "https://github.com/ml-explore/mlx-lm/releases/tag/v0.30.5";
92 };
93 });
94in
95python3Packages.buildPythonApplication (finalAttrs: {
96 pname = "exo";
97 pyproject = true;
98
99 inherit version src;
100
101 patches = [
102 (replaceVars ./inject-dashboard-path.patch {
103 dashboard = "${dashboard}/lib/node_modules/${dashboard.pname}/build";
104 })
105 ];
106
107 postPatch = ''
108 substituteInPlace pyproject.toml \
109 --replace-fail "uv_build>=0.8.9,<0.9.0" "uv_build"
110 ''
111 # MemoryObjectStreamState was renamed in
112 # https://github.com/agronholm/anyio/pull/1009/changes/bdc945a826d0d5917aea3517ceb9fe335b468094
113 + ''
114 substituteInPlace src/exo/utils/channels.py \
115 --replace-fail \
116 "MemoryObjectStreamState as AnyioState," \
117 "_MemoryObjectStreamState as AnyioState,"
118 ''
119 + lib.optionalString stdenv.hostPlatform.isDarwin ''
120 substituteInPlace src/exo/utils/info_gatherer/info_gatherer.py \
121 --replace-fail \
122 'shutil.which("macmon")' \
123 '"${lib.getExe macmon}"'
124 '';
125
126 build-system = with python3Packages; [
127 uv-build
128 ];
129
130 pythonRelaxDeps = true;
131
132 pythonRemoveDeps = [
133 "types-aiofiles"
134 "uuid"
135 ];
136 dependencies =
137 with python3Packages;
138 [
139 aiofiles
140 aiohttp
141 aiohttp-cors
142 anyio
143 fastapi
144 filelock
145 grpcio
146 grpcio-tools
147 httpx
148 huggingface-hub
149 hypercorn
150 jinja2
151 loguru
152 mflux
153 mlx
154 mlx-lm-unstable
155 nvidia-ml-py
156 openai
157 openai-harmony
158 opencv-python
159 pillow
160 prometheus-client
161 psutil
162 pydantic
163 pyo3-bindings
164 python-multipart
165 rustworkx
166 scapy
167 tiktoken
168 tinygrad
169 tomlkit
170 transformers
171 uvloop
172 ]
173 ++ sqlalchemy.optional-dependencies.asyncio;
174
175 pythonImportsCheck = [
176 "exo"
177 "exo.main"
178 ];
179
180 nativeCheckInputs = [
181 python3Packages.pytest-asyncio
182 python3Packages.pytestCheckHook
183 writableTmpDirAsHomeHook
184 ];
185
186 # Otherwise fails with 'import file mismatch'
187 preCheck = ''
188 rm src/exo/__init__.py
189 '';
190
191 disabledTests = lib.optionals stdenv.hostPlatform.isDarwin [
192 # AssertionError: assert "MacMon not found in PATH" in str(exc_info.value)
193 "test_macmon_not_found_raises_macmon_error"
194
195 # ValueError: zip() argument 2 is longer than argument 1
196 "test_events_processed_in_correct_order"
197
198 # system_profiler is not available in the sandbox
199 "test_tb_parsing"
200
201 # Flaky in the sandbox (even when __darwinAllowLocalNetworking is enabled)
202 # RuntimeError - Attempted to create a NULL object.
203 "test_sleep_on_multiple_items"
204
205 # Flaky in the sandbox (even when __darwinAllowLocalNetworking is enabled)
206 # AssertionError: Expected 2 results, got 0. Errors: {0: "[ring] Couldn't bind socket (error: 1)"}
207 "test_composed_call_works"
208 ];
209
210 disabledTestPaths = [
211 # All tests hang indefinitely
212 "src/exo/worker/tests/unittests/test_mlx/test_tokenizers.py"
213 ];
214
215 passthru = {
216 updateScript = nix-update-script { };
217 exo-pyo3-bindings = pyo3-bindings;
218 exo-dashboard = dashboard;
219 };
220
221 meta = {
222 description = "Run your own AI cluster at home with everyday devices";
223 homepage = "https://github.com/exo-explore/exo";
224 changelog = "https://github.com/exo-explore/exo/releases/tag/${finalAttrs.src.tag}";
225 license = lib.licenses.gpl3Only;
226 maintainers = with lib.maintainers; [ GaetanLepage ];
227 mainProgram = "exo";
228 };
229})