mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-03-14 21:47:37 +08:00
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.
364 lines
20 KiB
Markdown
364 lines
20 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]: All `--uv-compile` CLI commands (`uv-compile`, `install`, `reinstall`, `update`, `fix`, `restore-snapshot`, `restore-dependencies`, `install-deps`) 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 |
|
|
| `cm_cli reinstall --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped, batch resolution after all reinstalls; mutually exclusive with `--no-deps` |
|
|
| `cm_cli update --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped during updates, batch resolution after |
|
|
| `cm_cli fix --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped during dep fix, batch resolution after |
|
|
| `cm_cli restore-snapshot --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped during restore, batch resolution after |
|
|
| `cm_cli restore-dependencies --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped, batch resolution after all node deps restored |
|
|
| `cm_cli install-deps --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped, batch resolution after deps-spec install |
|