Skip to content

Commit

Permalink
Add builder for standalone python for comfy (#158)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
telamonian authored Aug 23, 2024
1 parent cddeb56 commit 7ce59eb
Show file tree
Hide file tree
Showing 9 changed files with 472 additions and 80 deletions.
28 changes: 0 additions & 28 deletions .pylintrc

This file was deleted.

107 changes: 107 additions & 0 deletions comfy_cli/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.")
Expand Down
20 changes: 13 additions & 7 deletions comfy_cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down
168 changes: 168 additions & 0 deletions comfy_cli/standalone.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions comfy_cli/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os
from typing import Union

PathLike = Union[os.PathLike[str], str]
Loading

0 comments on commit 7ce59eb

Please sign in to comment.