From 7ce59eb89ad252ed8db4a9206cbbc66e943936d3 Mon Sep 17 00:00:00 2001 From: Max Klein Date: Fri, 23 Aug 2024 13:47:22 -0400 Subject: [PATCH] Add builder for standalone python for comfy (#158) * remove unneeded pylint config cruft * added utils to grab pre-built distros from the python-build-standalone project * added `StandalonePython` class * added type hinting to `standalone.py` and related * allow specification of python executable in `DependencyCompiler` * added `install_*` methods to `StandalonePython` * replaced `tqdm.wrapattr` with `rich.progress.wrap_file` as per review comment * `StandalonePython`: build precache based on existing comfy install * added `standalone` cli command * fix `test_compile` unittest * linted and formatted --- .pylintrc | 28 ------- comfy_cli/cmdline.py | 107 +++++++++++++++++++++++++ comfy_cli/constants.py | 20 +++-- comfy_cli/standalone.py | 168 ++++++++++++++++++++++++++++++++++++++ comfy_cli/typing.py | 4 + comfy_cli/utils.py | 50 ++++++++++-- comfy_cli/uv.py | 173 +++++++++++++++++++++++++++++++--------- pyproject.toml | 1 + tests/uv/test_uv.py | 1 + 9 files changed, 472 insertions(+), 80 deletions(-) delete mode 100644 .pylintrc create mode 100644 comfy_cli/standalone.py create mode 100644 comfy_cli/typing.py diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 873ec59..0000000 --- a/.pylintrc +++ /dev/null @@ -1,28 +0,0 @@ -; [MASTER] -; disable= -; C0114, # missing-module-docstring -; C0115, # missing-class-docstring -; C0116, # missing-function-docstring -; C0209, # consider-using-f-string -; W0622, # redefined-builtin -; W1113, # too-many-arguments -; W0613, # unused-argument -; W0718, # broad-exception-caught -; W0511, # fixme -; W0621, # redefined-outer-name -; W1514, # unspecified-encoding -; W0603, # global-statement -; W1203, # logging-fstring-interpolation -; W0212, # protected-access -; C0301, # line-too-long -; W0707, # raise-missing-from - -; # TODO -; W3101, # missing timeout on request -; W0719, # broad-exception-raised - -; [FORMAT] -; max-line-length=120 - -; [SIMILARITIES] -; ignore-imports=yes diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index 8f98e78..45e8169 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -19,6 +19,7 @@ from comfy_cli.config_manager import ConfigManager from comfy_cli.constants import GPU_OPTION, CUDAVersion from comfy_cli.env_checker import EnvChecker +from comfy_cli.standalone import StandalonePython from comfy_cli.update import check_for_updates from comfy_cli.workspace_manager import WorkspaceManager, check_comfy_repo @@ -543,6 +544,112 @@ def feedback(): print("Thank you for your feedback!") +@app.command(help="Download a standalone Python interpreter and dependencies based on an existing comfyui workspace") +@tracking.track_command() +def standalone( + platform: Annotated[ + Optional[constants.OS], + typer.Option( + show_default=False, + help="Create standalone Python for specified platform", + ), + ] = None, + proc: Annotated[ + Optional[constants.PROC], + typer.Option( + show_default=False, + help="Create standalone Python for specified processor", + ), + ] = None, + nvidia: Annotated[ + Optional[bool], + typer.Option( + show_default=False, + help="Create standalone Python for Nvidia gpu", + callback=g_gpu_exclusivity.validate, + ), + ] = None, + cuda_version: Annotated[CUDAVersion, typer.Option(show_default=True)] = CUDAVersion.v12_1, + amd: Annotated[ + Optional[bool], + typer.Option( + show_default=False, + help="Create standalone Python for AMD gpu", + callback=g_gpu_exclusivity.validate, + ), + ] = None, + m_series: Annotated[ + Optional[bool], + typer.Option( + show_default=False, + help="Create standalone Python for Mac M-Series gpu", + callback=g_gpu_exclusivity.validate, + ), + ] = None, + intel_arc: Annotated[ + Optional[bool], + typer.Option( + hidden=True, + show_default=False, + help="(Beta support) Create standalone Python for Intel Arc gpu, based on https://github.com/comfyanonymous/ComfyUI/pull/3439", + callback=g_gpu_exclusivity.validate, + ), + ] = None, + cpu: Annotated[ + Optional[bool], + typer.Option( + show_default=False, + help="Create standalone Python for CPU", + callback=g_gpu_exclusivity.validate, + ), + ] = None, +): + comfy_path, _ = workspace_manager.get_workspace_path() + + platform = utils.get_os() if platform is None else platform + proc = utils.get_proc() if proc is None else proc + + if cpu: + gpu = GPU_OPTION.CPU + elif nvidia: + gpu = GPU_OPTION.NVIDIA + elif amd: + gpu = GPU_OPTION.AMD + elif m_series: + gpu = GPU_OPTION.M_SERIES + elif intel_arc: + gpu = GPU_OPTION.INTEL_ARC + else: + if platform == constants.OS.MACOS: + gpu = ui.prompt_select_enum( + "What type of Mac do you have?", + [GPU_OPTION.M_SERIES, GPU_OPTION.MAC_INTEL], + ) + else: + gpu = ui.prompt_select_enum( + "What GPU do you have?", + [GPU_OPTION.NVIDIA, GPU_OPTION.AMD, GPU_OPTION.INTEL_ARC, GPU_OPTION.CPU], + ) + + if gpu == GPU_OPTION.INTEL_ARC: + print("[bold yellow]Installing on Intel ARC is not yet completely supported[/bold yellow]") + env_check = env_checker.EnvChecker() + if env_check.conda_env is None: + print("[bold red]Intel ARC support requires conda environment to be activated.[/bold red]") + raise typer.Exit(code=1) + if intel_arc is None: + confirm_result = ui.prompt_confirm_action( + "Are you sure you want to try beta install feature on Intel ARC?", True + ) + if not confirm_result: + raise typer.Exit(code=0) + print("[bold yellow]Installing on Intel ARC is in beta stage.[/bold yellow]") + + sty = StandalonePython.FromDistro(platform=platform, proc=proc) + sty.precache_comfy_deps(comfyDir=comfy_path, gpu=gpu) + sty.to_tarball() + + app.add_typer(models_command.app, name="model", help="Manage models.") app.add_typer(custom_nodes.app, name="node", help="Manage custom nodes.") app.add_typer(custom_nodes.manager_app, name="manager", help="Manage ComfyUI-Manager.") diff --git a/comfy_cli/constants.py b/comfy_cli/constants.py index 73f8870..dbdef27 100644 --- a/comfy_cli/constants.py +++ b/comfy_cli/constants.py @@ -2,12 +2,17 @@ from enum import Enum -class OS(Enum): +class OS(str, Enum): WINDOWS = "windows" MACOS = "macos" LINUX = "linux" +class PROC(str, Enum): + X86_64 = "x86_64" + ARM = "arm" + + COMFY_GITHUB_URL = "https://github.com/comfyanonymous/ComfyUI" COMFY_MANAGER_GITHUB_URL = "https://github.com/ltdrdata/ComfyUI-Manager" @@ -58,12 +63,13 @@ class CUDAVersion(str, Enum): v11_8 = "11.8" -class GPU_OPTION(Enum): - NVIDIA = "Nvidia" - AMD = "Amd" - INTEL_ARC = "Intel Arc" - M_SERIES = "Mac M Series" - MAC_INTEL = "Mac Intel" +class GPU_OPTION(str, Enum): + CPU = None + NVIDIA = "nvidia" + AMD = "amd" + INTEL_ARC = "intel_arc" + M_SERIES = "mac_m_series" + MAC_INTEL = "mac_intel" # Referencing supported pt extension from ComfyUI diff --git a/comfy_cli/standalone.py b/comfy_cli/standalone.py new file mode 100644 index 0000000..b4c9fe0 --- /dev/null +++ b/comfy_cli/standalone.py @@ -0,0 +1,168 @@ +import os +import shutil +import subprocess +import tarfile +from pathlib import Path +from typing import Optional + +import requests + +from comfy_cli.constants import OS, PROC +from comfy_cli.typing import PathLike +from comfy_cli.utils import download_progress, get_os, get_proc +from comfy_cli.uv import DependencyCompiler + +_here = Path(__file__).expanduser().resolve().parent + +_platform_targets = { + (OS.MACOS, PROC.ARM): "aarch64-apple-darwin", + (OS.MACOS, PROC.X86_64): "x86_64-apple-darwin", + (OS.LINUX, PROC.X86_64): "x86_64_v3-unknown-linux-gnu", # x86_64_v3 assumes AVX256 support, no AVX512 support + (OS.WINDOWS, PROC.X86_64): "x86_64-pc-windows-msvc-shared", +} + +_latest_release_json_url = ( + "https://raw.githubusercontent.com/indygreg/python-build-standalone/latest-release/latest-release.json" +) +_asset_url_prefix = "https://github.com/indygreg/python-build-standalone/releases/download/{tag}" + + +def download_standalone_python( + platform: Optional[str] = None, + proc: Optional[str] = None, + version: str = "3.12.5", + tag: str = "latest", + flavor: str = "install_only", + cwd: PathLike = ".", +) -> PathLike: + """grab a pre-built distro from the python-build-standalone project. See + https://gregoryszorc.com/docs/python-build-standalone/main/""" + platform = get_os() if platform is None else platform + proc = get_proc() if proc is None else proc + target = _platform_targets[(platform, proc)] + + if tag == "latest": + # try to fetch json with info about latest release + response = requests.get(_latest_release_json_url) + if response.status_code != 200: + response.raise_for_status() + raise RuntimeError(f"Request to {_latest_release_json_url} returned status code {response.status_code}") + + latest_release = response.json() + tag = latest_release["tag"] + asset_url_prefix = latest_release["asset_url_prefix"] + else: + asset_url_prefix = _asset_url_prefix.format(tag=tag) + + name = f"cpython-{version}+{tag}-{target}-{flavor}" + fname = f"{name}.tar.gz" + url = os.path.join(asset_url_prefix, fname) + + return download_progress(url, fname, cwd=cwd) + + +class StandalonePython: + @staticmethod + def FromDistro( + platform: Optional[str] = None, + proc: Optional[str] = None, + version: str = "3.12.5", + tag: str = "latest", + flavor: str = "install_only", + cwd: PathLike = ".", + name: PathLike = "python", + ): + fpath = download_standalone_python( + platform=platform, + proc=proc, + version=version, + tag=tag, + flavor=flavor, + cwd=cwd, + ) + return StandalonePython.FromTarball(fpath, name) + + @staticmethod + def FromTarball(fpath: PathLike, name: PathLike = "python"): + fpath = Path(fpath) + with tarfile.open(fpath) as tar: + info = tar.next() + old_name = info.name.split("/")[0] + tar.extractall() + + old_rpath = fpath.parent / old_name + rpath = fpath.parent / name + shutil.move(old_rpath, rpath) + return StandalonePython(rpath=rpath) + + def __init__(self, rpath: PathLike): + self.rpath = Path(rpath) + self.name = self.rpath.name + self.bin = self.rpath / "bin" + self.executable = self.bin / "python" + + # paths to store package artifacts + self.cache = self.rpath / "cache" + self.wheels = self.rpath / "wheels" + + self.dep_comp = None + + # upgrade pip if needed, install uv + self.pip_install("-U", "pip", "uv") + + def run_module(self, mod: str, *args: list[str]): + cmd: list[str] = [ + str(self.executable), + "-m", + mod, + *args, + ] + + subprocess.run(cmd, check=True) + + def pip_install(self, *args: list[str]): + self.run_module("pip", "install", *args) + + def uv_install(self, *args: list[str]): + self.run_module("uv", "pip", "install", *args) + + def install_comfy_cli(self, dev: bool = False): + if dev: + self.uv_install(str(_here.parent)) + else: + self.uv_install("comfy_cli") + + def run_comfy_cli(self, *args: list[str]): + self.run_module("comfy_cli", *args) + + def install_comfy(self, *args: list[str], gpu_arg: str = "--nvidia"): + self.run_comfy_cli("--here", "--skip-prompt", "install", "--fast-deps", gpu_arg, *args) + + def compile_comfy_deps(self, comfyDir: PathLike, gpu: str, outDir: Optional[PathLike] = None): + outDir = self.rpath if outDir is None else outDir + + self.dep_comp = DependencyCompiler(cwd=comfyDir, executable=self.executable, gpu=gpu, outDir=outDir) + self.dep_comp.compile_comfy_deps() + + def install_comfy_deps(self, comfyDir: PathLike, gpu: str, outDir: Optional[PathLike] = None): + outDir = self.rpath if outDir is None else outDir + + self.dep_comp = DependencyCompiler(cwd=comfyDir, executable=self.executable, gpu=gpu, outDir=outDir) + self.dep_comp.install_core_plus_ext() + + def precache_comfy_deps(self, comfyDir: PathLike, gpu: str, outDir: Optional[PathLike] = None): + outDir = self.rpath if outDir is None else outDir + + self.dep_comp = DependencyCompiler(cwd=comfyDir, executable=self.executable, gpu=gpu, outDir=outDir) + self.dep_comp.precache_comfy_deps() + + def wheel_comfy_deps(self, comfyDir: PathLike, gpu: str, outDir: Optional[PathLike] = None): + outDir = self.rpath if outDir is None else outDir + + self.dep_comp = DependencyCompiler(cwd=comfyDir, executable=self.executable, gpu=gpu, outDir=outDir) + self.dep_comp.wheel_comfy_deps() + + def to_tarball(self, outPath: Optional[PathLike] = None): + outPath = self.rpath.with_suffix(".tgz") if outPath is None else Path(outPath) + with tarfile.open(outPath, "w:gz") as tar: + tar.add(self.rpath, arcname=self.rpath.parent) diff --git a/comfy_cli/typing.py b/comfy_cli/typing.py new file mode 100644 index 0000000..81d0e80 --- /dev/null +++ b/comfy_cli/typing.py @@ -0,0 +1,4 @@ +import os +from typing import Union + +PathLike = Union[os.PathLike[str], str] diff --git a/comfy_cli/utils.py b/comfy_cli/utils.py index 08ed34a..811867c 100644 --- a/comfy_cli/utils.py +++ b/comfy_cli/utils.py @@ -2,14 +2,20 @@ Module for utility functions. """ +import functools +import platform +import shutil import subprocess import sys +from pathlib import Path import psutil +import requests import typer -from rich import print +from rich import print, progress -from comfy_cli import constants +from comfy_cli.constants import DEFAULT_COMFY_WORKSPACE, OS, PROC +from comfy_cli.typing import PathLike def singleton(cls): @@ -34,11 +40,22 @@ def get_instance(*args, **kwargs): def get_os(): if sys.platform == "darwin": - return constants.OS.MACOS + return OS.MACOS elif "win" in sys.platform: - return constants.OS.WINDOWS + return OS.WINDOWS - return constants.OS.LINUX + return OS.LINUX + + +def get_proc(): + proc = platform.processor() + + if proc == "x86_64": + return PROC.X86_64 + elif "arm" in proc: + return PROC.ARM + else: + raise ValueError def install_conda_package(package_name): @@ -51,7 +68,7 @@ def install_conda_package(package_name): def get_not_user_set_default_workspace(): - return constants.DEFAULT_COMFY_WORKSPACE[get_os()] + return DEFAULT_COMFY_WORKSPACE[get_os()] def kill_all(pid): @@ -78,3 +95,24 @@ def f(incomplete: str) -> list[str]: return [opt for opt in opts if opt.startswith(incomplete)] return f + + +def download_progress(url: str, fname: PathLike, cwd: PathLike = ".", allow_redirects: bool = True) -> PathLike: + """download url to local file fname and show a progress bar. + See https://stackoverflow.com/q/37573483""" + cwd = Path(cwd).expanduser().resolve() + fpath = cwd / fname + + response = requests.get(url, stream=True, allow_redirects=allow_redirects) + if response.status_code != 200: + response.raise_for_status() # Will only raise for 4xx codes, so... + raise RuntimeError(f"Request to {url} returned status code {response.status_code}") + fsize = int(response.headers.get("Content-Length", 0)) + + desc = "(Unknown total file size)" if fsize == 0 else "" + response.raw.read = functools.partial(response.raw.read, decode_content=True) # Decompress if needed + with progress.wrap_file(response.raw, total=fsize, description=desc) as response_raw: + with fpath.open("wb") as f: + shutil.copyfileobj(response_raw, f) + + return fpath diff --git a/comfy_cli/uv.py b/comfy_cli/uv.py index f77686e..be33289 100644 --- a/comfy_cli/uv.py +++ b/comfy_cli/uv.py @@ -1,6 +1,4 @@ -import os import re -import shutil import subprocess import sys from importlib import metadata @@ -10,12 +8,11 @@ from comfy_cli import ui from comfy_cli.constants import GPU_OPTION +from comfy_cli.typing import PathLike -PathLike = Union[os.PathLike[str], str] - -def _run(cmd: list[str], cwd: PathLike) -> subprocess.CompletedProcess[Any]: - return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, check=True) +def _run(cmd: list[str], cwd: PathLike, check: bool = True) -> subprocess.CompletedProcess[Any]: + return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, check=check) def _check_call(cmd: list[str], cwd: Optional[PathLike] = None): @@ -47,7 +44,7 @@ def parse_uv_compile_error(err: str) -> tuple[str, list[str]]: class DependencyCompiler: - rocmPytorchUrl = "https://download.pytorch.org/whl/rocm6.0" + rocmPytorchUrl = "https://download.pytorch.org/whl/rocm6.1" nvidiaPytorchUrl = "https://download.pytorch.org/whl/cu121" overrideGpu = dedent( @@ -77,24 +74,23 @@ def Find_Req_Files(*ders: PathLike) -> list[Path]: ] @staticmethod - def Install_Build_Deps(): + def Install_Build_Deps(executable: PathLike = sys.executable): """Use pip to install bare minimum requirements for uv to do its thing""" - if shutil.which("uv") is None: - cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "pip", "uv"] - - _check_call(cmd=cmd) + cmd = [str(executable), "-m", "pip", "install", "--upgrade", "pip", "uv"] + _check_call(cmd=cmd) @staticmethod def Compile( cwd: PathLike, reqFiles: list[PathLike], + executable: PathLike = sys.executable, + index_strategy: str = "unsafe-best-match", override: Optional[PathLike] = None, out: Optional[PathLike] = None, - index_strategy: Optional[str] = "unsafe-best-match", resolve_strategy: Optional[str] = None, ) -> subprocess.CompletedProcess[Any]: cmd = [ - sys.executable, + str(executable), "-m", "uv", "pip", @@ -149,13 +145,14 @@ def Compile( def Install( cwd: PathLike, reqFile: list[PathLike], - override: Optional[PathLike] = None, - extraUrl: Optional[str] = None, - index_strategy: Optional[str] = "unsafe-best-match", dry: bool = False, + executable: PathLike = sys.executable, + extraUrl: Optional[str] = None, + index_strategy: str = "unsafe-best-match", + override: Optional[PathLike] = None, ) -> subprocess.CompletedProcess[Any]: cmd = [ - sys.executable, + str(executable), "-m", "uv", "pip", @@ -182,12 +179,13 @@ def Install( def Sync( cwd: PathLike, reqFile: list[PathLike], - extraUrl: Optional[str] = None, - index_strategy: Optional[str] = "unsafe-best-match", dry: bool = False, + executable: PathLike = sys.executable, + extraUrl: Optional[str] = None, + index_strategy: str = "unsafe-best-match", ) -> subprocess.CompletedProcess[Any]: cmd = [ - sys.executable, + str(executable), "-m", "uv", "pip", @@ -207,7 +205,67 @@ def Sync( return _check_call(cmd, cwd) @staticmethod - def Resolve_Gpu(gpu: Union[str, None]): + def Download( + cwd: PathLike, + reqFile: list[PathLike], + executable: PathLike = sys.executable, + extraUrl: Optional[str] = None, + noDeps: bool = False, + out: Optional[PathLike] = None, + ) -> subprocess.CompletedProcess[Any]: + """For now, the `download` cmd has no uv support, so use pip""" + cmd = [ + str(executable), + "-m", + "pip", + "download", + "-r", + str(reqFile), + ] + + if extraUrl is not None: + cmd.extend(["--extra-index-url", extraUrl]) + + if noDeps: + cmd.append("--no-deps") + + if out is not None: + cmd.extend(["-d", str(out)]) + + return _check_call(cmd, cwd) + + @staticmethod + def Wheel( + cwd: PathLike, + reqFile: list[PathLike], + executable: PathLike = sys.executable, + extraUrl: Optional[str] = None, + noDeps: bool = False, + out: Optional[PathLike] = None, + ) -> subprocess.CompletedProcess[Any]: + """For now, the `wheel` cmd has no uv support, so use pip""" + cmd = [ + str(executable), + "-m", + "pip", + "wheel", + "-r", + str(reqFile), + ] + + if extraUrl is not None: + cmd.extend(["--extra-index-url", extraUrl]) + + if noDeps: + cmd.append("--no-deps") + + if out is not None: + cmd.extend(["-w", str(out)]) + + return _check_call(cmd, cwd) + + @staticmethod + def Resolve_Gpu(gpu: Union[GPU_OPTION, str, None]): if gpu is None: try: tver = metadata.version("torch") @@ -219,30 +277,35 @@ def Resolve_Gpu(gpu: Union[str, None]): return None except metadata.PackageNotFoundError: return None + elif isinstance(gpu, str): + return GPU_OPTION[gpu.upper()] else: return gpu def __init__( self, cwd: PathLike = ".", + executable: PathLike = sys.executable, + gpu: Union[GPU_OPTION, str, None] = None, + outDir: PathLike = ".", + outName: str = "requirements.compiled", reqFilesCore: Optional[list[PathLike]] = None, reqFilesExt: Optional[list[PathLike]] = None, - gpu: Optional[str] = None, - outName: str = "requirements.compiled", ): - self.cwd = Path(cwd) - self.reqFiles = [Path(reqFile) for reqFile in reqFilesExt] if reqFilesExt is not None else None + self.cwd = Path(cwd).expanduser().resolve() + self.outDir = Path(outDir).expanduser().resolve() + # use .absolute since .resolve breaks the softlink-is-interpreter assumption of venvs + self.executable = Path(executable).expanduser().absolute() self.gpu = DependencyCompiler.Resolve_Gpu(gpu) + self.reqFiles = [Path(reqFile) for reqFile in reqFilesExt] if reqFilesExt is not None else None self.gpuUrl = ( - DependencyCompiler.nvidiaPytorchUrl - if self.gpu == GPU_OPTION.NVIDIA - else DependencyCompiler.rocmPytorchUrl - if self.gpu == GPU_OPTION.AMD - else None - ) - self.out = self.cwd / outName - self.override = self.cwd / "override.txt" + DependencyCompiler.nvidiaPytorchUrl if self.gpu == GPU_OPTION.NVIDIA else + DependencyCompiler.rocmPytorchUrl if self.gpu == GPU_OPTION.AMD else + None + ) # fmt: skip + self.out = self.outDir / outName + self.override = self.outDir / "override.txt" self.reqFilesCore = reqFilesCore if reqFilesCore is not None else self.find_core_reqs() self.reqFilesExt = reqFilesExt if reqFilesExt is not None else self.find_ext_reqs() @@ -263,7 +326,12 @@ def make_override(self): f.write(DependencyCompiler.overrideGpu.format(gpu=self.gpu, gpuUrl=self.gpuUrl)) f.write("\n\n") - completed = DependencyCompiler.Compile(cwd=self.cwd, reqFiles=self.reqFilesCore, override=self.override) + completed = DependencyCompiler.Compile( + cwd=self.cwd, + reqFiles=self.reqFilesCore, + executable=self.executable, + override=self.override, + ) with open(self.override, "a") as f: f.write("# ensure that core comfyui deps take precedence over any 3rd party extension deps\n") @@ -280,6 +348,7 @@ def compile_core_plus_ext(self): DependencyCompiler.Compile( cwd=self.cwd, reqFiles=(self.reqFilesCore + self.reqFilesExt), + executable=self.executable, override=self.override, out=self.out, resolve_strategy="ask", @@ -297,14 +366,16 @@ def install_core_plus_ext(self): DependencyCompiler.Install( cwd=self.cwd, reqFile=self.out, - override=self.override, + executable=self.executable, extraUrl=self.gpuUrl, + override=self.override, ) def sync_core_plus_ext(self): DependencyCompiler.Sync( cwd=self.cwd, reqFile=self.out, + executable=self.executable, extraUrl=self.gpuUrl, ) @@ -329,11 +400,35 @@ def handle_opencv(self): if "opencv-python==" not in line: f.write(line) - def install_comfy_deps(self): - DependencyCompiler.Install_Build_Deps() - + def compile_comfy_deps(self): self.make_override() self.compile_core_plus_ext() self.handle_opencv() + def precache_comfy_deps(self): + self.compile_comfy_deps() + DependencyCompiler.Download( + cwd=self.cwd, + reqFile=self.out, + executable=self.executable, + extraUrl=self.gpuUrl, + noDeps=True, + out=self.outDir / "cache", + ) + + def wheel_comfy_deps(self): + self.compile_comfy_deps() + DependencyCompiler.Wheel( + cwd=self.cwd, + reqFile=self.out, + executable=self.executable, + extraUrl=self.gpuUrl, + noDeps=True, + out=self.outDir / "wheels", + ) + + def install_comfy_deps(self): + DependencyCompiler.Install_Build_Deps(executable=self.executable) + + self.compile_comfy_deps() self.install_core_plus_ext() diff --git a/pyproject.toml b/pyproject.toml index 8a20bb9..272d47c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ "tomlkit", "typer>=0.9.0", "typing-extensions>=4.7.0", + "uv", "websocket-client", ] diff --git a/tests/uv/test_uv.py b/tests/uv/test_uv.py index d55c6d6..b38b9c1 100644 --- a/tests/uv/test_uv.py +++ b/tests/uv/test_uv.py @@ -29,6 +29,7 @@ def _mock_prompt_select(*args, **kwargs): def test_compile(mock_prompt_select): depComp = DependencyCompiler( cwd=temp, + outDir=temp, reqFilesCore=[reqsDir / "core_reqs.txt"], reqFilesExt=[reqsDir / "x_reqs.txt", reqsDir / "y_reqs.txt"], )