Skip to content

Fix: pyvenv.cfg corruption and wrong Python distribution selection when multiple Python versions are installed (3.13 after 3.12) - #1698

Draft
NeuralFault wants to merge 5 commits into
LykosAI:mainfrom
NeuralFault:fix/uv-fallback-contains-matches-wrong-version
Draft

Fix: pyvenv.cfg corruption and wrong Python distribution selection when multiple Python versions are installed (3.13 after 3.12)#1698
NeuralFault wants to merge 5 commits into
LykosAI:mainfrom
NeuralFault:fix/uv-fallback-contains-matches-wrong-version

Conversation

@NeuralFault

@NeuralFault NeuralFault commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

Installing forge-neo (requires Python 3.13.12) alongside an existing WebUI package installation (using Python 3.12.10) silently corrupts that pre-existing package's pyvenv.cfg, causing error no: 2 at launch. Manually correcting the file has no effect as it is rewritten incorrectly on every subsequent launch.

Steps to reproduce:

  1. Install ComfyUI via Stability Matrix. Python 3.12.10 is installed to Data/Assets/Python/cpython-3.12.10-... and a venv is created with a correct pyvenv.cfg
  2. Install forge-neo. Python 3.13.12 is installed to Data/Assets/Python/cpython-3.13.12-... and its own venv is created correctly
  3. Launch ComfyUI. Its pyvenv.cfg now has base-prefix, base-exec-prefix, and base-executable pointing to the 3.13.12 distribution, while home remains the original 3.12.10 path
  4. ComfyUI fails to start because the venv's Python interpreter cannot resolve the mixed paths

Root cause (three compounding bugs)

Bug A: Fallback directory scanner matches wrong version (UvManager.cs):

When UV's python list fails and the fallback scanner runs, Contains("3.12") matches both cpython-3.12.10-... and cpython-3.13.12-... (the substring "3.12" appears in "3.13.12").
Results are ordered by CreationTimeUtc descending, so the more recently installed 3.13.12 directory is selected as the "discovered" 3.12.10 installation.

Bug B: installedOnly parameter is dead code (UvManager.cs):

ListAvailablePythonsAsync(installedOnly: true) never filters to installed-only entries.
Uninstalled Python entries with Path = null produce an empty InstallPath, which throws ArgumentException in PyInstallation's constructor, aborting the entire UV discovery loop via the catch-all in GetAllInstallationsAsync. This pushes the system into Bug A's fallback path.

Bug C: ConfigParser silently fails on pre-existing home key
(PyVenvRunner.cs / UvVenvRunner.cs):

The SetPyvenvCfg method prepends [top] to make the sectionless pyvenv.cfg parseable by Salaros.Configuration.ConfigParser, then calls SetValue("top", "home", ...). The ConfigParser silently refuses to update the existing home key while successfully adding the new keys (base-prefix, base-exec-prefix, base-executable), producing the mixed-path config with home at 3.12 and the other three at 3.13.

Changes

StabilityMatrix.Core/Python/PyVenvConfigHelper.cs (new file)

Replaces the ConfigParser roundtrip with a direct line-by-line key=value reader/writer.
Extracts the key before = on each line, compares with exact Equals, updates matching keys, and appends missing ones.
Preserves all non-path keys in their original order. This eliminates the fragile [top] section-header hack, the silent key-update failure, and the dependency on a third-party INI parser for a format that is not INI.

Why a new helper instead of fixing ConfigParser:

  • pyvenv.cfg is a simple key = value format with no sections, no quoting, and no escaping. A section-based INI parser adds indirection without adding value.
  • The prepend-[top] → parse → SetValueToString() → strip-[top] roundtrip has three fragility points: the section injection, the key update semantics on a sectionless file, and the section removal via Replace.
  • The direct approach is ~70 lines of straightforward string manipulation vs. depending on a NuGet package that was only used at this one call site across the entire codebase.

StabilityMatrix.Core/Python/PyVenvRunner.cs

SetPyvenvCfg reduced from a 15-line ConfigParser roundtrip to a 3-line call to PyVenvConfigHelper.WritePyVenvCfg. Removed using Salaros.Configuration.

StabilityMatrix.Core/Python/UvVenvRunner.cs

Identical change to PyVenvRunner.cs. Removed using Salaros.Configuration.

StabilityMatrix.Core/Python/UvManager.cs

  • ListAvailablePythonsAsync: The installedOnly parameter now actually filters when true, entries with Path == null are excluded. Explicit null check on e.Path in the Select projection rather than a null-forgiving operator.
  • InstallPythonVersionAsync fallback scanner: Contains("3.12") replaced with Contains("3.12.") plus an EndsWith("-3.12")` fallback for PyPy-style directory names. The trailing dot prevents substring collision with higher versions.

NeuralFault and others added 5 commits July 29, 2026 13:00
…/writer

- Remove dependency on Salaros.Configuration.ConfigParser for pyvenv.cfg serialization in both PyVenvRunner and UvVenvRunner SetPyvenvCfg methods
- Adds PyVenvConfigHelper.WritePyVenvCfg that reads, updates, and writes the key=value lines directly without section-header round-tripping
- Fixes silent failure where ConfigParser.SetValue would not update the existing "home" key in a sectionless INI file, while successfully adding new keys (base-prefix, base-exec-prefix, base-executable), producing a corrupt config with mixed Python distribution paths
- Preserve all non-path keys (include-system-site-packages, version, executable, command, etc.) in their original line order
- Append missing path keys if the venv was created by an older version that did not write them
…re installed

- Fix fallback directory scanner in UvManager.InstallPythonVersionAsync using Contains("3.12") which also matched "3.13.12" directory names, causing the wrong Python distribution to be selected when UV listing failed and the newer 3.13 installation had a more recent creation timestamp
- Switched to strict version prefix matching ("3.12.") with an EndsWith fallback for edge cases like "pypy-3.12" naming
- Fix installedOnly parameter in ListAvailablePythonsAsync being ignored, causing uninstalled Python entries with null Path to reach the PyInstallation constructor and throw ArgumentException, which aborted the entire UV discovery loop via the catch-all in GetAllInstallationsAsync
- Wire PyVenvConfigHelper.WritePyVenvCfg into PyVenvRunner and UvVenvRunner SetPyvenvCfg, replacing the Salaros.Configuration.ConfigParser round-trip that silently failed to update the existing "home" key
- Remove unused Salaros.Configuration using directives from both runner files
- Parse each line into key and value by splitting on '=', then compare the
  key with ordinal case-insensitive Equals rather than StartsWith
- Preserve lines with no '=' delimiter as-is
- Eliminates ordering dependency between key checks. Each key is now
  matched exactly and independently, so reordering the checks or adding a
  new key like "base" cannot silently swallow "base-prefix" or
  "base-executable" through prefix collision
… UvManager

- When installedOnly is false the preceding Where clause allows e.Path to be
  null, making the null-forgiving operator (!) semantically incorrect and
  misleading
- Replace with a conditional that uses Path.GetDirectoryName only when
  e.Path is non-null, falling back to string.Empty otherwise
@NeuralFault NeuralFault changed the title Fix: pyvenv.cfg corruption and wrong Python distribution selection when multiple Python versions are installed Fix: pyvenv.cfg corruption and wrong Python distribution selection when multiple Python versions are installed (3.13 after 3.12) Jul 29, 2026
@NeuralFault

Copy link
Copy Markdown
Contributor Author

@mohnjiles @ionite34 can also remove the salaros reference in the package.prop and csproj files.

The NuGet package still gets pulled during restore but its DLL is left out of the output as of this PR if merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant