When dev_environment.py fails while replacing a package directory with a symlink, it aborts on a raw traceback and leaves the extension in a partially-linked state that Blender cannot import. Nothing in the output indicates what went wrong or that re-running the script fixes it.
What happened
Blender 4.5, Bonsai installed minutes earlier via offline installation:
Symlinking extension to the git repo...
Linking ...\extensions\user_default\bonsai\__init__.py -> ...\src\bonsai\bonsai\__init__.py.
Linking ...\site-packages\bonsai -> ...\src\bonsai\bonsai.
Linking ...\site-packages\ifcopenshell -> ...\src\ifcopenshell-python\ifcopenshell.
Traceback (most recent call last):
File "...\dev_environment.py", line 229, in <module>
main()
File "...\dev_environment.py", line 192, in main
shutil.rmtree(path)
File "...\shutil.py", line 808, in rmtree
return _rmtree_unsafe(path, onexc)
File "...\shutil.py", line 636, in _rmtree_unsafe
onexc(os.unlink, fullname, err)
File "...\shutil.py", line 634, in _rmtree_unsafe
os.unlink(fullname)
PermissionError: [WinError 5] Access is denied: '...\site-packages\ifcopenshell\_ifcopenshell_wrapper.cp311-win_amd64.pyd'
rmtree had already deleted all but 10 files of site-packages/ifcopenshell before hitting the 54 MB .pyd, and the symlink was never created — so import ifcopenshell was broken in Blender until the script was run again.
Investigating afterwards, the lock was transient and already gone: no process had the .pyd loaded as a module, Blender was not running, the file was not read-only (Archive only), the ACL granted FullControl, and opening it exclusively for ReadWrite with no sharing succeeded. Re-running the script completed without any intervention. The extension had been installed at 08:39 and the script run immediately after, so an antivirus scan of freshly written native binaries is the likely holder. (Deleting a file held as a mapped image or by a scanner returns WinError 5, not the WinError 32 one might expect.)
Why this is worth handling
https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.8.0/src/bonsai/scripts/dev_environment.py#L175-L187
for path, dest in symlinks:
print(f"Linking {path} -> {dest}.")
if path.is_dir():
if path.is_symlink():
path.unlink()
else:
shutil.rmtree(path)
rmtree is only reachable when the target is still a real directory — that is, on the first run after installing the extension. Every later run finds a symlink and takes path.unlink() instead. So the one branch with no error handling is also the one that always runs while the extension's binaries are newly written and most likely to be held by a scanner. New contributors following the setup instructions hit it under precisely the worst conditions, on their first attempt.
Suggested changes
-
Retry rmtree on PermissionError with a short backoff, rather than aborting on the first failure. In this case the lock cleared on its own well within a few seconds.
-
Report the partial state and the remedy. Wrapping the symlink loop so a failure prints something like:
Failed while replacing <path>. The extension is now partially linked and Blender will not import it until this completes. This is usually a file lock (Blender running, or an antivirus scanning a fresh install) — re-running this script is safe and will finish the job.
would have made the situation self-explanatory. The recovery genuinely is just "run it again", but that is not discoverable from a shutil traceback.
-
Preflight check that Blender is not running. It was not the cause here, but it is the most common cause of a locked file at this step and is cheap to detect.
-
Minor, while in the area: rmtree on Windows also fails on read-only files, which an onexc handler that clears the read-only bit and retries would cover.
Worth preserving deliberately: the script copies the *_wrapper* binaries into the repo before any deletion happens, which is why nothing was actually lost when this aborted. That ordering is load-bearing and could use a comment saying so.
Environment: Windows 10, Blender 4.5, Bonsai installed via offline installation (extensions/user_default/bonsai). Unrelated to #9227 / #9228, which cover PACKAGE_PATH detection in the same script.
Happy to open a PR for 1 and 2 if the approach looks right.
When
dev_environment.pyfails while replacing a package directory with a symlink, it aborts on a raw traceback and leaves the extension in a partially-linked state that Blender cannot import. Nothing in the output indicates what went wrong or that re-running the script fixes it.What happened
Blender 4.5, Bonsai installed minutes earlier via offline installation:
rmtreehad already deleted all but 10 files ofsite-packages/ifcopenshellbefore hitting the 54 MB.pyd, and the symlink was never created — soimport ifcopenshellwas broken in Blender until the script was run again.Investigating afterwards, the lock was transient and already gone: no process had the
.pydloaded as a module, Blender was not running, the file was not read-only (Archiveonly), the ACL grantedFullControl, and opening it exclusively forReadWritewith no sharing succeeded. Re-running the script completed without any intervention. The extension had been installed at 08:39 and the script run immediately after, so an antivirus scan of freshly written native binaries is the likely holder. (Deleting a file held as a mapped image or by a scanner returnsWinError 5, not theWinError 32one might expect.)Why this is worth handling
https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.8.0/src/bonsai/scripts/dev_environment.py#L175-L187
rmtreeis only reachable when the target is still a real directory — that is, on the first run after installing the extension. Every later run finds a symlink and takespath.unlink()instead. So the one branch with no error handling is also the one that always runs while the extension's binaries are newly written and most likely to be held by a scanner. New contributors following the setup instructions hit it under precisely the worst conditions, on their first attempt.Suggested changes
Retry
rmtreeonPermissionErrorwith a short backoff, rather than aborting on the first failure. In this case the lock cleared on its own well within a few seconds.Report the partial state and the remedy. Wrapping the symlink loop so a failure prints something like:
would have made the situation self-explanatory. The recovery genuinely is just "run it again", but that is not discoverable from a
shutiltraceback.Preflight check that Blender is not running. It was not the cause here, but it is the most common cause of a locked file at this step and is cheap to detect.
Minor, while in the area:
rmtreeon Windows also fails on read-only files, which anonexchandler that clears the read-only bit and retries would cover.Worth preserving deliberately: the script copies the
*_wrapper*binaries into the repo before any deletion happens, which is why nothing was actually lost when this aborted. That ordering is load-bearing and could use a comment saying so.Environment: Windows 10, Blender 4.5, Bonsai installed via offline installation (
extensions/user_default/bonsai). Unrelated to #9227 / #9228, which coverPACKAGE_PATHdetection in the same script.Happy to open a PR for 1 and 2 if the approach looks right.