mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-03-08 02:27:31 +08:00
Add two CLI entry points for the unified dependency resolver: - `cm_cli uv-compile`: standalone batch resolution of all installed node pack dependencies via uv pip compile - `cm_cli install --uv-compile`: skip per-node pip, batch-resolve all deps after install completes (mutually exclusive with --no-deps) Both use a shared `_run_unified_resolve()` helper that passes real cm_global values (pip_blacklist, pip_overrides, pip_downgrade_blacklist) and guarantees PIPFixer.fix_broken() runs via try/finally. Update DESIGN, PRD, and TEST docs for consistency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
358 lines
19 KiB
Markdown
358 lines
19 KiB
Markdown
# PRD: Unified Dependency Resolver
|
|
|
|
## 1. Overview
|
|
|
|
### 1.1 Background
|
|
ComfyUI Manager currently installs each node pack's `requirements.txt` individually via `pip install`.
|
|
This approach causes dependency conflicts where installing a new node pack can break previously installed node packs' dependencies.
|
|
|
|
**Current flow:**
|
|
```mermaid
|
|
graph LR
|
|
A1[Install node pack A] --> A2[pip install A's deps] --> A3[Run install.py]
|
|
B1[Install node pack B] --> B2[pip install B's deps] --> B3[Run install.py]
|
|
B2 -.->|May break<br/>A's deps| A2
|
|
```
|
|
|
|
### 1.2 Goal
|
|
Implement a unified dependency installation module that uses `uv` to resolve all dependencies (installed node packs + new node packs) at once.
|
|
|
|
**New flow (unified resolver mode):**
|
|
```mermaid
|
|
graph TD
|
|
subgraph "Install Time (immediate)"
|
|
A1[User installs node pack X] --> A2[Git clone / download]
|
|
A2 --> A3["Run X's install.py (if exists)"]
|
|
A3 --> A4["Skip per-node pip install<br/>(deps deferred to restart)"]
|
|
end
|
|
|
|
subgraph "ComfyUI Restart (startup batch)"
|
|
B1[prestartup_script.py] --> B2[Collect ALL installed node packs' deps]
|
|
B2 --> B3["uv pip compile → pinned requirements.txt"]
|
|
B3 --> B4["uv pip install -r → Batch install"]
|
|
B4 --> B5[PIPFixer environment correction]
|
|
end
|
|
```
|
|
|
|
> **Terminology**: In this document, "lockfile" refers to the **pinned requirements.txt** generated by `uv pip compile`.
|
|
> This is different from the `uv.lock` (TOML format) generated by `uv lock`. We use a pip-compatible workflow.
|
|
|
|
### 1.3 Scope
|
|
- Develop a new dedicated dependency resolution module
|
|
- Opt-in activation from the existing install process
|
|
- **Handles dependency resolution (deps install) only**. `install.py` execution is handled by existing logic
|
|
|
|
---
|
|
|
|
## 2. Constraints
|
|
|
|
| Item | Description |
|
|
|------|-------------|
|
|
| **uv required** | Only operates in environments where `uv` is available |
|
|
| **Independent of `use_uv` flag** | `use_unified_resolver` is separate from the existing `use_uv` flag. Even if `use_uv=False`, setting `use_unified_resolver=True` attempts resolver activation. Auto-fallback if uv is not installed |
|
|
| **Pre-validated list** | Input node pack list is assumed to be pre-verified for mutual dependency compatibility |
|
|
| **Backward compatibility** | Existing pip-based install process is fully preserved (fallback) |
|
|
| **Blacklist/overrides bypassed** | In unified mode, `pip_blacklist`, `pip_overrides`, `pip_downgrade_blacklist` are NOT applied (empty values passed). Constructor interface is preserved for future extensibility. `uv pip compile` handles all conflict resolution natively. **[DEFERRED]** Reading actual values from `cm_global` at startup is deferred to a future version — v1 always passes empty values |
|
|
| **Multiple custom_nodes paths** | Supports all paths returned by `folder_paths.get_folder_paths('custom_nodes')` |
|
|
| **Scope of application** | Batch resolver runs at **module scope** in `prestartup_script.py` (unconditionally when enabled, independent of `install-scripts.txt` existence). The 2 `execute_install_script()` locations skip per-node pip install when unified mode is active (deps deferred to restart). `execute_lazy_install_script()` is also modified to skip per-node pip install in unified mode. Other install paths such as `install_manager_requirements()`, `pip_install()` are outside v1 scope (future extension) |
|
|
| **Legacy module** | `comfyui_manager/legacy/manager_core.py` is excluded from modification. Legacy paths retain existing pip behavior |
|
|
|
|
---
|
|
|
|
## 3. Functional Requirements
|
|
|
|
### FR-1: Node Pack List and Base Dependency Input
|
|
|
|
**Input:**
|
|
- Node pack list (fullpath list of installed + to-be-installed node packs)
|
|
- Base dependencies (ComfyUI's `requirements.txt` and `manager_requirements.txt`)
|
|
|
|
**Behavior:**
|
|
- Validate each node pack path
|
|
- Exclude disabled (`.disabled`) node packs
|
|
- Detection criteria: Existence of `custom_nodes/.disabled/{node_pack_name}` **directory**
|
|
- Existing mechanism: Disabling a node pack **moves** it from `custom_nodes/` to `custom_nodes/.disabled/` (does NOT create a `.disabled` file inside the node pack)
|
|
- At resolver input time, disabled node packs should already be absent from `custom_nodes/`, so normally they won't be in `node_pack_paths`
|
|
- Defensively exclude any node pack paths that are within the `.disabled` directory
|
|
- Base dependencies are treated as constraints
|
|
- Traverse all paths from `folder_paths.get_folder_paths('custom_nodes')`
|
|
|
|
**`cm_global` runtime dependencies:**
|
|
- `cm_global.pip_overrides`, `pip_blacklist`, `pip_downgrade_blacklist` are dynamically assigned during `prestartup_script.py` execution
|
|
- In unified mode, these are **not applied** — empty values are passed to the resolver constructor
|
|
- The constructor interface accepts these parameters for future extensibility (defaults to empty when `None`)
|
|
|
|
### FR-2: Dependency List Extraction
|
|
|
|
**Behavior:**
|
|
- Parse `requirements.txt` from each node pack directory
|
|
- Encoding: Use `robust_readlines()` pattern (`chardet` detection, assumes UTF-8 if not installed)
|
|
- Package name remapping (constructor accepts `overrides` dict — **empty in v1**, interface preserved for extensibility)
|
|
- Blacklist package filtering (constructor accepts `blacklist` set — **empty in v1**, uv handles torch etc. natively)
|
|
- Downgrade blacklist filtering (constructor accepts `downgrade_blacklist` list — **empty in v1**)
|
|
- **Note**: In unified mode, `uv pip compile` resolves all version conflicts natively. The blacklist/overrides/downgrade_blacklist mechanisms from the existing pip flow are bypassed
|
|
- Strip comments (`#`) and blank lines
|
|
- **Input sanitization** (see below)
|
|
- Separate handling of `--index-url` entries (see below)
|
|
|
|
**Input sanitization:**
|
|
- Requirements lines matching the following patterns are **rejected and logged** (security defense):
|
|
- `-r`, `--requirement` (recursive include → path traversal risk)
|
|
- `-e`, `--editable` (VCS/local path install → arbitrary code execution risk)
|
|
- `-c`, `--constraint` (external constraint file injection)
|
|
- `--find-links`, `-f` (external package source specification)
|
|
- `@ file://` (local file reference → path traversal risk)
|
|
- Package names containing path separators (`/`, `\`)
|
|
- Allowed items: Package specs (`name>=version`), specs with `--index-url`, environment markers (containing `;`)
|
|
- Rejected lines are recorded in the `skipped` list with reason
|
|
|
|
**`--index-url` handling:**
|
|
- Existing code (standalone function `execute_install_script()`) parses `package_name --index-url URL` format for special handling
|
|
- **Note**: The class method `UnifiedManager.execute_install_script()` does NOT have this handling (asymmetric)
|
|
- The unified resolver **unifies both paths** for consistent handling:
|
|
- Package spec → added to the general dependency list
|
|
- `--extra-index-url URL` → passed as `uv pip compile` argument
|
|
- Separated index URLs are collected in `CollectedDeps.extra_index_urls`
|
|
- **Credential redaction**: Authentication info (`user:pass@`) in index URLs is masked during logging
|
|
|
|
**Duplicate handling strategy:**
|
|
- No deduplication is performed directly
|
|
- Different version specs of the same package are **all passed as-is** to uv
|
|
- `uv pip compile` handles version resolution (uv determines the optimal version)
|
|
|
|
**Output:**
|
|
- Unified dependency list (tracked by source node pack)
|
|
- Additional index URL list
|
|
|
|
### FR-3: uv pip compile Execution
|
|
|
|
**Behavior:**
|
|
- Generate temporary requirements file from the collected dependency list
|
|
- Execute `uv pip compile` to produce a pinned requirements.txt
|
|
- `--output-file` (required): Specify output file (outputs to stdout only if not specified)
|
|
- `--constraint`: Pass base dependencies as constraints
|
|
- `--python`: Current Python interpreter path
|
|
- `--extra-index-url`: Additional index URLs collected from FR-2 (multiple allowed)
|
|
- Resolve for the current platform (platform-specific results)
|
|
|
|
**Error handling:**
|
|
- Return conflict package report when resolution fails
|
|
- Timeout handling (300s): Explicitly catch `subprocess.TimeoutExpired`, terminate child process, then fallback
|
|
- Lockfile output file existence verification: Confirm file was actually created even when `returncode == 0`
|
|
- Temp file cleanup: Guaranteed in `finally` block. Includes stale temp file cleanup logic at next execution for abnormal termination (SIGKILL) scenarios
|
|
|
|
**Output:**
|
|
- pinned requirements.txt (file with all packages pinned to exact versions)
|
|
|
|
### FR-4: Pinned Requirements-based Dependency Installation
|
|
|
|
**Behavior:**
|
|
- Execute `uv pip install -r <pinned-requirements.txt>`
|
|
- **Do NOT use `uv pip sync`**: sync deletes packages not in the lockfile, risking removal of torch, ComfyUI's own dependencies, etc.
|
|
- Already-installed packages at the same version are skipped (default uv behavior)
|
|
- Log installation results
|
|
|
|
**Error handling:**
|
|
- `uv pip install -r` is an **atomic operation** (all-or-nothing)
|
|
- On total failure: Parse stderr for failure cause report → fallback to existing pip
|
|
- **No partial failure report** (not possible due to uv's behavior)
|
|
- `InstallResult`'s `installed`/`skipped` fields are populated by parsing uv stdout; `stderr` records failure cause (no separate `failed` field needed due to atomic model)
|
|
|
|
### FR-5: Post-install Environment Correction
|
|
|
|
**Behavior:**
|
|
- Call `PIPFixer.fix_broken()` for environment integrity correction
|
|
- Restore torch version (when change detected)
|
|
- Fix OpenCV conflicts
|
|
- Restore comfyui-frontend-package
|
|
- Restore packages based on `pip_auto_fix.list`
|
|
- **This step is already performed in the existing `execute_install_script()` flow, so the unified resolver itself doesn't need to call it**
|
|
- However, an optional call option is provided for cases where the resolver is invoked independently outside the existing flow
|
|
|
|
### FR-6: install.py Execution (Existing Flow Maintained)
|
|
|
|
**Behavior:**
|
|
- The unified resolver handles deps installation **at startup time only**
|
|
- `install.py` execution is handled by the existing `execute_install_script()` flow and runs **immediately** at install time
|
|
- Deps are deferred to startup batch resolution; `install.py` runs without waiting for deps
|
|
|
|
**Control flow specification (unified mode active):**
|
|
- `execute_install_script()`: **skip** the `requirements.txt`-based individual pip install loop entirely (deps will be resolved at next restart)
|
|
- `install.py` execution runs **immediately** as before
|
|
- At next ComfyUI restart: `prestartup_script.py` runs the unified resolver for all installed node packs
|
|
|
|
**Control flow specification (unified mode inactive / fallback):**
|
|
- Existing pip install loop runs as-is (no change)
|
|
- `install.py` execution runs **immediately** as before
|
|
|
|
### FR-7: Startup Batch Resolution
|
|
|
|
**Behavior:**
|
|
- When `use_unified_resolver=True`, **all dependency resolution is deferred to ComfyUI startup**
|
|
- At install time: node pack itself is installed (git clone, etc.) and `install.py` runs immediately, but `requirements.txt` deps are **not** installed per-request
|
|
- At startup time: `prestartup_script.py` runs the unified resolver once for all installed node packs
|
|
|
|
**Startup execution flow (in `prestartup_script.py`):**
|
|
1. At **module scope** (before `execute_startup_script()` gate): check `manager_util.use_unified_resolver` flag
|
|
2. If enabled: collect all installed node pack paths, read base requirements from `comfy_path`
|
|
3. Create `UnifiedDepResolver` with empty blacklist/overrides/downgrade_blacklist (uv handles resolution natively)
|
|
4. Call `resolve_and_install()` — collects all deps → compile → install in one batch
|
|
5. On success: set `_unified_resolver_succeeded = True`, skip per-node pip in `execute_lazy_install_script()`
|
|
6. On failure: log warning, `execute_lazy_install_script()` falls back to existing per-node pip install
|
|
7. **Note**: Runs unconditionally when enabled, independent of `install-scripts.txt` existence
|
|
|
|
**`execute_install_script()` behavior in unified mode:**
|
|
- Skip the `requirements.txt` pip install loop entirely (deps will be handled at restart)
|
|
- `install.py` execution still runs immediately
|
|
|
|
**`execute_lazy_install_script()` behavior in unified mode:**
|
|
- Skip the `requirements.txt` pip install loop (already handled by startup batch resolver)
|
|
- `install.py` execution still runs
|
|
|
|
**Windows-specific behavior:**
|
|
- Windows lazy install path also benefits from startup batch resolution
|
|
- `try_install_script()` defers to `reserve_script()` as before for non-`instant_execution=True` installs
|
|
|
|
---
|
|
|
|
## 4. Non-functional Requirements
|
|
|
|
| Item | Requirement |
|
|
|------|-------------|
|
|
| **Performance** | Equal to or faster than existing individual installs |
|
|
| **Stability** | Must not break the existing environment |
|
|
| **Logging** | Log progress and results at each step (details below) |
|
|
| **Error recovery** | Fallback to existing pip method on failure |
|
|
| **Testing** | Unit test coverage above 80% |
|
|
| **Security** | requirements.txt input sanitization (see FR-2), credential log redaction, subprocess list-form invocation |
|
|
| **Concurrency** | Prevent lockfile path collisions on concurrent install requests. Use process/thread-unique suffixes or temp directories |
|
|
| **Temp files** | Guarantee temp file cleanup on both normal and abnormal termination. Clean stale files on next execution |
|
|
|
|
### Logging Requirements
|
|
|
|
| Step | Log Level | Content |
|
|
|------|-----------|---------|
|
|
| Resolver start | `INFO` | Node pack count, total dependency count, mode (unified/pip) |
|
|
| Dependency collection | `INFO` | Collection summary (collected N, skipped N, sources N) |
|
|
| Dependency collection | `DEBUG` | Per-package collection/skip/remap details |
|
|
| `--index-url` detection | `INFO` | Detected additional index URL list |
|
|
| uv compile start | `INFO` | Execution command (excluding sensitive info) |
|
|
| uv compile success | `INFO` | Pinned package count, elapsed time |
|
|
| uv compile failure | `WARNING` | Conflict details, fallback transition notice |
|
|
| Install start | `INFO` | Number of packages to install |
|
|
| Install success | `INFO` | Installed/skipped/failed count summary, elapsed time |
|
|
| Install failure | `WARNING` | Failed package list, fallback transition notice |
|
|
| Fallback transition | `WARNING` | Transition reason, original error message |
|
|
| Overall completion | `INFO` | Final result summary (success/fallback/failure) |
|
|
|
|
> **Log prefix**: All logs use `[UnifiedDepResolver]` prefix to distinguish from existing pip install logs
|
|
|
|
---
|
|
|
|
## 5. Usage Scenarios
|
|
|
|
### Scenario 1: Single Node Pack Installation (unified mode)
|
|
```
|
|
User requests installation of node pack X
|
|
→ Git clone / download node pack X
|
|
→ Run X's install.py (if exists) — immediately
|
|
→ Skip per-node pip install (deps deferred)
|
|
→ User restarts ComfyUI
|
|
→ prestartup_script.py: Collect deps from ALL installed node packs (A,B,C,X)
|
|
→ uv pip compile resolves fully compatible versions
|
|
→ uv pip install -r for batch installation
|
|
→ PIPFixer environment correction
|
|
```
|
|
|
|
### Scenario 2: Multi Node Pack Batch Installation (unified mode)
|
|
```
|
|
User requests installation of node packs X, Y, Z
|
|
→ Each node pack: git clone + install.py — immediately
|
|
→ Per-node pip install skipped for all
|
|
→ User restarts ComfyUI
|
|
→ prestartup_script.py: Collect deps from ALL installed node packs (including X,Y,Z)
|
|
→ Single uv pip compile → single uv pip install -r
|
|
→ PIPFixer environment correction
|
|
```
|
|
|
|
### Scenario 3: Dependency Resolution Failure (Edge Case)
|
|
```
|
|
Even pre-validated lists may fail due to uv version differences or platform issues
|
|
→ uv pip compile failure → return conflict report
|
|
→ Display conflict details to user
|
|
→ Auto-execute existing pip fallback
|
|
```
|
|
|
|
### Scenario 4: uv Not Installed
|
|
```
|
|
uv unavailable detected → auto-fallback to existing pip method
|
|
→ Display uv installation recommendation to user
|
|
```
|
|
|
|
### Scenario 5: Windows Lazy Installation (unified mode)
|
|
```
|
|
Node pack installation requested on Windows
|
|
→ Node pack install deferred to startup (existing lazy mechanism)
|
|
→ On next ComfyUI startup: unified resolver runs first (batch deps)
|
|
→ execute_lazy_install_script() skips per-node pip (already resolved)
|
|
→ install.py still runs per node pack
|
|
```
|
|
|
|
### Scenario 6: Malicious/Non-standard requirements.txt
|
|
```
|
|
Node pack's requirements.txt contains `-r ../../../etc/hosts` or `-e git+https://...`
|
|
→ Sanitization filter rejects the line
|
|
→ Log rejection reason and continue processing remaining valid packages
|
|
→ Notify user of rejected item count
|
|
```
|
|
|
|
### Scenario 7: Concurrent Install Requests (unified mode)
|
|
```
|
|
User requests installation of node packs A and B nearly simultaneously from UI
|
|
→ Each request: git clone + install.py immediately, deps skipped
|
|
→ On restart: single unified resolver run handles both A and B deps together
|
|
→ No concurrency issue (single batch at startup)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Success Metrics
|
|
|
|
| Metric | Target |
|
|
|--------|--------|
|
|
| Dependency conflict reduction | 90%+ reduction compared to current |
|
|
| Install success rate | 99%+ (for compatibility-verified lists) |
|
|
| Performance | Equal to or better than existing individual installs |
|
|
| Adoption rate | 50%+ of eligible users |
|
|
|
|
---
|
|
|
|
## 7. Future Extensions
|
|
|
|
- ~~**`cm_global` integration** [DONE]: `cm_cli uv-compile` and `cm_cli install --uv-compile` pass real `cm_global` values. Startup path (`prestartup_script.py`) still passes empty by design~~
|
|
- Lockfile caching: Reuse for identical node pack configurations
|
|
- Pre-install dependency conflict validation API: Check compatibility before installation
|
|
- Dependency tree visualization: Display dependency relationships to users
|
|
- `uv lock`-based cross-platform lockfile support (TOML format)
|
|
- `install_manager_requirements()` integration: Resolve manager's own dependencies through unified resolver
|
|
- `pip_install()` integration: Route UI direct installs through unified resolver
|
|
- Legacy module (`comfyui_manager/legacy/`) unified resolver support
|
|
|
|
---
|
|
|
|
## Appendix A: Existing Code Install Path Mapping
|
|
|
|
> This section is reference material to clarify the unified resolver's scope of application.
|
|
|
|
| Install Path | Location | v1 Applied | Notes |
|
|
|-------------|----------|------------|-------|
|
|
| `UnifiedManager.execute_install_script()` | `glob/manager_core.py` (method) | ✅ Yes | Skips per-node pip in unified mode (deps deferred to restart) |
|
|
| Standalone `execute_install_script()` | `glob/manager_core.py` (function) | ✅ Yes | Skips per-node pip in unified mode (deps deferred to restart) |
|
|
| `execute_lazy_install_script()` | `prestartup_script.py` | ✅ Yes | Skips per-node pip in unified mode (already batch-resolved) |
|
|
| Startup batch resolver | `prestartup_script.py` | ✅ Yes | **New**: Runs unified resolver once at startup for all node packs |
|
|
| `install_manager_requirements()` | `glob/manager_core.py` | ❌ No | Manager's own deps |
|
|
| `pip_install()` | `glob/manager_core.py` | ❌ No | UI direct install |
|
|
| Legacy `execute_install_script()` (2 locations) | `legacy/manager_core.py` | ❌ No | Legacy paths |
|
|
| `cm_cli uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Standalone CLI batch resolution (with `cm_global` values) |
|
|
| `cm_cli install --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped, batch resolution after all installs |
|