From cfc3ace590c1cc6e47180b58222c175b6390afa5 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 15 Jun 2026 23:51:29 +0800 Subject: [PATCH 1/3] Use uv pip for workspace dependency installs --- README.md | 10 ++++ comfy_cli/cmdline.py | 7 +-- comfy_cli/command/install.py | 25 +++++---- comfy_cli/uv.py | 38 +++++++++++-- tests/comfy_cli/command/test_manager_gui.py | 19 ++++--- .../test_cmdline_python_resolution.py | 13 ++--- tests/comfy_cli/test_install.py | 20 +++---- .../test_install_python_resolution.py | 53 +++++++++---------- tests/uv/test_uv.py | 48 ++++++++++++++++- 9 files changed, 155 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 38c05daf..e9af3c27 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,16 @@ dependencies using the following precedence: tool environment): a `.venv` is created inside the ComfyUI workspace. Use `comfy launch` to start ComfyUI with the correct Python. +ComfyUI and ComfyUI-Manager dependency installs use uv's pip-compatible installer +when uv is available, targeting the selected environment explicitly. This means +workspace virtual environments created by uv do not need pip installed inside +them. This avoids `No module named pip` failures when the workspace environment +is otherwise valid but was created by uv without pip. It also keeps the default +`comfy install` and `comfy update` paths aligned with comfy-cli's uv support, +instead of requiring users to switch to a separate manual restore flow. + +If uv is unavailable, comfy-cli falls back to `python -m pip`. + ### Specifying execution path - You can specify the path of ComfyUI where the command will be applied through path indicators as follows: diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index e59c0450..1a5dfec9 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -24,7 +24,7 @@ from comfy_cli.resolve_python import resolve_workspace_python from comfy_cli.standalone import StandalonePython from comfy_cli.update import check_for_updates -from comfy_cli.uv import DependencyCompiler +from comfy_cli.uv import DependencyCompiler, run_pip_install from comfy_cli.workspace_manager import WorkspaceManager, check_comfy_repo logging.setup_logging() @@ -412,10 +412,7 @@ def update( os.chdir(comfy_path) subprocess.run(["git", "pull"], check=True) python = resolve_workspace_python(comfy_path) - subprocess.run( - [python, "-m", "pip", "install", "-r", "requirements.txt"], - check=True, - ) + run_pip_install(executable=python, args=["-r", "requirements.txt"], check=True) try: custom_nodes.command.update_node_id_cache() diff --git a/comfy_cli/command/install.py b/comfy_cli/command/install.py index a6d6ac52..78547b15 100755 --- a/comfy_cli/command/install.py +++ b/comfy_cli/command/install.py @@ -22,7 +22,7 @@ from comfy_cli.cuda_detect import DEFAULT_CUDA_TAG from comfy_cli.git_utils import checkout_pr, git_checkout_tag from comfy_cli.resolve_python import ensure_workspace_python -from comfy_cli.uv import DependencyCompiler +from comfy_cli.uv import DependencyCompiler, run_pip_install from comfy_cli.workspace_manager import WorkspaceManager, check_comfy_repo workspace_manager = WorkspaceManager() @@ -37,8 +37,9 @@ def get_os_details(): def _pip_install_torch(python: str, index_args: list[str]) -> subprocess.CompletedProcess: """Install torch, torchvision, and torchaudio with the given index arguments.""" - return subprocess.run( - [python, "-m", "pip", "install", "torch", "torchvision", "torchaudio"] + index_args, + return run_pip_install( + executable=python, + args=["torch", "torchvision", "torchaudio", *index_args], check=False, ) @@ -85,16 +86,13 @@ def pip_install_comfyui_dependencies( # install directml for AMD windows if gpu == GPU_OPTION.AMD and plat == constants.OS.WINDOWS: - subprocess.run([python, "-m", "pip", "install", "torch-directml"], check=True) + run_pip_install(executable=python, args=["torch-directml"], check=True) # install torch for Mac M Series if gpu == GPU_OPTION.MAC_M_SERIES: - subprocess.run( - [ - python, - "-m", - "pip", - "install", + run_pip_install( + executable=python, + args=[ "--pre", "torch", "torchvision", @@ -108,7 +106,7 @@ def pip_install_comfyui_dependencies( # install requirements.txt if skip_requirement: return - result = subprocess.run([python, "-m", "pip", "install", "-r", "requirements.txt"], check=False) + result = run_pip_install(executable=python, args=["-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) @@ -125,8 +123,9 @@ def pip_install_manager(repo_dir, python=sys.executable): "Skipping manager installation (older ComfyUI version?).[/bold yellow]" ) return False - result = subprocess.run( - [python, "-m", "pip", "install", "-r", constants.MANAGER_REQUIREMENTS_FILE], + result = run_pip_install( + executable=python, + args=["-r", constants.MANAGER_REQUIREMENTS_FILE], cwd=repo_dir, check=False, capture_output=True, diff --git a/comfy_cli/uv.py b/comfy_cli/uv.py index eb31bdc7..30638e78 100644 --- a/comfy_cli/uv.py +++ b/comfy_cli/uv.py @@ -2,6 +2,7 @@ import subprocess import sys from importlib import metadata +from importlib.util import find_spec from pathlib import Path from textwrap import dedent from typing import Any, cast @@ -15,6 +16,38 @@ def _run(cmd: list[str], cwd: PathLike, check: bool = True) -> subprocess.Comple return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, check=check) +def pip_install_command(executable: PathLike, args: list[str]) -> list[str]: + """Build an installer command for a Python environment. + + Prefer uv from the comfy-cli runtime and target the workspace interpreter + explicitly. This avoids requiring pip to be installed inside uv-created + workspace virtual environments. + """ + if find_spec("uv") is not None: + cmd = [sys.executable, "-m", "uv", "pip", "install", "--python", str(executable), *args] + if "--extra-index-url" in args and "--index-strategy" not in args: + cmd.extend(["--index-strategy", "unsafe-best-match"]) + return cmd + return [str(executable), "-m", "pip", "install", *args] + + +def run_pip_install( + executable: PathLike, + args: list[str], + cwd: PathLike | None = None, + check: bool = False, + capture_output: bool = False, + text: bool = False, +) -> subprocess.CompletedProcess[Any]: + return subprocess.run( + pip_install_command(executable, args), + cwd=cwd, + check=check, + capture_output=capture_output, + text=text, + ) + + def _check_call(cmd: list[str], cwd: PathLike | None = None): """uses check_call to run pip, as reccomended by the pip maintainers. see https://pip.pypa.io/en/stable/user_guide/#using-pip-from-your-program""" @@ -126,9 +159,8 @@ def Find_Req_Files(*ders: PathLike) -> list[Path]: @staticmethod def Install_Build_Deps(executable: PathLike = sys.executable): - """Use pip to install bare minimum requirements for uv to do its thing""" - cmd = [str(executable), "-m", "pip", "install", "--upgrade", "pip", "uv"] - _check_call(cmd=cmd) + """Install uv into the target environment before running uv as a module there.""" + run_pip_install(executable=executable, args=["--upgrade", "pip", "uv"], check=True) @staticmethod def Compile( diff --git a/tests/comfy_cli/command/test_manager_gui.py b/tests/comfy_cli/command/test_manager_gui.py index b515f066..fd700ec5 100644 --- a/tests/comfy_cli/command/test_manager_gui.py +++ b/tests/comfy_cli/command/test_manager_gui.py @@ -776,14 +776,14 @@ class TestPipInstallManagerCacheClear: """Tests for pip_install_manager cache clearing after successful install.""" @patch("comfy_cli.command.custom_nodes.cm_cli_util.find_cm_cli") - @patch("comfy_cli.command.install.subprocess.run") + @patch("comfy_cli.command.install.run_pip_install") @patch("os.path.exists", return_value=True) - def test_pip_install_manager_clears_cache_on_success(self, mock_exists, mock_run, mock_find_cm_cli): + def test_pip_install_manager_clears_cache_on_success(self, mock_exists, mock_install, mock_find_cm_cli): """When pip install succeeds, find_cm_cli cache should be cleared.""" from comfy_cli.command.install import pip_install_manager # Simulate successful pip install - mock_run.return_value = MagicMock(returncode=0, stderr="") + mock_install.return_value = MagicMock(returncode=0, stderr="") # Call pip_install_manager result = pip_install_manager("/fake/repo") @@ -793,14 +793,14 @@ def test_pip_install_manager_clears_cache_on_success(self, mock_exists, mock_run # Verify cache_clear was called on the mock mock_find_cm_cli.cache_clear.assert_called_once() - @patch("comfy_cli.command.install.subprocess.run") + @patch("comfy_cli.command.install.run_pip_install") @patch("os.path.exists", return_value=True) - def test_pip_install_manager_no_cache_clear_on_failure(self, mock_exists, mock_run): + def test_pip_install_manager_no_cache_clear_on_failure(self, mock_exists, mock_install): """When pip install fails, cache should not be affected (function returns early).""" from comfy_cli.command.install import pip_install_manager # Simulate failed pip install - mock_run.return_value = MagicMock(returncode=1) + mock_install.return_value = MagicMock(returncode=1) # Call pip_install_manager result = pip_install_manager("/fake/repo") @@ -1119,17 +1119,16 @@ def test_find_cm_cli_cache_behavior(self): class TestPipInstallManagerEdgeCases: """Additional edge case tests for pip_install_manager().""" - @patch("comfy_cli.command.install.subprocess.run") + @patch("comfy_cli.command.install.run_pip_install") @patch("os.path.exists", return_value=False) - def test_pip_install_manager_requirements_not_found(self, mock_exists, mock_run): + def test_pip_install_manager_requirements_not_found(self, mock_exists, mock_install): """When requirements file doesn't exist, should return False without calling pip.""" from comfy_cli.command.install import pip_install_manager result = pip_install_manager("/fake/repo") assert result is False - # subprocess.run should NOT be called - mock_run.assert_not_called() + mock_install.assert_not_called() class TestValidateComfyuiManager: diff --git a/tests/comfy_cli/test_cmdline_python_resolution.py b/tests/comfy_cli/test_cmdline_python_resolution.py index e4552376..115132fd 100644 --- a/tests/comfy_cli/test_cmdline_python_resolution.py +++ b/tests/comfy_cli/test_cmdline_python_resolution.py @@ -10,20 +10,14 @@ def test_uses_resolved_python(self, tmp_path): patch.object(cmdline.workspace_manager, "workspace_path", str(tmp_path)), patch("comfy_cli.cmdline.os.chdir"), patch("comfy_cli.cmdline.subprocess.run") as mock_run, + patch("comfy_cli.cmdline.run_pip_install") as mock_install, patch("comfy_cli.cmdline.custom_nodes.command.update_node_id_cache"), ): cmdline.update(target="comfy") mock_resolve.assert_called_once_with(str(tmp_path)) - pip_call = None - for c in mock_run.call_args_list: - cmd = c[0][0] - if "-m" in cmd and "pip" in cmd: - pip_call = cmd - break - - assert pip_call is not None, "pip install call not found" - assert pip_call[0] == "/resolved/python" + mock_run.assert_called_once_with(["git", "pull"], check=True) + mock_install.assert_called_once_with(executable="/resolved/python", args=["-r", "requirements.txt"], check=True) def test_update_comfy_succeeds_when_cm_cli_missing(self, tmp_path): """Regression test for #403: comfy update must not crash when cm-cli is absent.""" @@ -32,6 +26,7 @@ def test_update_comfy_succeeds_when_cm_cli_missing(self, tmp_path): patch.object(cmdline.workspace_manager, "workspace_path", str(tmp_path)), patch("comfy_cli.cmdline.os.chdir"), patch("comfy_cli.cmdline.subprocess.run"), + patch("comfy_cli.cmdline.run_pip_install"), patch( "comfy_cli.cmdline.custom_nodes.command.update_node_id_cache", side_effect=FileNotFoundError("cm-cli not found"), diff --git a/tests/comfy_cli/test_install.py b/tests/comfy_cli/test_install.py index 2aee5d1c..dbe50741 100644 --- a/tests/comfy_cli/test_install.py +++ b/tests/comfy_cli/test_install.py @@ -33,30 +33,30 @@ def test_validate_version_empty(): class TestPipInstallManager: @patch("comfy_cli.command.custom_nodes.cm_cli_util.find_cm_cli") - @patch("comfy_cli.command.install.subprocess.run") + @patch("comfy_cli.command.install.run_pip_install") @patch("os.path.exists", return_value=True) - def test_success(self, mock_exists, mock_run, mock_find): - mock_run.return_value = MagicMock(returncode=0) + def test_success(self, mock_exists, mock_install, mock_find): + mock_install.return_value = MagicMock(returncode=0) result = pip_install_manager("/fake/repo") assert result is True - mock_run.assert_called_once() + mock_install.assert_called_once() @patch("os.path.exists", return_value=False) def test_missing_requirements_file(self, mock_exists): result = pip_install_manager("/fake/repo") assert result is False - @patch("comfy_cli.command.install.subprocess.run") + @patch("comfy_cli.command.install.run_pip_install") @patch("os.path.exists", return_value=True) - def test_pip_failure(self, mock_exists, mock_run): - mock_run.return_value = MagicMock(returncode=1, stderr="some error") + def test_pip_failure(self, mock_exists, mock_install): + mock_install.return_value = MagicMock(returncode=1, stderr="some error") result = pip_install_manager("/fake/repo") assert result is False - @patch("comfy_cli.command.install.subprocess.run") + @patch("comfy_cli.command.install.run_pip_install") @patch("os.path.exists", return_value=True) - def test_pip_failure_no_stderr(self, mock_exists, mock_run): - mock_run.return_value = MagicMock(returncode=1, stderr="") + def test_pip_failure_no_stderr(self, mock_exists, mock_install): + mock_install.return_value = MagicMock(returncode=1, stderr="") result = pip_install_manager("/fake/repo") assert result is False diff --git a/tests/comfy_cli/test_install_python_resolution.py b/tests/comfy_cli/test_install_python_resolution.py index 17ff540c..e0887d4b 100644 --- a/tests/comfy_cli/test_install_python_resolution.py +++ b/tests/comfy_cli/test_install_python_resolution.py @@ -13,7 +13,7 @@ def test_uses_python_param_cpu(self, tmp_path): repo_dir = str(tmp_path) (tmp_path / "requirements.txt").write_text("some-package\n") - with patch("comfy_cli.command.install.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run: + with patch("comfy_cli.command.install.run_pip_install", return_value=MagicMock(returncode=0)) as mock_install: install.pip_install_comfyui_dependencies( repo_dir, gpu=None, @@ -24,10 +24,10 @@ def test_uses_python_param_cpu(self, tmp_path): python="/resolved/python", ) - for c in mock_run.call_args_list: - cmd = c[0][0] - assert cmd[0] == "/resolved/python", f"Expected /resolved/python but got {cmd[0]} in {cmd}" - assert cmd[0] != sys.executable + for c in mock_install.call_args_list: + executable = c.kwargs["executable"] + assert executable == "/resolved/python" + assert executable != sys.executable class TestPipInstallManager: @@ -35,14 +35,13 @@ def test_uses_python_param(self, tmp_path): (tmp_path / "manager_requirements.txt").write_text("comfyui-manager\n") with ( - patch("comfy_cli.command.install.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run, + patch("comfy_cli.command.install.run_pip_install", return_value=MagicMock(returncode=0)) as mock_install, patch("comfy_cli.command.custom_nodes.cm_cli_util.find_cm_cli") as mock_find, ): mock_find.cache_clear = MagicMock() install.pip_install_manager(str(tmp_path), python="/resolved/python") - cmd = mock_run.call_args[0][0] - assert cmd[0] == "/resolved/python" + assert mock_install.call_args.kwargs["executable"] == "/resolved/python" class TestExecute: @@ -196,7 +195,7 @@ def test_auto_detected_cuda_tag_used(self, tmp_path): repo_dir = str(tmp_path) (tmp_path / "requirements.txt").write_text("some-package\n") - with patch("comfy_cli.command.install.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run: + with patch("comfy_cli.command.install.run_pip_install", return_value=MagicMock(returncode=0)) as mock_install: install.pip_install_comfyui_dependencies( repo_dir, gpu=GPU_OPTION.NVIDIA, @@ -208,14 +207,14 @@ def test_auto_detected_cuda_tag_used(self, tmp_path): cuda_tag="cu130", ) - cmd = _get_torch_install_cmd(mock_run.call_args_list) + cmd = _get_torch_install_args(mock_install.call_args_list) assert "https://download.pytorch.org/whl/cu130" in cmd def test_auto_detect_failure_falls_back(self, tmp_path): repo_dir = str(tmp_path) (tmp_path / "requirements.txt").write_text("some-package\n") - with patch("comfy_cli.command.install.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run: + with patch("comfy_cli.command.install.run_pip_install", return_value=MagicMock(returncode=0)) as mock_install: install.pip_install_comfyui_dependencies( repo_dir, gpu=GPU_OPTION.NVIDIA, @@ -227,14 +226,14 @@ def test_auto_detect_failure_falls_back(self, tmp_path): cuda_tag=None, ) - cmd = _get_torch_install_cmd(mock_run.call_args_list) + cmd = _get_torch_install_args(mock_install.call_args_list) assert "https://download.pytorch.org/whl/cu126" in cmd def test_explicit_cuda_version_used_when_no_tag(self, tmp_path): repo_dir = str(tmp_path) (tmp_path / "requirements.txt").write_text("some-package\n") - with patch("comfy_cli.command.install.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run: + with patch("comfy_cli.command.install.run_pip_install", return_value=MagicMock(returncode=0)) as mock_install: install.pip_install_comfyui_dependencies( repo_dir, gpu=GPU_OPTION.NVIDIA, @@ -246,14 +245,14 @@ def test_explicit_cuda_version_used_when_no_tag(self, tmp_path): cuda_tag=None, ) - cmd = _get_torch_install_cmd(mock_run.call_args_list) + cmd = _get_torch_install_args(mock_install.call_args_list) assert "https://download.pytorch.org/whl/cu118" in cmd def test_cuda_tag_takes_precedence_over_enum(self, tmp_path): repo_dir = str(tmp_path) (tmp_path / "requirements.txt").write_text("some-package\n") - with patch("comfy_cli.command.install.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run: + with patch("comfy_cli.command.install.run_pip_install", return_value=MagicMock(returncode=0)) as mock_install: install.pip_install_comfyui_dependencies( repo_dir, gpu=GPU_OPTION.NVIDIA, @@ -265,16 +264,16 @@ def test_cuda_tag_takes_precedence_over_enum(self, tmp_path): cuda_tag="cu130", ) - cmd = _get_torch_install_cmd(mock_run.call_args_list) + cmd = _get_torch_install_args(mock_install.call_args_list) assert "https://download.pytorch.org/whl/cu130" in cmd -def _get_torch_install_cmd(calls): - """Find the subprocess.run call that installs torch packages.""" +def _get_torch_install_args(calls): + """Find the installer call that installs torch packages.""" for c in calls: - cmd = c[0][0] - if "torch" in cmd and "requirements.txt" not in cmd: - return cmd + args = c.kwargs["args"] + if "torch" in args and "requirements.txt" not in args: + return args return None @@ -293,7 +292,7 @@ def test_amd_uses_index_url_with_rocm_version(self, tmp_path, rocm_version, expe repo_dir = str(tmp_path) (tmp_path / "requirements.txt").write_text("some-package\n") - with patch("comfy_cli.command.install.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run: + with patch("comfy_cli.command.install.run_pip_install", return_value=MagicMock(returncode=0)) as mock_install: install.pip_install_comfyui_dependencies( repo_dir, gpu=GPU_OPTION.AMD, @@ -305,7 +304,7 @@ def test_amd_uses_index_url_with_rocm_version(self, tmp_path, rocm_version, expe rocm_version=rocm_version, ) - cmd = _get_torch_install_cmd(mock_run.call_args_list) + cmd = _get_torch_install_args(mock_install.call_args_list) assert "--index-url" in cmd assert "--extra-index-url" not in cmd assert expected_url in cmd @@ -324,7 +323,7 @@ def test_nvidia_uses_index_url_with_cuda_version(self, tmp_path, cuda_version, e repo_dir = str(tmp_path) (tmp_path / "requirements.txt").write_text("some-package\n") - with patch("comfy_cli.command.install.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run: + with patch("comfy_cli.command.install.run_pip_install", return_value=MagicMock(returncode=0)) as mock_install: install.pip_install_comfyui_dependencies( repo_dir, gpu=GPU_OPTION.NVIDIA, @@ -335,7 +334,7 @@ def test_nvidia_uses_index_url_with_cuda_version(self, tmp_path, cuda_version, e python="/usr/bin/python", ) - cmd = _get_torch_install_cmd(mock_run.call_args_list) + cmd = _get_torch_install_args(mock_install.call_args_list) assert "--index-url" in cmd assert "--extra-index-url" not in cmd assert expected_url in cmd @@ -344,7 +343,7 @@ def test_nvidia_linux_uses_index_url(self, tmp_path): repo_dir = str(tmp_path) (tmp_path / "requirements.txt").write_text("some-package\n") - with patch("comfy_cli.command.install.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run: + with patch("comfy_cli.command.install.run_pip_install", return_value=MagicMock(returncode=0)) as mock_install: install.pip_install_comfyui_dependencies( repo_dir, gpu=GPU_OPTION.NVIDIA, @@ -355,6 +354,6 @@ def test_nvidia_linux_uses_index_url(self, tmp_path): python="/usr/bin/python", ) - cmd = _get_torch_install_cmd(mock_run.call_args_list) + cmd = _get_torch_install_args(mock_install.call_args_list) assert "--index-url" in cmd assert "https://download.pytorch.org/whl/cu126" in cmd diff --git a/tests/uv/test_uv.py b/tests/uv/test_uv.py index 8fa74aa8..80cbf777 100644 --- a/tests/uv/test_uv.py +++ b/tests/uv/test_uv.py @@ -1,5 +1,6 @@ import shutil import subprocess +import sys from pathlib import Path from unittest.mock import patch @@ -7,7 +8,7 @@ from comfy_cli import ui from comfy_cli.constants import GPU_OPTION -from comfy_cli.uv import DependencyCompiler, _check_call, parse_req_file +from comfy_cli.uv import DependencyCompiler, _check_call, parse_req_file, pip_install_command hereDir = Path(__file__).parent.resolve() mockComfyDir = hereDir / "mock_comfy" @@ -120,6 +121,51 @@ def test_compile_passes_torch_backend(): assert cmd[idx + 1] == "cu126" +def test_pip_install_command_prefers_uv_with_target_python(): + with patch("comfy_cli.uv.find_spec", return_value=object()): + cmd = pip_install_command("/workspace/.venv/bin/python", ["-r", "requirements.txt"]) + + assert cmd == [ + sys.executable, + "-m", + "uv", + "pip", + "install", + "--python", + "/workspace/.venv/bin/python", + "-r", + "requirements.txt", + ] + + +def test_pip_install_command_falls_back_to_pip_when_uv_missing(): + with patch("comfy_cli.uv.find_spec", return_value=None): + cmd = pip_install_command("/workspace/.venv/bin/python", ["-r", "requirements.txt"]) + + assert cmd == ["/workspace/.venv/bin/python", "-m", "pip", "install", "-r", "requirements.txt"] + + +def test_pip_install_command_matches_pip_multi_index_behavior(): + with patch("comfy_cli.uv.find_spec", return_value=object()): + cmd = pip_install_command( + "/workspace/.venv/bin/python", + ["torch", "--extra-index-url", "https://download.pytorch.org/whl/cpu"], + ) + + assert cmd[-2:] == ["--index-strategy", "unsafe-best-match"] + + +def test_install_build_deps_uses_uv_installer_without_requiring_pip_in_target(): + with patch("comfy_cli.uv.run_pip_install") as mock_install: + DependencyCompiler.Install_Build_Deps(executable="/workspace/.venv/bin/python") + + mock_install.assert_called_once_with( + executable="/workspace/.venv/bin/python", + args=["--upgrade", "pip", "uv"], + check=True, + ) + + def test_compile_omits_torch_backend_when_none(): """Verify that Compile() does not include --torch-backend when torch_backend is None.""" with patch("comfy_cli.uv._run") as mock_run: From bf36794cdafa56e28ac0d450382a6eb309f90984 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 15 Jun 2026 23:54:19 +0800 Subject: [PATCH 2/3] Document uv install benefits --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e9af3c27..0148eb57 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,17 @@ dependencies using the following precedence: Use `comfy launch` to start ComfyUI with the correct Python. ComfyUI and ComfyUI-Manager dependency installs use uv's pip-compatible installer -when uv is available, targeting the selected environment explicitly. This means -workspace virtual environments created by uv do not need pip installed inside -them. This avoids `No module named pip` failures when the workspace environment -is otherwise valid but was created by uv without pip. It also keeps the default -`comfy install` and `comfy update` paths aligned with comfy-cli's uv support, -instead of requiring users to switch to a separate manual restore flow. +when uv is available, targeting the selected environment explicitly. This keeps +the default `comfy install` and `comfy update` paths aligned with comfy-cli's uv +support: + +- uv-created workspace virtual environments do not need pip installed inside + them, avoiding `No module named pip` failures when the environment is otherwise + valid. +- Warm-cache restores and updates can reuse uv's resolver/cache/linking behavior, + so repeated installs often complete much faster. +- Users do not need to switch to a separate manual restore flow just to keep a + ComfyUI workspace uv-managed. If uv is unavailable, comfy-cli falls back to `python -m pip`. From 8de0323c6662c6d2bc7e2e30867aec908f2983a1 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 16 Jun 2026 00:15:10 +0800 Subject: [PATCH 3/3] Document uv first install flow --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 0148eb57..85e55382 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,35 @@ support: - Users do not need to switch to a separate manual restore flow just to keep a ComfyUI workspace uv-managed. +The difference is most visible when comparing the documented flow with a +uv-managed workspace: + +| Step | Existing documented flow | uv-managed workspace flow | +| --- | --- | --- | +| Install comfy-cli | `pip install comfy-cli` | `uv tool install comfy-cli` or `uv run comfy ...` | +| Install ComfyUI | `comfy install` | `uv run comfy install --restore` from an existing ComfyUI workspace, or `uv run comfy --workspace install` | +| Update ComfyUI | `comfy update` | `comfy update` from a workspace whose `.venv` was created by uv | +| Dependency install behavior | Historically called ` -m pip install ...` | Uses ` -m uv pip install --python ...` | +| Failure this avoids | A valid workspace environment can still fail with `No module named pip` if pip was not seeded into `.venv` | uv targets the workspace interpreter directly, so pip does not need to be importable inside the workspace | +| Practical result | Users may need to manually repair the environment before install/update can continue | Install, restore, Manager setup, and repeated updates can stay uv-managed and reuse uv's cache | + +For a fresh machine or another SSH host, the uv-managed flow does not require +downloading the comfy-cli repository first: + +| Goal | pip-first instructions | uv-first instructions | +| --- | --- | --- | +| Install the CLI tool | `pip install comfy-cli` | `uv tool install comfy-cli` | +| Check the CLI | `comfy --help` | `comfy --help` | +| Install ComfyUI into a specific path | `comfy --workspace install` | `comfy --workspace install` | +| Restore an existing checkout | `comfy --workspace install --restore` | `comfy --workspace install --restore` | +| Update an existing checkout | `comfy --workspace update` | `comfy --workspace update` | +| Dependency installer used by comfy-cli | The selected workspace Python may need `pip` importable | comfy-cli uses uv's installer and passes `--python ` | + +In both flows, users run the same `comfy` commands after installing the CLI. +The uv-backed installer changes the internal dependency restore step so fresh +machines and uv-managed workspaces do not depend on pip being present inside the +ComfyUI workspace environment. + If uv is unavailable, comfy-cli falls back to `python -m pip`. ### Specifying execution path