5
5
from pathlib import Path
6
6
import shutil
7
7
import re
8
+ import os
8
9
from typing import Callable , NamedTuple , Optional , Union
9
10
from PyQt5 .QtNetwork import QNetworkAccessManager
10
11
@@ -48,11 +49,13 @@ class Server:
48
49
comfy_dir : Optional [Path ] = None
49
50
version : Optional [str ] = None
50
51
52
+ _uv_cmd : Optional [Path ] = None
51
53
_python_cmd : Optional [Path ] = None
52
54
_cache_dir : Path
53
55
_version_file : Path
54
56
_process : Optional [asyncio .subprocess .Process ] = None
55
57
_task : Optional [asyncio .Task ] = None
58
+ _installed_backend : Optional [ServerBackend ] = None
56
59
57
60
def __init__ (self , path : Optional [str ] = None ):
58
61
self .path = Path (path or settings .server_path )
@@ -66,22 +69,38 @@ def check_install(self):
66
69
self ._cache_dir = self .path / ".cache"
67
70
68
71
self ._version_file = self .path / ".version"
72
+ self .version = None
69
73
if self ._version_file .exists ():
70
- self .version = self ._version_file .read_text ().strip ()
71
- log .info (f"Found server installation v{ self .version } at { self .path } " )
72
- else :
73
- self .version = None
74
+ content = self ._version_file .read_text ().strip ().split ()
75
+ if len (content ) > 0 :
76
+ self .version = content [0 ]
77
+ if len (content ) > 1 and content [1 ] in ServerBackend .__members__ :
78
+ self ._installed_backend = ServerBackend [content [1 ]]
79
+ if self .version is not None :
80
+ backend = f" [{ self ._installed_backend .name } ]" if self ._installed_backend else ""
81
+ log .info (f"Found server installation v{ self .version } { backend } at { self .path } " )
74
82
75
83
comfy_pkg = ["main.py" , "nodes.py" , "custom_nodes" ]
76
84
self .comfy_dir = _find_component (comfy_pkg , [self .path / "ComfyUI" ])
77
85
78
- python_pkg = ["python3.dll" , "python.exe" ] if is_windows else ["python3" , "pip3" ]
79
- python_search_paths = [self .path / "python" , self .path / "venv" / "bin" ]
86
+ uv_exe = "uv.exe" if is_windows else "uv"
87
+ self ._uv_cmd = self .path / "uv" / uv_exe
88
+ if not self ._uv_cmd .exists ():
89
+ self ._uv_cmd = None
90
+ self ._uv_cmd = _find_program (uv_exe )
91
+
92
+ python_pkg = ["python.exe" ] if is_windows else ["python3" ]
93
+ python_search_paths = [
94
+ self .path / "python" ,
95
+ self .path / "venv" / "bin" ,
96
+ self .path / "venv" / "Scripts" ,
97
+ ]
80
98
python_path = _find_component (python_pkg , python_search_paths )
81
99
if python_path is None :
82
- self ._python_cmd = _find_program (
83
- "python3.12" , "python3.11" , "python3.10" , "python3" , "python"
84
- )
100
+ if not is_windows :
101
+ self ._python_cmd = _find_program (
102
+ "python3.12" , "python3.11" , "python3.10" , "python3" , "python"
103
+ )
85
104
else :
86
105
self ._python_cmd = python_path / f"python{ _exe } "
87
106
@@ -124,16 +143,15 @@ async def _install(self, cb: InternalCB):
124
143
self ._cache_dir .mkdir (parents = True , exist_ok = True )
125
144
self ._version_file .write_text ("incomplete" )
126
145
127
- if is_windows and (self .comfy_dir is None or self . _python_cmd is None ):
128
- # On Windows install an embedded version of Python
129
- python_dir = self .path / "python"
130
- self . _python_cmd = python_dir / f"python { _exe } "
131
- await install_if_missing ( python_dir , self ._install_python , network , cb )
132
- elif not is_windows and ( self . comfy_dir is None or not ( self . path / "venv" ). exists ()):
133
- # On Linux a system Python is required to create a virtual environment
146
+ has_venv = (self .path / "venv" ). exists ()
147
+ has_embedded_python = ( self . path / "python" ). exists ()
148
+ has_uv = self ._uv_cmd is not None
149
+ if not any (( has_venv , has_embedded_python , has_uv )):
150
+ await try_install ( self . path / "uv" , self ._install_uv , network , cb )
151
+
152
+ if self . comfy_dir is None or not ( has_venv or has_embedded_python ):
134
153
python_dir = self .path / "venv"
135
154
await install_if_missing (python_dir , self ._create_venv , cb )
136
- self ._python_cmd = python_dir / "bin" / "python3"
137
155
assert self ._python_cmd is not None
138
156
await self ._log_python_version ()
139
157
await determine_system_encoding (str (self ._python_cmd ))
@@ -146,57 +164,59 @@ async def _install(self, cb: InternalCB):
146
164
dir = comfy_dir / "custom_nodes" / pkg .folder
147
165
await install_if_missing (dir , self ._install_custom_node , pkg , network , cb )
148
166
149
- self ._version_file .write_text (resources .version )
167
+ self ._version_file .write_text (f" { resources .version } { self . backend . name } " )
150
168
self .state = ServerState .stopped
151
169
cb ("Finished" , f"Installation finished in { self .path } " )
152
170
self .check_install ()
153
171
154
172
async def _log_python_version (self ):
173
+ if self ._uv_cmd is not None :
174
+ uv_ver = await get_python_version_string (self ._uv_cmd )
175
+ log .info (f"Using uv: { uv_ver } " )
155
176
if self ._python_cmd is not None :
156
177
python_ver = await get_python_version_string (self ._python_cmd )
157
178
log .info (f"Using Python: { python_ver } , { self ._python_cmd } " )
158
- pip_ver = await get_python_version_string (self ._python_cmd , "-m" , "pip" )
159
- log .info (f"Using pip: { pip_ver } " )
160
-
161
- def _pip_install (self , * args ):
162
- return [self ._python_cmd , "-su" , "-m" , "pip" , "install" , * args ]
163
-
164
- async def _install_python (self , network : QNetworkAccessManager , cb : InternalCB ):
165
- url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-embed-amd64.zip"
166
- archive_path = self ._cache_dir / "python-3.11.9-embed-amd64.zip"
167
- dir = self .path / "python"
168
-
169
- await _download_cached ("Python" , network , url , archive_path , cb )
170
- await _extract_archive ("Python" , archive_path , dir , cb )
171
-
172
- python_pth = dir / "python311._pth"
173
- cb ("Installing Python" , f"Patching { python_pth } " )
174
- with open (python_pth , "a" ) as file :
175
- file .write ("import site\n " )
176
-
177
- git_pip_url = "https://bootstrap.pypa.io/get-pip.py"
178
- get_pip_file = dir / "get-pip.py"
179
- await _download_cached ("Python" , network , git_pip_url , get_pip_file , cb )
180
- await _execute_process ("Python" , [self ._python_cmd , get_pip_file ], dir , cb )
181
- await _execute_process ("Python" , self ._pip_install ("wheel" , "setuptools" ), dir , cb )
179
+ if self ._uv_cmd is None :
180
+ pip_ver = await get_python_version_string (self ._python_cmd , "-m" , "pip" )
181
+ log .info (f"Using pip: { pip_ver } " )
182
+
183
+ def _pip_install (self , name : str , args : list [str ], cb : InternalCB ):
184
+ env = None
185
+ if self ._uv_cmd is not None :
186
+ env = {"VIRTUAL_ENV" : str (self .path / "venv" )}
187
+ cmd = [self ._uv_cmd , "pip" , "install" , * args ]
188
+ else :
189
+ cmd = [self ._python_cmd , "-su" , "-m" , "pip" , "install" , * args ]
190
+ return _execute_process (name , cmd , self .path , cb , env = env )
191
+
192
+ async def _install_uv (self , network : QNetworkAccessManager , cb : InternalCB ):
193
+ script_ext = ".ps1" if is_windows else ".sh"
194
+ url = f"https://astral.sh/uv/0.6.10/install{ script_ext } "
195
+ script_path = self ._cache_dir / f"install_uv{ script_ext } "
196
+ await _download_cached ("Python" , network , url , script_path , cb )
197
+
198
+ if is_windows :
199
+ del os .environ ["PSModulePath" ] # Don't inherit this from parent process
200
+ cmd = ["powershell" , "-ExecutionPolicy" , "Bypass" , "-File" , str (script_path )]
201
+ else :
202
+ cmd = ["/bin/sh" , str (script_path )]
203
+ env = {"UV_INSTALL_DIR" : str (self .path / "uv" )}
204
+ await _execute_process ("Python" , cmd , self .path , cb , env = env )
182
205
183
- cb ("Installing Python" , f"Patching { python_pth } " )
184
- _prepend_file (python_pth , "../ComfyUI\n " )
185
- cb ("Installing Python" , "Finished installing Python" )
206
+ self ._uv_cmd = self .path / "uv" / ("uv" + _exe )
207
+ cb ("Installing Python" , f"Installed uv at { self ._uv_cmd } " )
186
208
187
209
async def _create_venv (self , cb : InternalCB ):
188
210
cb ("Creating Python virtual environment" , f"Creating venv in { self .path / 'venv' } " )
189
- assert self ._python_cmd is not None
190
- python_version , major , minor = await get_python_version (self ._python_cmd )
191
- if major is not None and minor is not None and (major < 3 or minor < 9 ):
192
- raise Exception (
193
- _ (
194
- "Python version 3.9 or higher is required, but found {version} at {location}. Please make sure a compatible version of Python is installed and can be found by the Krita process."
195
- ).format (version = python_version , location = self ._python_cmd )
196
- )
197
- venv_cmd = [self ._python_cmd , "-m" , "venv" , "venv" ]
211
+ assert self ._uv_cmd is not None
212
+ venv_cmd = [self ._uv_cmd , "venv" , "--python" , "3.12" , str (self .path / "venv" )]
198
213
await _execute_process ("Python" , venv_cmd , self .path , cb )
199
214
215
+ if is_windows :
216
+ self ._python_cmd = self .path / "venv" / "Scripts" / "python.exe"
217
+ else :
218
+ self ._python_cmd = self .path / "venv" / "bin" / "python3"
219
+
200
220
async def _install_comfy (self , comfy_dir : Path , network : QNetworkAccessManager , cb : InternalCB ):
201
221
url = f"{ resources .comfy_url } /archive/{ resources .comfy_version } .zip"
202
222
archive_path = self ._cache_dir / f"ComfyUI-{ resources .comfy_version } .zip"
@@ -211,10 +231,13 @@ async def _install_comfy(self, comfy_dir: Path, network: QNetworkAccessManager,
211
231
torch_args += ["--index-url" , "https://download.pytorch.org/whl/cu124" ]
212
232
elif self .backend is ServerBackend .directml :
213
233
torch_args = ["numpy<2" , "torch-directml" ]
214
- await _execute_process ("PyTorch" , self ._pip_install (* torch_args ), self .path , cb )
234
+ await self ._pip_install ("PyTorch" , torch_args , cb )
235
+
236
+ requirements_txt = Path (__file__ ).parent / "server_requirements.txt"
237
+ await self ._pip_install ("ComfyUI" , ["-r" , str (requirements_txt )], cb )
215
238
216
239
requirements_txt = temp_comfy_dir / "requirements.txt"
217
- await _execute_process ("ComfyUI" , self . _pip_install ( "-r" , requirements_txt ), self . path , cb )
240
+ await self . _pip_install ("ComfyUI" , [ "-r" , str ( requirements_txt )] , cb )
218
241
219
242
_configure_extra_model_paths (temp_comfy_dir )
220
243
await rename_extracted_folder ("ComfyUI" , comfy_dir , resources .comfy_version )
@@ -233,26 +256,22 @@ async def _install_custom_node(
233
256
await _download_cached (pkg .name , network , resource_url , resource_zip_path , cb )
234
257
await _extract_archive (pkg .name , resource_zip_path , folder .parent , cb )
235
258
await rename_extracted_folder (pkg .name , folder , pkg .version )
236
-
237
- requirements_txt = folder / "requirements.txt"
238
- if requirements_txt .exists ():
239
- await _execute_process (pkg .name , self ._pip_install ("-r" , requirements_txt ), folder , cb )
240
259
cb (f"Installing { pkg .name } " , f"Finished installing { pkg .name } " )
241
260
242
261
async def _install_insightface (self , network : QNetworkAccessManager , cb : InternalCB ):
243
262
assert self .comfy_dir is not None and self ._python_cmd is not None
244
263
245
264
dependencies = ["onnx==1.16.1" , "onnxruntime" ] # onnx version pinned due to #1033
246
- await _execute_process ("FaceID" , self . _pip_install ( * dependencies ), self . path , cb )
265
+ await self . _pip_install ("FaceID" , dependencies , cb )
247
266
248
267
pyver = await get_python_version_string (self ._python_cmd )
249
268
if is_windows and "3.11" in pyver :
250
269
whl_file = self ._cache_dir / "insightface-0.7.3-cp311-cp311-win_amd64.whl"
251
270
whl_url = "https://github.com/bihailantian655/insightface_wheel/raw/main/insightface-0.7.3-cp311-cp311-win_amd64%20(1).whl"
252
271
await _download_cached ("FaceID" , network , whl_url , whl_file , cb )
253
- await _execute_process ("FaceID" , self . _pip_install (whl_file ), self . path , cb )
272
+ await self . _pip_install ("FaceID" , [ str (whl_file )] , cb )
254
273
else :
255
- await _execute_process ("FaceID" , self . _pip_install ( "insightface" ), self . path , cb )
274
+ await self . _pip_install ("FaceID" , [ "insightface" ] , cb )
256
275
257
276
async def _install_requirements (
258
277
self , requirements : ModelRequirements , network : QNetworkAccessManager , cb : InternalCB
@@ -519,11 +538,18 @@ def can_install(self):
519
538
520
539
@property
521
540
def upgrade_required (self ):
541
+ backend_mismatch = (
542
+ self ._installed_backend is not None
543
+ and self ._installed_backend != self .backend
544
+ and self .backend in [ServerBackend .cuda , ServerBackend .directml ]
545
+ and self ._installed_backend
546
+ in [ServerBackend .cuda , ServerBackend .directml , ServerBackend .cpu ]
547
+ )
522
548
return (
523
549
self .state is not ServerState .not_installed
524
550
and self .version is not None
525
551
and self .version != "incomplete"
526
- and self .version != resources .version
552
+ and ( self .version != resources .version or backend_mismatch )
527
553
)
528
554
529
555
@@ -563,12 +589,14 @@ async def _extract_archive(name: str, archive: Path, target: Path, cb: InternalC
563
589
zip_file .extractall (target )
564
590
565
591
566
- async def _execute_process (name : str , cmd : list , cwd : Path , cb : InternalCB ):
592
+ async def _execute_process (
593
+ name : str , cmd : list , cwd : Path , cb : InternalCB , env : dict | None = None
594
+ ):
567
595
errlog = ""
568
596
569
597
cmd = [str (c ) for c in cmd ]
570
598
cb (f"Installing { name } " , f"Executing { ' ' .join (cmd )} " )
571
- process = await create_process (cmd [0 ], * cmd [1 :], cwd = cwd , pipe_stderr = True )
599
+ process = await create_process (cmd [0 ], * cmd [1 :], cwd = cwd , additional_env = env , pipe_stderr = True )
572
600
573
601
async def forward (stream : asyncio .StreamReader ):
574
602
async for line in stream :
0 commit comments