mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-07-26 10:07:38 +08:00
feat(cli): expand --uv-compile to all node management commands with conflict attribution (#2682)
* feat(cli): expand --uv-compile to all node management commands with conflict attribution Add --uv-compile flag to reinstall, update, fix, restore-snapshot, restore-dependencies, and install-deps commands. Each skips per-node pip installs and runs batch uv pip compile after all operations. Change CollectedDeps.sources type to dict[str, list[tuple[str, str]]] to store (pack_path, pkg_spec) per requester. On resolution failure, _run_unified_resolve() cross-references conflict packages with sources using word-boundary regex and displays which node packs requested each conflicting package. Update EN/KO user docs and DESIGN/PRD developer docs to cover the expanded commands and conflict attribution output. Strengthen unit tests for sources tuple format and compile failure attribution. Bump version to 4.1b3. * refactor(cli): extract _finalize_resolve helper, add CNR nightly fallback and pydantic guard - Extract `_finalize_resolve()` to eliminate 7x duplicated uv-compile error handling blocks in cm_cli (~-85 lines) - Move conflict attribution regex to `attribute_conflicts()` in unified_dep_resolver.py for direct testability - Update 4 attribution tests to call production function instead of re-implementing regex - Add CNR nightly fallback: when node is absent from nightly manifest, fall back to cnr_map repository URL (glob + legacy) - Add pydantic Union guard: use getattr for is_unknown in uninstall and disable handlers to prevent Union type mismatch - Add E2E test suites for endpoint install/uninstall and uv-compile CLI commands (conflict + success cases) - Add nightly CNR fallback regression tests
This commit is contained in:
+121
-27
@@ -688,21 +688,7 @@ def install(
|
||||
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
|
||||
for_each_nodes(nodes, act=install_node, exit_on_fail=exit_on_fail)
|
||||
|
||||
if uv_compile:
|
||||
try:
|
||||
_run_unified_resolve()
|
||||
except ImportError as e:
|
||||
print(f"[bold red]Failed to import unified_dep_resolver: {e}[/bold red]")
|
||||
raise typer.Exit(1)
|
||||
except typer.Exit:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[bold red]Batch resolution failed: {e}[/bold red]")
|
||||
raise typer.Exit(1)
|
||||
finally:
|
||||
pip_fixer.fix_broken()
|
||||
else:
|
||||
pip_fixer.fix_broken()
|
||||
_finalize_resolve(pip_fixer, uv_compile)
|
||||
|
||||
|
||||
@app.command(help="Reinstall custom nodes")
|
||||
@@ -729,6 +715,14 @@ def reinstall(
|
||||
help="Skip installing any Python dependencies",
|
||||
),
|
||||
] = False,
|
||||
uv_compile: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option(
|
||||
"--uv-compile",
|
||||
show_default=False,
|
||||
help="After reinstalling, batch-resolve all dependencies via uv pip compile",
|
||||
),
|
||||
] = False,
|
||||
user_directory: str = typer.Option(
|
||||
None,
|
||||
help="user directory"
|
||||
@@ -736,11 +730,20 @@ def reinstall(
|
||||
):
|
||||
cmd_ctx.set_user_directory(user_directory)
|
||||
cmd_ctx.set_channel_mode(channel, mode)
|
||||
cmd_ctx.set_no_deps(no_deps)
|
||||
|
||||
if uv_compile and no_deps:
|
||||
print("[bold red]--uv-compile and --no-deps are mutually exclusive.[/bold red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if uv_compile:
|
||||
cmd_ctx.set_no_deps(True)
|
||||
else:
|
||||
cmd_ctx.set_no_deps(no_deps)
|
||||
|
||||
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
|
||||
for_each_nodes(nodes, act=reinstall_node)
|
||||
pip_fixer.fix_broken()
|
||||
|
||||
_finalize_resolve(pip_fixer, uv_compile)
|
||||
|
||||
|
||||
@app.command(help="Uninstall custom nodes")
|
||||
@@ -785,10 +788,21 @@ def update(
|
||||
None,
|
||||
help="user directory"
|
||||
),
|
||||
uv_compile: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option(
|
||||
"--uv-compile",
|
||||
show_default=False,
|
||||
help="After updating, batch-resolve all dependencies via uv pip compile",
|
||||
),
|
||||
] = False,
|
||||
):
|
||||
cmd_ctx.set_user_directory(user_directory)
|
||||
cmd_ctx.set_channel_mode(channel, mode)
|
||||
|
||||
if uv_compile:
|
||||
cmd_ctx.set_no_deps(True)
|
||||
|
||||
if 'all' in nodes:
|
||||
asyncio.run(auto_save_snapshot())
|
||||
|
||||
@@ -800,7 +814,8 @@ def update(
|
||||
break
|
||||
|
||||
update_parallel(nodes)
|
||||
pip_fixer.fix_broken()
|
||||
|
||||
_finalize_resolve(pip_fixer, uv_compile)
|
||||
|
||||
|
||||
@app.command(help="Disable custom nodes")
|
||||
@@ -886,16 +901,28 @@ def fix(
|
||||
None,
|
||||
help="user directory"
|
||||
),
|
||||
uv_compile: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option(
|
||||
"--uv-compile",
|
||||
show_default=False,
|
||||
help="After fixing, batch-resolve all dependencies via uv pip compile",
|
||||
),
|
||||
] = False,
|
||||
):
|
||||
cmd_ctx.set_user_directory(user_directory)
|
||||
cmd_ctx.set_channel_mode(channel, mode)
|
||||
|
||||
if uv_compile:
|
||||
cmd_ctx.set_no_deps(True)
|
||||
|
||||
if 'all' in nodes:
|
||||
asyncio.run(auto_save_snapshot())
|
||||
|
||||
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
|
||||
for_each_nodes(nodes, fix_node, allow_all=True)
|
||||
pip_fixer.fix_broken()
|
||||
|
||||
_finalize_resolve(pip_fixer, uv_compile)
|
||||
|
||||
|
||||
@app.command("show-versions", help="Show all available versions of the node")
|
||||
@@ -1092,7 +1119,7 @@ def save_snapshot(
|
||||
|
||||
@app.command("restore-snapshot", help="Restore snapshot from snapshot file")
|
||||
def restore_snapshot(
|
||||
snapshot_name: str,
|
||||
snapshot_name: str,
|
||||
pip_non_url: Optional[bool] = typer.Option(
|
||||
default=None,
|
||||
show_default=False,
|
||||
@@ -1118,13 +1145,24 @@ def restore_snapshot(
|
||||
restore_to: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Manually specify the installation path for the custom node. Ignore user directory."
|
||||
)
|
||||
),
|
||||
uv_compile: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option(
|
||||
"--uv-compile",
|
||||
show_default=False,
|
||||
help="After restoring, batch-resolve all dependencies via uv pip compile",
|
||||
),
|
||||
] = False,
|
||||
):
|
||||
cmd_ctx.set_user_directory(user_directory)
|
||||
|
||||
if restore_to:
|
||||
cmd_ctx.update_custom_nodes_dir(restore_to)
|
||||
|
||||
if uv_compile:
|
||||
cmd_ctx.set_no_deps(True)
|
||||
|
||||
extras = []
|
||||
if pip_non_url:
|
||||
extras.append('--pip-non-url')
|
||||
@@ -1151,8 +1189,11 @@ def restore_snapshot(
|
||||
except Exception:
|
||||
print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
|
||||
traceback.print_exc()
|
||||
if uv_compile:
|
||||
pip_fixer.fix_broken()
|
||||
raise typer.Exit(code=1)
|
||||
pip_fixer.fix_broken()
|
||||
|
||||
_finalize_resolve(pip_fixer, uv_compile)
|
||||
|
||||
|
||||
@app.command(
|
||||
@@ -1162,10 +1203,21 @@ def restore_dependencies(
|
||||
user_directory: str = typer.Option(
|
||||
None,
|
||||
help="user directory"
|
||||
)
|
||||
),
|
||||
uv_compile: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option(
|
||||
"--uv-compile",
|
||||
show_default=False,
|
||||
help="After restoring, batch-resolve all dependencies via uv pip compile",
|
||||
),
|
||||
] = False,
|
||||
):
|
||||
cmd_ctx.set_user_directory(user_directory)
|
||||
|
||||
if uv_compile:
|
||||
cmd_ctx.set_no_deps(True)
|
||||
|
||||
node_paths = []
|
||||
|
||||
for base_path in cmd_ctx.get_custom_nodes_paths():
|
||||
@@ -1181,9 +1233,10 @@ def restore_dependencies(
|
||||
for x in node_paths:
|
||||
print("----------------------------------------------------------------------------------------------------")
|
||||
print(f"Restoring [{i}/{total}]: {x}")
|
||||
unified_manager.execute_install_script('', x, instant_execution=True)
|
||||
unified_manager.execute_install_script('', x, instant_execution=True, no_deps=bool(uv_compile))
|
||||
i += 1
|
||||
pip_fixer.fix_broken()
|
||||
|
||||
_finalize_resolve(pip_fixer, uv_compile)
|
||||
|
||||
|
||||
@app.command(
|
||||
@@ -1224,9 +1277,21 @@ def install_deps(
|
||||
None,
|
||||
help="user directory"
|
||||
),
|
||||
uv_compile: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option(
|
||||
"--uv-compile",
|
||||
show_default=False,
|
||||
help="After installing, batch-resolve all dependencies via uv pip compile",
|
||||
),
|
||||
] = False,
|
||||
):
|
||||
cmd_ctx.set_user_directory(user_directory)
|
||||
cmd_ctx.set_channel_mode(channel, mode)
|
||||
|
||||
if uv_compile:
|
||||
cmd_ctx.set_no_deps(True)
|
||||
|
||||
asyncio.run(auto_save_snapshot())
|
||||
|
||||
if not os.path.exists(deps):
|
||||
@@ -1246,19 +1311,40 @@ def install_deps(
|
||||
if state == 'installed':
|
||||
continue
|
||||
elif state == 'not-installed':
|
||||
asyncio.run(core.gitclone_install(k, instant_execution=True))
|
||||
asyncio.run(core.gitclone_install(k, instant_execution=True, no_deps=bool(uv_compile)))
|
||||
else: # disabled
|
||||
core.gitclone_set_active([k], False)
|
||||
pip_fixer.fix_broken()
|
||||
|
||||
_finalize_resolve(pip_fixer, uv_compile)
|
||||
|
||||
print("Dependency installation and activation complete.")
|
||||
|
||||
|
||||
def _finalize_resolve(pip_fixer, uv_compile) -> None:
|
||||
"""Run batch resolution if --uv-compile is set, then fix broken packages."""
|
||||
if uv_compile:
|
||||
try:
|
||||
_run_unified_resolve()
|
||||
except ImportError as e:
|
||||
print(f"[bold red]Failed to import unified_dep_resolver: {e}[/bold red]")
|
||||
raise typer.Exit(1)
|
||||
except typer.Exit:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[bold red]Batch resolution failed: {e}[/bold red]")
|
||||
raise typer.Exit(1)
|
||||
finally:
|
||||
pip_fixer.fix_broken()
|
||||
else:
|
||||
pip_fixer.fix_broken()
|
||||
|
||||
|
||||
def _run_unified_resolve():
|
||||
"""Shared logic for unified batch dependency resolution."""
|
||||
from comfyui_manager.common.unified_dep_resolver import (
|
||||
UnifiedDepResolver,
|
||||
UvNotAvailableError,
|
||||
attribute_conflicts,
|
||||
collect_base_requirements,
|
||||
collect_node_pack_paths,
|
||||
)
|
||||
@@ -1295,6 +1381,14 @@ def _run_unified_resolve():
|
||||
print("[bold green]Resolution complete (no deps needed).[/bold green]")
|
||||
else:
|
||||
print(f"[bold red]Resolution failed: {result.error}[/bold red]")
|
||||
if result.lockfile and result.lockfile.conflicts and result.collected:
|
||||
attributed = attribute_conflicts(result.collected.sources, result.lockfile.conflicts)
|
||||
if attributed:
|
||||
print("[bold yellow]Conflicting packages (by node pack):[/bold yellow]")
|
||||
for pkg_name, requesters in sorted(attributed.items()):
|
||||
print(f" [yellow]{pkg_name}[/yellow]:")
|
||||
for pack_path, pkg_spec in requesters:
|
||||
print(f" {os.path.basename(pack_path)} → {pkg_spec}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user