Skip to content

dev_environment.py: a locked file during symlinking leaves the extension half-linked with no recovery hint #9239

Description

@theoryshaw

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    BonsaiPR ProposedA PR has been proposed for this bug/feature.Severity:MinorOptimisation, warning, inconsistency, accommodate invalid data

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions