fix: symlink libpython for MuJoCo mjpython on macOS with uv#1503
fix: symlink libpython for MuJoCo mjpython on macOS with uv#1503spomichter merged 6 commits intodevfrom
Conversation
When Python is installed via uv, mjpython fails because it expects libpython at .venv/lib/ but uv places it in its own managed directory. Add a LibPythonConfiguratorMacOS system configurator that auto-creates the symlink during system setup before launching the MuJoCo subprocess.
Greptile SummaryThis PR introduces Key changes:
One logic issue remains: the Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[configure_system called] --> B{platform == Darwin?}
B -->|No| C[check returns True, skip]
B -->|Yes| D[Resolve real_lib from sys.executable]
D --> E[Glob libpython dylibs in real_lib]
E --> F{For each dylib: target in venv_lib?}
F -->|exists True| G[Skip, already present]
F -->|exists False AND is_symlink True| H[Warning: skipped broken symlink, check returns True, mjpython still fails]
F -->|exists False AND is_symlink False| I[Append to _missing]
I --> J[check returns False]
J --> K[explanation lists required symlinks]
K --> L{User approves?}
L -->|No| M[Exit, non-critical]
L -->|Yes| N[fix called]
N --> O[mkdir -p venv_lib]
O --> P{is_symlink at target?}
P -->|Yes| Q[unlink stale symlink]
P -->|No| R[symlink_to real_path]
Q --> R
R --> S{OSError?}
S -->|Yes| T[logger.warning, continue]
S -->|No| U[logger.warning Created symlink]
Last reviewed commit: dd50ff7 |
|
@greptile |
| for dylib in real_lib.glob("libpython*.dylib"): | ||
| target = venv_lib / dylib.name | ||
| if not target.exists() and not target.is_symlink(): | ||
| self._missing.append((target, dylib)) |
There was a problem hiding this comment.
Broken symlinks silently pass check(), leaving mjpython broken
When a broken symlink exists at target (e.g., after uv updates/moves the managed Python installation), target.exists() returns False but target.is_symlink() returns True. The current condition excludes it from _missing, so check() returns True (appears OK) — but mjpython will still fail because the symlink is dangling.
The guard in fix() (if symlink_path.is_symlink(): symlink_path.unlink()) is effectively dead code under the normal check() → fix() flow, since broken symlinks are never added to _missing.
To also detect and repair broken symlinks, change the condition to:
| for dylib in real_lib.glob("libpython*.dylib"): | |
| target = venv_lib / dylib.name | |
| if not target.exists() and not target.is_symlink(): | |
| self._missing.append((target, dylib)) | |
| for dylib in real_lib.glob("libpython*.dylib"): | |
| target = venv_lib / dylib.name | |
| if not target.exists(): | |
| self._missing.append((target, dylib)) |
This restores the original intent: exists() returns False for both absent paths and broken symlinks, so both cases are queued for repair. The existing if symlink_path.is_symlink(): symlink_path.unlink() guard in fix() already handles the unlink-before-recreate scenario correctly.
target.exists() follows symlinks and returns False for dangling ones, so the extra is_symlink() guard was silently skipping broken symlinks. The fix() method already handles unlinking stale symlinks before recreating them.
Problem
MuJoCo's
mjpythonfails withLibrary not loaded: libpython3.12.dylibwhen Python is installed viauv. The lib exists in uv's managed directory but mjpython looks in.venv/lib/.Solution
New
LibPythonConfiguratorMacOSthat auto-creates a symlink in the venv lib dir. Follows existing configurator pattern.Breaking Changes
None
Contributor License Agreement