mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2025-12-22 04:40:49 +08:00
Remove package-level caching in cnr_utils and node_package modules to enable proper dynamic custom node installation and version switching without ComfyUI server restarts. Key Changes: - Remove @lru_cache decorators from version-sensitive functions - Remove cached_property from NodePackage for dynamic state updates - Add comprehensive test suite with parallel execution support - Implement version switching tests (CNR ↔ Nightly) - Add case sensitivity integration tests - Improve error handling and logging API Priority Rules (manager_core.py:1801): - Enabled-Priority: Show only enabled version when both exist - CNR-Priority: Show only CNR when both CNR and Nightly are disabled - Prevents duplicate package entries in /v2/customnode/installed API - Cross-match using cnr_id and aux_id for CNR ↔ Nightly detection Test Infrastructure: - 8 test files with 59 comprehensive test cases - Parallel test execution across 5 isolated environments - Automated test scripts with environment setup - Configurable timeout (60 minutes default) - Support for both master and dr-support-pip-cm branches Bug Fixes: - Fix COMFYUI_CUSTOM_NODES_PATH environment variable export - Resolve test fixture regression with module-level variables - Fix import timing issues in test configuration - Register pytest integration marker to eliminate warnings - Fix POSIX compliance in shell scripts (((var++)) → $((var + 1))) Documentation: - CNR_VERSION_MANAGEMENT_DESIGN.md v1.0 → v1.1 with API priority rules - Add test guides and execution documentation (TESTING_PROMPT.md) - Add security-enhanced installation guide - Create CLI migration guides and references - Document package version management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
497 lines
18 KiB
Markdown
497 lines
18 KiB
Markdown
# Package Version Management Design
|
|
|
|
## Overview
|
|
|
|
ComfyUI Manager supports two package version types, each with distinct installation methods and version switching mechanisms:
|
|
|
|
1. **CNR Version (Archive)**: Production-ready releases with semantic versioning (e.g., v1.0.2), published to CNR server, verified, and distributed as ZIP archives
|
|
2. **Nightly Version**: Real-time development builds from Git repository without semantic versioning, providing direct access to latest code via git pull
|
|
|
|
## Package ID Normalization
|
|
|
|
### Case Sensitivity Handling
|
|
|
|
**Source of Truth**: Package IDs originate from `pyproject.toml` with their original case (e.g., `ComfyUI_SigmoidOffsetScheduler`)
|
|
|
|
**Normalization Process**:
|
|
1. `cnr_utils.normalize_package_name()` provides centralized normalization (`cnr_utils.py:28-48`):
|
|
```python
|
|
def normalize_package_name(name: str) -> str:
|
|
"""
|
|
Normalize package name for case-insensitive matching.
|
|
- Strip leading/trailing whitespace
|
|
- Convert to lowercase
|
|
"""
|
|
return name.strip().lower()
|
|
```
|
|
2. `cnr_utils.read_cnr_info()` uses this normalization when indexing (`cnr_utils.py:314`):
|
|
```python
|
|
name = project.get('name').strip().lower()
|
|
```
|
|
3. Package indexed in `installed_node_packages` with lowercase ID: `'comfyui_sigmoidoffsetscheduler'`
|
|
4. **Critical**: All lookups (`is_enabled()`, `unified_disable()`) must use `cnr_utils.normalize_package_name()` for matching
|
|
|
|
**Implementation** (`manager_core.py:1374, 1389`):
|
|
```python
|
|
# Before checking if package is enabled or disabling
|
|
packname_normalized = cnr_utils.normalize_package_name(packname)
|
|
if self.is_enabled(packname_normalized):
|
|
self.unified_disable(packname_normalized)
|
|
```
|
|
|
|
## Package Identification
|
|
|
|
### How Packages Are Identified
|
|
|
|
**Critical**: Packages MUST be identified by marker files and metadata, NOT by directory names.
|
|
|
|
**Identification Flow** (`manager_core.py:691-703`, `node_package.py:49-81`):
|
|
|
|
```python
|
|
def resolve_from_path(fullpath):
|
|
"""
|
|
Identify package type and ID using markers and metadata files.
|
|
|
|
Priority:
|
|
1. Check for .git directory (Nightly)
|
|
2. Check for .tracking + pyproject.toml (CNR)
|
|
3. Unknown/legacy (fallback to directory name)
|
|
"""
|
|
# 1. Nightly Detection
|
|
url = git_utils.git_url(fullpath) # Checks for .git/config
|
|
if url:
|
|
url = git_utils.compact_url(url)
|
|
commit_hash = git_utils.get_commit_hash(fullpath)
|
|
return {'id': url, 'ver': 'nightly', 'hash': commit_hash}
|
|
|
|
# 2. CNR Detection
|
|
info = cnr_utils.read_cnr_info(fullpath) # Checks for .tracking + pyproject.toml
|
|
if info:
|
|
return {'id': info['id'], 'ver': info['version']}
|
|
|
|
# 3. Unknown (fallback)
|
|
return None
|
|
```
|
|
|
|
### Marker-Based Identification
|
|
|
|
**1. Nightly Packages**:
|
|
- **Marker**: `.git` directory presence
|
|
- **ID Extraction**: Read URL from `.git/config` using `git_utils.git_url()` (`git_utils.py:34-53`)
|
|
- **ID Format**: Compact URL (e.g., `https://github.com/owner/repo` → compact form)
|
|
- **Why**: Git repositories are uniquely identified by their remote URL
|
|
|
|
**2. CNR Packages**:
|
|
- **Markers**: `.tracking` file AND `pyproject.toml` file (`.git` must NOT exist)
|
|
- **ID Extraction**: Read `name` from `pyproject.toml` using `cnr_utils.read_cnr_info()` (`cnr_utils.py:302-334`)
|
|
- **ID Format**: Normalized lowercase from `pyproject.toml` (e.g., `ComfyUI_Foo` → `comfyui_foo`)
|
|
- **Why**: CNR packages are identified by their canonical name in package metadata
|
|
|
|
**Implementation** (`cnr_utils.py:302-334`):
|
|
```python
|
|
def read_cnr_info(fullpath):
|
|
toml_path = os.path.join(fullpath, 'pyproject.toml')
|
|
tracking_path = os.path.join(fullpath, '.tracking')
|
|
|
|
# MUST have both markers and NO .git directory
|
|
if not os.path.exists(toml_path) or not os.path.exists(tracking_path):
|
|
return None # not valid CNR node pack
|
|
|
|
with open(toml_path, "r", encoding="utf-8") as f:
|
|
data = toml.load(f)
|
|
project = data.get('project', {})
|
|
name = project.get('name').strip().lower() # ← Normalized for indexing
|
|
original_name = project.get('name') # ← Original case preserved
|
|
version = str(manager_util.StrictVersion(project.get('version')))
|
|
|
|
return {
|
|
"id": name, # Normalized ID for lookups
|
|
"original_name": original_name,
|
|
"version": version,
|
|
"url": repository
|
|
}
|
|
```
|
|
|
|
### Why NOT Directory Names?
|
|
|
|
**Problem with directory-based identification**:
|
|
1. **Case Sensitivity Issues**: Same package can have different directory names
|
|
- Active: `ComfyUI_Foo` (original case)
|
|
- Disabled: `comfyui_foo@1_0_2` (lowercase)
|
|
2. **Version Suffix Confusion**: Disabled directories include version in name
|
|
3. **User Modifications**: Users can rename directories, breaking identification
|
|
|
|
**Correct Approach**:
|
|
- **Source of Truth**: Marker files (`.git`, `.tracking`, `pyproject.toml`)
|
|
- **Consistent IDs**: Based on metadata content, not filesystem names
|
|
- **Case Insensitive**: Normalized lookups work regardless of directory name
|
|
|
|
### Package Lookup Flow
|
|
|
|
**Index Building** (`manager_core.py:444-478`):
|
|
```python
|
|
def reload(self):
|
|
self.installed_node_packages: dict[str, list[InstalledNodePackage]] = defaultdict(list)
|
|
|
|
# Scan active packages
|
|
for x in os.listdir(custom_nodes_path):
|
|
fullpath = os.path.join(custom_nodes_path, x)
|
|
if x not in ['__pycache__', '.disabled']:
|
|
node_package = InstalledNodePackage.from_fullpath(fullpath, self.resolve_from_path)
|
|
# ↓ Uses ID from resolve_from_path(), NOT directory name
|
|
self.installed_node_packages[node_package.id].append(node_package)
|
|
|
|
# Scan disabled packages
|
|
for x in os.listdir(disabled_dir):
|
|
fullpath = os.path.join(disabled_dir, x)
|
|
node_package = InstalledNodePackage.from_fullpath(fullpath, self.resolve_from_path)
|
|
# ↓ Same ID extraction, consistent indexing
|
|
self.installed_node_packages[node_package.id].append(node_package)
|
|
```
|
|
|
|
**Lookup Process**:
|
|
1. Normalize search term: `cnr_utils.normalize_package_name(packname)`
|
|
2. Look up in `installed_node_packages` dict by normalized ID
|
|
3. Match found packages by version if needed
|
|
4. Return `InstalledNodePackage` objects with full metadata
|
|
|
|
### Edge Cases
|
|
|
|
**1. Package with `.git` AND `.tracking`**:
|
|
- **Detection**: Treated as Nightly (`.git` checked first)
|
|
- **Reason**: Git repo takes precedence over archive markers
|
|
- **Fix**: Remove `.tracking` file to avoid confusion
|
|
|
|
**2. Missing Marker Files**:
|
|
- **CNR without `.tracking`**: Treated as Unknown
|
|
- **Nightly without `.git`**: Treated as Unknown or CNR (if has `.tracking`)
|
|
- **Recovery**: Re-install package to restore correct markers
|
|
|
|
**3. Corrupted `pyproject.toml`**:
|
|
- **Detection**: `read_cnr_info()` returns `None`
|
|
- **Result**: Package treated as Unknown
|
|
- **Recovery**: Manual fix or re-install
|
|
|
|
## Version Types
|
|
|
|
ComfyUI Manager supports two main package version types:
|
|
|
|
### 1. CNR Version (Comfy Node Registry - Versioned Releases)
|
|
|
|
**Also known as**: Archive version (because it's distributed as ZIP archive)
|
|
|
|
**Purpose**: Production-ready releases that have been versioned, published to CNR server, and verified before distribution
|
|
|
|
**Characteristics**:
|
|
- Semantic versioning assigned (e.g., v1.0.2, v2.1.0)
|
|
- Published to CNR server with verification process
|
|
- Stable, tested releases for production use
|
|
- Distributed as ZIP archives for reliability
|
|
|
|
**Installation Method**: ZIP file extraction from CNR (Comfy Node Registry)
|
|
|
|
**Identification**:
|
|
- Presence of `.tracking` file in package directory
|
|
- **Directory naming**:
|
|
- **Active** (`custom_nodes/`): Uses `name` from `pyproject.toml` with original case (e.g., `ComfyUI_SigmoidOffsetScheduler`)
|
|
- This is the `original_name` in glob/ implementation
|
|
- **Disabled** (`.disabled/`): Uses `{package_name}@{version}` format (e.g., `comfyui_sigmoidoffsetscheduler@1_0_2`)
|
|
- Package indexed with lowercase ID from `pyproject.toml`
|
|
- Versioned releases (e.g., v1.0.2, v2.1.0)
|
|
|
|
**`.tracking` File Purpose**:
|
|
- **Primary**: Marker to identify this as a CNR/archive installation
|
|
- **Critical**: Contains list of original files from the archive
|
|
- **Update Use Case**: When updating to a new version:
|
|
1. Read `.tracking` to identify original archive files
|
|
2. Delete ONLY original archive files
|
|
3. Preserve user-generated files (configs, models, custom code)
|
|
4. Extract new archive version
|
|
5. Update `.tracking` with new file list
|
|
|
|
**File Structure**:
|
|
```
|
|
custom_nodes/
|
|
ComfyUI_SigmoidOffsetScheduler/
|
|
.tracking # List of original archive files
|
|
pyproject.toml # name = "ComfyUI_SigmoidOffsetScheduler"
|
|
__init__.py
|
|
nodes.py
|
|
(user-created files preserved during update)
|
|
```
|
|
|
|
### 2. Nightly Version (Development Builds)
|
|
|
|
**Purpose**: Real-time development builds from Git repository without semantic versioning
|
|
|
|
**Characteristics**:
|
|
- No semantic version assigned (version = "nightly")
|
|
- Direct access to latest development code
|
|
- Real-time updates via git pull
|
|
- For testing, development, and early adoption
|
|
- Not verified through CNR publication process
|
|
|
|
**Installation Method**: Git repository clone
|
|
|
|
**Identification**:
|
|
- Presence of `.git` directory in package directory
|
|
- `version: "nightly"` in package metadata
|
|
- **Directory naming**:
|
|
- **Active** (`custom_nodes/`): Uses `name` from `pyproject.toml` with original case (e.g., `ComfyUI_SigmoidOffsetScheduler`)
|
|
- This is the `original_name` in glob/ implementation
|
|
- **Disabled** (`.disabled/`): Uses `{package_name}@nightly` format (e.g., `comfyui_sigmoidoffsetscheduler@nightly`)
|
|
|
|
**Update Mechanism**:
|
|
- `git pull` on existing repository
|
|
- All user modifications in git working tree preserved by git
|
|
|
|
**File Structure**:
|
|
```
|
|
custom_nodes/
|
|
ComfyUI_SigmoidOffsetScheduler/
|
|
.git/ # Git repository marker
|
|
pyproject.toml
|
|
__init__.py
|
|
nodes.py
|
|
(git tracks all changes)
|
|
```
|
|
|
|
## Version Switching Mechanisms
|
|
|
|
### CNR ↔ Nightly (Uses `.disabled/` Directory)
|
|
|
|
**Mechanism**: Enable/disable toggling - only ONE version active at a time
|
|
|
|
**Process**:
|
|
1. **CNR → Nightly**:
|
|
```
|
|
Before: custom_nodes/ComfyUI_SigmoidOffsetScheduler/ (has .tracking)
|
|
After: custom_nodes/ComfyUI_SigmoidOffsetScheduler/ (has .git)
|
|
.disabled/comfyui_sigmoidoffsetscheduler@1_0_2/ (has .tracking)
|
|
```
|
|
- Move archive directory to `.disabled/comfyui_sigmoidoffsetscheduler@{version}/`
|
|
- Git clone nightly to `custom_nodes/ComfyUI_SigmoidOffsetScheduler/`
|
|
|
|
2. **Nightly → CNR**:
|
|
```
|
|
Before: custom_nodes/ComfyUI_SigmoidOffsetScheduler/ (has .git)
|
|
.disabled/comfyui_sigmoidoffsetscheduler@1_0_2/ (has .tracking)
|
|
After: custom_nodes/ComfyUI_SigmoidOffsetScheduler/ (has .tracking)
|
|
.disabled/comfyui_sigmoidoffsetscheduler@nightly/ (has .git)
|
|
```
|
|
- Move nightly directory to `.disabled/comfyui_sigmoidoffsetscheduler@nightly/`
|
|
- Restore archive from `.disabled/comfyui_sigmoidoffsetscheduler@{version}/`
|
|
|
|
**Key Points**:
|
|
- Both versions preserved in filesystem (one in `.disabled/`)
|
|
- Switching is fast (just move operations)
|
|
- No re-download needed when switching back
|
|
|
|
### CNR Version Update (In-Place Update)
|
|
|
|
**Mechanism**: Direct directory content update - NO `.disabled/` directory used
|
|
|
|
**When**: Switching between different CNR versions (e.g., v1.0.1 → v1.0.2)
|
|
|
|
**Process**:
|
|
```
|
|
Before: custom_nodes/ComfyUI_SigmoidOffsetScheduler/ (v1.0.1, has .tracking)
|
|
After: custom_nodes/ComfyUI_SigmoidOffsetScheduler/ (v1.0.2, has .tracking)
|
|
```
|
|
|
|
**Steps**:
|
|
1. Read `.tracking` to identify original v1.0.1 files
|
|
2. Delete only original v1.0.1 files (preserve user-created files)
|
|
3. Extract v1.0.2 archive to same directory
|
|
4. Update `.tracking` with v1.0.2 file list
|
|
5. Update `pyproject.toml` version metadata
|
|
|
|
**Critical**: Directory name and location remain unchanged
|
|
|
|
## API Design Decisions
|
|
|
|
### Enable/Disable Operations
|
|
|
|
**Design Decision**: ❌ **NO DIRECT ENABLE/DISABLE API PROVIDED**
|
|
|
|
**Rationale**:
|
|
- Enable/disable operations occur **ONLY as a by-product** of version switching
|
|
- Version switching is the primary operation that manages package state
|
|
- Direct enable/disable API would:
|
|
1. Create ambiguity about which version to enable/disable
|
|
2. Bypass version management logic
|
|
3. Lead to inconsistent package state
|
|
|
|
**Implementation**:
|
|
- `unified_enable()` and `unified_disable()` are **internal methods only**
|
|
- Called exclusively from version switching operations:
|
|
- `install_by_id()` (manager_core.py:1695-1724)
|
|
- `cnr_switch_version_instant()` (manager_core.py:941)
|
|
- `repo_update()` (manager_core.py:2144-2232)
|
|
|
|
**User Workflow**:
|
|
```
|
|
User wants to disable CNR version and enable Nightly:
|
|
✅ Correct: install(package, version="nightly")
|
|
→ automatically disables CNR, enables Nightly
|
|
❌ Wrong: disable(package) + enable(package, "nightly")
|
|
→ not supported, ambiguous
|
|
```
|
|
|
|
**Testing Approach**:
|
|
- Enable/disable tested **indirectly** through version switching tests
|
|
- Test 1-12 validate enable/disable behavior via install/update operations
|
|
- No direct enable/disable API tests needed (API doesn't exist)
|
|
|
|
## Implementation Details
|
|
|
|
### Version Detection Logic
|
|
|
|
**Location**: `comfyui_manager/common/node_package.py`
|
|
|
|
```python
|
|
@dataclass
|
|
class InstalledNodePackage:
|
|
@property
|
|
def is_nightly(self) -> bool:
|
|
return self.version == "nightly"
|
|
|
|
@property
|
|
def is_from_cnr(self) -> bool:
|
|
return not self.is_unknown and not self.is_nightly
|
|
```
|
|
|
|
**Detection Order**:
|
|
1. Check for `.tracking` file → CNR (Archive) version
|
|
2. Check for `.git` directory → Nightly version
|
|
3. Otherwise → Unknown/legacy
|
|
|
|
### Reload Timing
|
|
|
|
**Critical**: `unified_manager.reload()` must be called:
|
|
1. **Before each queued task** (`manager_server.py:1245`):
|
|
```python
|
|
# Reload installed packages before each task to ensure latest state
|
|
core.unified_manager.reload()
|
|
```
|
|
2. **Before version switching** (`manager_core.py:1370`):
|
|
```python
|
|
# Reload to ensure we have the latest package state before checking
|
|
self.reload()
|
|
```
|
|
|
|
**Why**: Ensures `installed_node_packages` dict reflects actual filesystem state
|
|
|
|
### Disable Mechanism
|
|
|
|
**Implementation** (`manager_core.py:982-1017`, specifically line 1011):
|
|
```python
|
|
def unified_disable(self, packname: str):
|
|
# ... validation logic ...
|
|
|
|
# Generate disabled directory name with version suffix
|
|
base_path = extract_base_custom_nodes_dir(matched_active.fullpath)
|
|
folder_name = packname if not self.is_url_like(packname) else os.path.basename(matched_active.fullpath)
|
|
to_path = os.path.join(base_path, '.disabled', f"{folder_name}@{matched_active.version.replace('.', '_')}")
|
|
|
|
shutil.move(matched_active.fullpath, to_path)
|
|
```
|
|
|
|
**Naming Convention**:
|
|
- `{folder_name}@{version}` format for ALL version types
|
|
- CNR v1.0.2 → `comfyui_foo@1_0_2` (dots replaced with underscores)
|
|
- Nightly → `comfyui_foo@nightly`
|
|
|
|
### Case Sensitivity Fix
|
|
|
|
**Problem**: Package IDs normalized to lowercase during indexing but not during lookup
|
|
|
|
**Solution** (`manager_core.py:1372-1378, 1388-1393`):
|
|
```python
|
|
# Normalize packname using centralized cnr_utils function
|
|
# CNR packages are indexed with lowercase IDs from pyproject.toml
|
|
packname_normalized = cnr_utils.normalize_package_name(packname)
|
|
|
|
if self.is_enabled(packname_normalized):
|
|
self.unified_disable(packname_normalized)
|
|
```
|
|
|
|
**Why Centralized Function**:
|
|
- Consistent normalization across entire codebase
|
|
- Single source of truth for package name normalization logic
|
|
- Easier to maintain and test
|
|
- Located in `cnr_utils.py:28-48`
|
|
|
|
## Directory Structure Examples
|
|
|
|
### Complete Example: All Version Types Coexisting
|
|
|
|
```
|
|
custom_nodes/
|
|
ComfyUI_SigmoidOffsetScheduler/ # Active version (CNR v2.0.0 in this example)
|
|
pyproject.toml # name = "ComfyUI_SigmoidOffsetScheduler"
|
|
__init__.py
|
|
nodes.py
|
|
|
|
.disabled/ # Inactive versions storage
|
|
comfyui_sigmoidoffsetscheduler@nightly/ # ← Nightly (disabled)
|
|
.git/ # ← Nightly marker
|
|
pyproject.toml
|
|
__init__.py
|
|
nodes.py
|
|
|
|
comfyui_sigmoidoffsetscheduler@1_0_2/ # ← CNR v1.0.2 (disabled)
|
|
.tracking # ← CNR marker with file list
|
|
pyproject.toml
|
|
__init__.py
|
|
nodes.py
|
|
|
|
comfyui_sigmoidoffsetscheduler@1_0_1/ # ← CNR v1.0.1 (disabled)
|
|
.tracking
|
|
pyproject.toml
|
|
__init__.py
|
|
nodes.py
|
|
```
|
|
|
|
**Key Points**:
|
|
- Active directory ALWAYS uses `original_name` without version suffix
|
|
- Each disabled version has `@{version}` suffix to avoid conflicts
|
|
- Multiple disabled versions can coexist (nightly + multiple CNR versions)
|
|
|
|
## Summary Table
|
|
|
|
| Version Type | Purpose | Marker | Active Directory Name | Disabled Directory Name | Update Method | Switch Mechanism |
|
|
|--------------|---------|--------|----------------------|------------------------|---------------|------------------|
|
|
| **CNR** (Archive) | Production-ready releases with semantic versioning, published to CNR server and verified | `.tracking` file | `original_name` (e.g., `ComfyUI_Foo`) | `{package}@{version}` (e.g., `comfyui_foo@1_0_2`) | In-place update (preserve user files) | `.disabled/` toggle |
|
|
| **Nightly** | Real-time development builds from Git repository without semantic versioning | `.git/` directory | `original_name` (e.g., `ComfyUI_Foo`) | `{package}@nightly` (e.g., `comfyui_foo@nightly`) | `git pull` | `.disabled/` toggle |
|
|
|
|
**Important Constraints**:
|
|
- **Active directory name**: MUST use `original_name` (from `pyproject.toml`) without version suffix
|
|
- Other code may depend on this specific directory name
|
|
- Only ONE version can be active at a time
|
|
- **Disabled directory name**: MUST include `@{version}` suffix to allow multiple disabled versions to coexist
|
|
- CNR: `@{version}` (e.g., `@1_0_2`)
|
|
- Nightly: `@nightly`
|
|
|
|
## Edge Cases
|
|
|
|
### 1. Multiple CNR Versions
|
|
- Each stored in `.disabled/` with version suffix
|
|
- Only one can be active at a time
|
|
- Switching between CNR versions = direct content update (not via `.disabled/`)
|
|
|
|
### 2. Package ID Case Variations
|
|
- Always normalize to lowercase for internal lookups
|
|
- Preserve original case in filesystem/display
|
|
- Match against lowercase indexed keys
|
|
|
|
### 3. Corrupted `.tracking` File
|
|
- Treat as unknown version type
|
|
- Warn user before update/uninstall
|
|
- May require manual cleanup
|
|
|
|
### 4. Mixed CNR + Nightly in `.disabled/`
|
|
- Both can coexist in `.disabled/`
|
|
- Only one can be active in `custom_nodes/`
|
|
- Switch logic detects type and handles appropriately
|