diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index 247ba95a..b7421005 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -375,6 +375,7 @@ def update( raise typer.Exit(code=1) comfy_path = workspace_manager.workspace_path + python_exe = workspace_manager.python_exe if "all" == target: custom_nodes.command.execute_cm_cli(["update", "all"]) @@ -386,7 +387,7 @@ def update( os.chdir(comfy_path) subprocess.run(["git", "pull"], check=True) subprocess.run( - [sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], + [python_exe, "-m", "pip", "install", "-r", "requirements.txt"], check=True, ) diff --git a/comfy_cli/command/custom_nodes/cm_cli_util.py b/comfy_cli/command/custom_nodes/cm_cli_util.py index d74a1078..6667cf29 100644 --- a/comfy_cli/command/custom_nodes/cm_cli_util.py +++ b/comfy_cli/command/custom_nodes/cm_cli_util.py @@ -25,6 +25,7 @@ def execute_cm_cli(args, channel=None, fast_deps=False, mode=None) -> str | None _config_manager = ConfigManager() workspace_path = workspace_manager.workspace_path + python_exe = workspace_manager.python_exe if not workspace_path: print("\n[bold red]ComfyUI path is not resolved.[/bold red]\n", file=sys.stderr) @@ -38,7 +39,7 @@ def execute_cm_cli(args, channel=None, fast_deps=False, mode=None) -> str | None ) raise typer.Exit(code=1) - cmd = [sys.executable, cm_cli_path] + args + cmd = [python_exe, cm_cli_path] + args if channel is not None: cmd += ["--channel", channel] diff --git a/comfy_cli/command/custom_nodes/command.py b/comfy_cli/command/custom_nodes/command.py index d826c6c3..cac1aabe 100644 --- a/comfy_cli/command/custom_nodes/command.py +++ b/comfy_cli/command/custom_nodes/command.py @@ -70,10 +70,11 @@ def run_script(cmd, cwd="."): def get_installed_packages(): global pip_map + python_exe = workspace_manager.python_exe if pip_map is None: try: - result = subprocess.check_output([sys.executable, "-m", "pip", "list"], universal_newlines=True) + result = subprocess.check_output([python_exe, "-m", "pip", "list"], universal_newlines=True) pip_map = {} for line in result.split("\n"): @@ -138,6 +139,7 @@ def try_install_script(repo_path, install_cmd, instant_execution=False): def execute_install_script(repo_path): + python_exe = workspace_manager.python_exe install_script_path = os.path.join(repo_path, "install.py") requirements_path = os.path.join(repo_path, "requirements.txt") @@ -157,13 +159,13 @@ def execute_install_script(repo_path): # package_name = remap_pip_package(line.strip()) package_name = line.strip() if package_name and not package_name.startswith("#"): - install_cmd = [sys.executable, "-m", "pip", "install", package_name] + install_cmd = [python_exe, "-m", "pip", "install", package_name] if package_name.strip() != "": try_install_script(repo_path, install_cmd) if os.path.exists(install_script_path): print("Install: install script") - install_cmd = [sys.executable, "install.py"] + install_cmd = [python_exe, "install.py"] try_install_script(repo_path, install_cmd) @@ -481,6 +483,7 @@ def uninstall( def update_node_id_cache(): config_manager = ConfigManager() workspace_path = workspace_manager.workspace_path + python_exe = workspace_manager.python_exe cm_cli_path = os.path.join(workspace_path, "custom_nodes", "ComfyUI-Manager", "cm-cli.py") @@ -489,7 +492,7 @@ def update_node_id_cache(): os.makedirs(tmp_path) cache_path = os.path.join(tmp_path, "node-cache.list") - cmd = [sys.executable, cm_cli_path, "export-custom-node-ids", cache_path] + cmd = [python_exe, cm_cli_path, "export-custom-node-ids", cache_path] new_env = os.environ.copy() new_env["COMFYUI_PATH"] = workspace_path diff --git a/comfy_cli/command/install.py b/comfy_cli/command/install.py index abb26229..f0179561 100755 --- a/comfy_cli/command/install.py +++ b/comfy_cli/command/install.py @@ -37,6 +37,7 @@ def pip_install_comfyui_dependencies( skip_requirement: bool, ): os.chdir(repo_dir) + python_exe = workspace_manager.python_exe result = None if not skip_torch_or_directml: @@ -45,7 +46,7 @@ def pip_install_comfyui_dependencies( pip_url = ["--extra-index-url", "https://download.pytorch.org/whl/rocm6.0"] result = subprocess.run( [ - sys.executable, + python_exe, "-m", "pip", "install", @@ -60,7 +61,7 @@ def pip_install_comfyui_dependencies( # install torch for NVIDIA if gpu == GPU_OPTION.NVIDIA: base_command = [ - sys.executable, + python_exe, "-m", "pip", "install", @@ -102,12 +103,12 @@ def pip_install_comfyui_dependencies( utils.install_conda_package("libuv") # TODO: wrap pip install in a function subprocess.run( - [sys.executable, "-m", "pip", "install", "mkl", "mkl-dpcpp"], + [python_exe, "-m", "pip", "install", "mkl", "mkl-dpcpp"], check=True, ) result = subprocess.run( [ - sys.executable, + python_exe, "-m", "pip", "install", @@ -125,13 +126,13 @@ def pip_install_comfyui_dependencies( # install directml for AMD windows if gpu == GPU_OPTION.AMD and plat == constants.OS.WINDOWS: - result = subprocess.run([sys.executable, "-m", "pip", "install", "torch-directml"], check=True) + result = subprocess.run([python_exe, "-m", "pip", "install", "torch-directml"], check=True) # install torch for Mac M Series if gpu == GPU_OPTION.MAC_M_SERIES: result = subprocess.run( [ - sys.executable, + python_exe, "-m", "pip", "install", @@ -148,7 +149,7 @@ def pip_install_comfyui_dependencies( # install requirements.txt if skip_requirement: return - result = subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], check=False) + result = subprocess.run([python_exe, "-m", "pip", "install", "-r", "requirements.txt"], check=False) if result.returncode != 0: rprint("Failed to install ComfyUI dependencies. Please check your environment (`comfy env`) and try again.") sys.exit(1) @@ -156,8 +157,10 @@ def pip_install_comfyui_dependencies( # install requirements for manager def pip_install_manager_dependencies(repo_dir): + python_exe = workspace_manager.python_exe + os.chdir(os.path.join(repo_dir, "custom_nodes", "ComfyUI-Manager")) - subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], check=True) + subprocess.run([python_exe, "-m", "pip", "install", "-r", "requirements.txt"], check=True) def execute( diff --git a/comfy_cli/command/launch.py b/comfy_cli/command/launch.py index 52885e66..1c940019 100644 --- a/comfy_cli/command/launch.py +++ b/comfy_cli/command/launch.py @@ -34,6 +34,8 @@ def launch_comfyui(extra): # To minimize the possibility of leaving residue in the tmp directory, use files instead of directories. reboot_path = os.path.join(session_path + ".reboot") + python_exe = workspace_manager.python_exe + extra = extra if extra is not None else [] process = None @@ -41,7 +43,7 @@ def launch_comfyui(extra): if "COMFY_CLI_BACKGROUND" not in os.environ: # If not running in background mode, there's no need to use popen. This can prevent the issue of linefeeds occurring with tqdm. while True: - res = subprocess.run([sys.executable, "main.py"] + extra, env=new_env, check=False) + res = subprocess.run([python_exe, "main.py"] + extra, env=new_env, check=False) if reboot_path is None: print("[bold red]ComfyUI is not installed.[/bold red]\n") @@ -70,7 +72,7 @@ def redirector_stdout(): while True: if sys.platform == "win32": process = subprocess.Popen( - [sys.executable, "main.py"] + extra, + [python_exe, "main.py"] + extra, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, @@ -81,7 +83,7 @@ def redirector_stdout(): ) else: process = subprocess.Popen( - [sys.executable, "main.py"] + extra, + [python_exe, "main.py"] + extra, text=True, env=new_env, encoding="utf-8", diff --git a/comfy_cli/workspace_manager.py b/comfy_cli/workspace_manager.py index fc5f451a..764915a6 100644 --- a/comfy_cli/workspace_manager.py +++ b/comfy_cli/workspace_manager.py @@ -1,5 +1,7 @@ import concurrent.futures import os +import sys +import subprocess from dataclasses import dataclass, field from datetime import datetime from enum import Enum @@ -151,6 +153,7 @@ def __init__( self.use_recent = None self.workspace_path = None self.workspace_type = None + self.python_exe = None self.skip_prompting = None def setup_workspace_manager( @@ -164,6 +167,7 @@ def setup_workspace_manager( self.use_here = use_here self.use_recent = use_recent self.workspace_path, self.workspace_type = self.get_workspace_path() + self.python_exe = self.find_or_create_python_env() self.skip_prompting = skip_prompting def set_recent_workspace(self, path: str): @@ -316,3 +320,50 @@ def fill_print_table(self, table): "Current selected workspace", f"[bold green]→ {self.workspace_path}[/bold green]", ) + + def find_or_create_python_env(self, default_name=".venv"): + """ + Locates the Conda/virtual environment to use, else creates `.venv`. + """ + # Case 1: Find if .venv or venv is present in install directory. + for name in [default_name, "venv"]: + if utils.get_os() == constants.OS.WINDOWS: + venv_path = os.path.join(self.workspace_path, name, "Scripts", "python.exe") + else: + venv_path = os.path.join(self.workspace_path, name, "bin", "python") + if os.path.exists(venv_path): + return venv_path + + # Case 2: If CONDA_PREFIX is present, use the conda environment. + conda_prefix = os.environ.get("CONDA_PREFIX") + if conda_prefix: + if utils.get_os() == constants.OS.WINDOWS: + return os.path.join(conda_prefix, "python.exe") + else: + return os.path.join(conda_prefix, "bin", "python") + + + # Case 3: Create standalone venv using currently active Python executable. + try: + print("[bold yellow]Virtual environment not found. Creating...[/bold yellow]") + # TODO: Should probably check here if Python version is compatible. + subprocess.check_call( + [ + sys.executable, + "-m", + "venv", + "--copies", + "--upgrade-deps", + "--prompt", + "comfyui", + os.path.join(self.workspace_path, default_name), + ] + ) + if utils.get_os() == constants.OS.WINDOWS: + return os.path.join(self.workspace_path, default_name, "Scripts", "python.exe") + else: + return os.path.join(self.workspace_path, default_name, "bin", "python") + except subprocess.CalledProcessError as e: + print(f"[bold red]Failed to create virtual environment: {e}[/bold red]") + raise typer.Exit(code=1) +