Skip to content
Draft
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
e6f2f34
Fix #138: avoid creating duplicate link for addon
Mateusz-Grzelinski Jul 22, 2024
408442f
Rever capitalization of global vars
Mateusz-Grzelinski Jul 25, 2024
7c62eca
Support loading from externsion directory
Mateusz-Grzelinski Jul 25, 2024
6866abb
Add patch based tests
Mateusz-Grzelinski Jul 28, 2024
bf3d5a4
Fix tests and compability with python 3.7
Mateusz-Grzelinski Jul 28, 2024
99c7873
Fix addon reloading
Mateusz-Grzelinski Jul 29, 2024
869f0df
Fix type hint
Mateusz-Grzelinski Jul 29, 2024
74f9585
Fix logic for setting up addon links
Mateusz-Grzelinski Jul 29, 2024
3e9e7ee
Remove comment
Mateusz-Grzelinski Jul 29, 2024
b7d756d
Mock call to sys.path
Mateusz-Grzelinski Jul 30, 2024
f7b4f7e
Deduplicate patch calls
Mateusz-Grzelinski Jul 30, 2024
a0f99b6
Update comment
Mateusz-Grzelinski Jul 30, 2024
445778f
Extensions probably does not need sys.path
Mateusz-Grzelinski Jul 30, 2024
750fc41
Remove no longer needed gitignore
Mateusz-Grzelinski Jul 30, 2024
469435c
Fix missing directory
Mateusz-Grzelinski Jul 31, 2024
b803c97
Fix: prevent type hint from failing older verions of blender
Mateusz-Grzelinski Jul 31, 2024
c2e4330
Merge remote-tracking branch 'refs/remotes/jack/master' into master-n…
Mateusz-Grzelinski Aug 2, 2024
af8167a
Cover corner case with extension that can be an addon
Mateusz-Grzelinski Aug 2, 2024
21c4e78
Fix test and test new edge case
Mateusz-Grzelinski Aug 3, 2024
c6ab0fc
Remove link after blender exits
Mateusz-Grzelinski Aug 3, 2024
322cc2c
Disable loading from previous blender
Mateusz-Grzelinski Aug 3, 2024
c5feb7a
Send error notification when addon fails to load
Mateusz-Grzelinski Aug 3, 2024
bbe0c9e
Fix: sys.path was not handled correctly for fresh blender install
Mateusz-Grzelinski Aug 3, 2024
4794614
Fix: sys.path was not handled correctly for fresh blender install
Mateusz-Grzelinski Aug 3, 2024
702e4ea
Revert "Remove link after blender exits"
Mateusz-Grzelinski Aug 3, 2024
25b1454
Fix tests
Mateusz-Grzelinski Aug 3, 2024
95d7335
Merge branch 'refs/heads/master' into remove-link-on-exit
Mateusz-Grzelinski Aug 3, 2024
d82c598
Always print exception
Mateusz-Grzelinski Aug 3, 2024
236f5e8
Add VS code setting to enable legacy, no cleanup behaviour
Mateusz-Grzelinski Aug 4, 2024
4c6ef68
Remove debug print
Mateusz-Grzelinski Aug 4, 2024
ee612fe
Fix corner cases
Mateusz-Grzelinski Aug 8, 2024
3b29b68
Merge remote-tracking branch 'refs/remotes/jack/master' into master-c…
Mateusz-Grzelinski Aug 8, 2024
a95d6ee
Fix tests
Mateusz-Grzelinski Aug 9, 2024
50e1d01
Fix blender 2.8 compatibility
Mateusz-Grzelinski Aug 9, 2024
7377fe1
Update changelog and readme
Mateusz-Grzelinski Aug 11, 2024
e901876
Improve error message
Mateusz-Grzelinski Aug 11, 2024
5e5d0f9
Change ugly text rendering
Mateusz-Grzelinski Aug 11, 2024
1cc45ad
Fix blender 2.8 compability
Mateusz-Grzelinski Aug 11, 2024
e958c83
Update README
Mateusz-Grzelinski Aug 11, 2024
26972be
Update README
Mateusz-Grzelinski Aug 11, 2024
7a6c1e8
Update README
Mateusz-Grzelinski Aug 11, 2024
237de93
Update README
Mateusz-Grzelinski Aug 11, 2024
7c17ca3
Update README
Mateusz-Grzelinski Aug 12, 2024
fee5b4c
Resolve junction using windows cmd as fallback method of getting junc…
Mateusz-Grzelinski Aug 12, 2024
4e8601b
Fix tests and blender 2.8 compatibility
Mateusz-Grzelinski Aug 12, 2024
4f45939
Update readme
Mateusz-Grzelinski Aug 12, 2024
3e149d1
Use env var for passing VS code setting, disable uninstall operations
Mateusz-Grzelinski Aug 14, 2024
dea1282
Merge branch 'refs/heads/master' into remove-link-on-exit
Mateusz-Grzelinski Aug 14, 2024
95bb357
Fix test
Mateusz-Grzelinski Aug 14, 2024
6851545
Separate functionality to files, add optimize_defaults, rename files
Mateusz-Grzelinski Aug 14, 2024
5cf70f0
Update changelog
Mateusz-Grzelinski Aug 14, 2024
16a3fe4
Fix weird corner cases
Mateusz-Grzelinski Aug 14, 2024
15f2d4f
Merge remote-tracking branch 'refs/remotes/jack/master' into remove-l…
Mateusz-Grzelinski Aug 17, 2024
df1f3be
Merge branch 'refs/heads/master' into remove-link-on-exit
Mateusz-Grzelinski Sep 4, 2024
25a9bd9
Fix chenagelog, remove duplicate from readme
Mateusz-Grzelinski Sep 4, 2024
a943e6c
remove optimized defaults
Mateusz-Grzelinski Sep 4, 2024
058afca
rename var
Mateusz-Grzelinski Sep 4, 2024
ae4205d
Fix tests
Mateusz-Grzelinski Sep 4, 2024
ac24601
There is no need to disable extension remove
Mateusz-Grzelinski Sep 4, 2024
d5c51a9
disable_copy_settings_from_previous_version only on windows
Mateusz-Grzelinski Sep 4, 2024
0f576f9
Do not change default behavior
Mateusz-Grzelinski Sep 4, 2024
0730305
Document corner case
Mateusz-Grzelinski Sep 4, 2024
95cbcb3
Update redme
Mateusz-Grzelinski Sep 4, 2024
91fb1dd
Fix missing import
Mateusz-Grzelinski Sep 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### Added
- Disable "Load Previous settings" Blender feature during VS code session. Blender breaks when trying to copy link from previous version (#184)
- Windows only: disable addon uninstallation if addon is link because if will cause data loss on Windows (#184):
- Add option to make addon link/junction temporary [`blender.addon.keepAddonInstalled`](vscode://setting/blender.addon.keepAddonInstalled). If false addon will be available only during debug session (#184)
- Send notification if linking addon failed (#184)

## [0.0.22] - 2024-09-04

### Added
Expand Down
2 changes: 1 addition & 1 deletion EXTENSION-SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Manually remove links from locations:
- For older installations manually remove links from: `bpy.utils.user_resource("EXTENSIONS", path="user_default")`

> [!WARNING]
> Do not use Blender UI to uninstall addons:
> Do not use Blender UI to uninstall addons on Windows (or old Blender versions on Linux):
> - On windows uninstalling addon with Blender Preferences will result in data loss. It does not matter if your addon is linked or you are developing in directory that Blender recognizes by default (see above table).
> - On linux/mac from blender [2.80](https://projects.blender.org/blender/blender/commit/e6ba760ce8fda5cf2e18bf26dddeeabdb4021066) uninstalling **linked** addon with Blender Preferences is handled correctly. If you are developing in that Blender recognizes by default (see above table) data loss will occur.

Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@ You can develop your addon anywhere, VS Code will create a **permanent soft link
- VS code installs to local `vscode_development` extensions repository `Blender -> Preferences -> Get Extensions -> Repositories (dropdown, top right)`, see [`blender.addon.extensionsRepository`](vscode://settings/blender.addon.extensionsRepository)

> [!WARNING]
> In some cases uninstalling addon using Blender Preferences UI interface [might lead to data loss](./EXTENSION-SUPPORT.md#uninstall-addon-and-cleanup)
> Windows only: uninstalling addon using Blender Preferences UI interface [might lead to data loss](./EXTENSION-SUPPORT.md#uninstall-addon-and-cleanup).
>
> To prevent that from happening disable setting [`blender.addon.keepAddonInstalled`](vscode://setting/blender.addon.keepAddonInstalled).
> It will make you addon available only during VS Code debug session.

> [!WARNING]
> Windows only: after importing settings from previous Blender version you might experience permission denied error.
> Remove offending directories manually.
>
> To prevent that from happening disable setting [`blender.addon.keepAddonInstalled`](vscode://setting/blender.addon.keepAddonInstalled).

### How do I create a new addon?

Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@
],
"description": "Directory that contains the source code of the addon (used for path mapping for the debugger)."
},
"blender.addon.keepAddonInstalled": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "When false: keep the addon/extension installed only when running \"Blender: Start\" command (recommended).\nEnable to make link persistant (legacy behaviour). May cause broken links and problems with upgrading blender."
},
"blender.addon.moduleName": {
"type": "string",
"scope": "resource",
Expand Down
4 changes: 2 additions & 2 deletions pythonFiles/include/blender_vscode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ def startup(editor_address, addons_to_load: List[AddonInfo]):

from . import load_addons

path_mappings = load_addons.setup_addon_links(addons_to_load)
path_mappings, load_status = load_addons.setup_addon_links(addons_to_load)

from . import communication

communication.setup(editor_address, path_mappings)
communication.setup(editor_address, path_mappings, load_status)

from . import operators, ui

Expand Down
13 changes: 9 additions & 4 deletions pythonFiles/include/blender_vscode/communication.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import time
from typing import Callable, Dict

from typing import Callable, Dict, List
import flask
import debugpy
import random
import requests
import threading
from functools import partial
from .utils import run_in_main_thread

from .utils_blender import run_in_main_thread
from .environment import blender_path, scripts_folder, python_path

EDITOR_ADDRESS = None
Expand All @@ -18,13 +18,18 @@
post_handlers = {}


def setup(address: str, path_mappings):
def setup(address, path_mappings: List[Dict], load_status: List[Dict]):
global EDITOR_ADDRESS, OWN_SERVER_PORT, DEBUGPY_PORT
EDITOR_ADDRESS = address

OWN_SERVER_PORT = start_own_server()
DEBUGPY_PORT = start_debug_server()

for status in load_status:
send_dict_as_json(status)

if not path_mappings:
return
send_connection_information(path_mappings)

print("Waiting for debug client.")
Expand Down
10 changes: 5 additions & 5 deletions pythonFiles/include/blender_vscode/environment.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import os
import sys
from pathlib import Path
from typing import Optional

import bpy
import sys
import addon_utils
from pathlib import Path
import platform
import os
import bpy

# binary_path_python was removed in blender 2.92
# but it is the most reliable way of getting python path for older versions
Expand All @@ -19,3 +18,4 @@
addon_directories = tuple(map(Path, addon_utils.paths()))

EXTENSIONS_REPOSITORY: Optional[str] = os.environ.get("VSCODE_EXTENSIONS_REPOSITORY", "user_default") or "user_default"
KEEP_ADDON_INSTALLED: bool = os.environ.get("VSCODE_KEEP_ADDON_INSTALLED") == "yes"
111 changes: 52 additions & 59 deletions pythonFiles/include/blender_vscode/load_addons.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import atexit
import os
import subprocess
import sys
import traceback
from pathlib import Path
from typing import List, Union, Optional, Dict
from typing import List, Tuple, Union, Optional, Dict

import bpy

from . import AddonInfo
from .communication import send_dict_as_json
from .environment import addon_directories, EXTENSIONS_REPOSITORY
from .utils import is_addon_legacy, addon_has_bl_info
from .environment import addon_directories, KEEP_ADDON_INSTALLED, EXTENSIONS_REPOSITORY
from .modify_blender import (
disable_copy_settings_from_previous_version,
disable_addon_remove,
)
from .utils_blender import is_addon_legacy, addon_has_bl_info
from .utils_files import resolve_link

if bpy.app.version >= (4, 2, 0):
_EXTENSIONS_DEFAULT_DIR = Path(bpy.utils.user_resource("EXTENSIONS", path=EXTENSIONS_REPOSITORY))
Expand All @@ -19,9 +24,7 @@
_ADDONS_DEFAULT_DIR = Path(bpy.utils.user_resource("SCRIPTS", path="addons"))


def setup_addon_links(addons_to_load: List[AddonInfo]) -> List[Dict]:
path_mappings: List[Dict] = []

def setup_addon_links(addons_to_load: List[AddonInfo]) -> Tuple[List[Dict], List[Dict]]:
# always make sure addons are in path, important when running fresh blender install
# do it always to avoid very confusing logic in the loop below
os.makedirs(_ADDONS_DEFAULT_DIR, exist_ok=True)
Expand All @@ -33,6 +36,13 @@ def setup_addon_links(addons_to_load: List[AddonInfo]) -> List[Dict]:
ensure_extension_repo_exists(EXTENSIONS_REPOSITORY)
remove_broken_extension_links()

if sys.platform == "win32":
# todo disable copy settings only when there is a junction in blender config folder
disable_copy_settings_from_previous_version()
disable_addon_remove()

path_mappings: List[Dict] = []
load_status: List[Dict] = []
for addon_info in addons_to_load:
try:
load_path = _link_addon_or_extension(addon_info)
Expand All @@ -44,11 +54,14 @@ def setup_addon_links(addons_to_load: List[AddonInfo]) -> List[Dict]:
- Windows only: You upgraded Blender version and imported old setting. Now links became real directories.
- Path is a real directory with the same name as addon (removing might cause data loss!)"""
)
raise e
load_status.append({"type": "enableFailure", "addonPath": str(addon_info.load_dir)})
except Exception:
traceback.print_exc()
load_status.append({"type": "enableFailure", "addonPath": str(addon_info.load_dir)})
else:
path_mappings.append({"src": str(addon_info.load_dir), "load": str(load_path)})

return path_mappings
return path_mappings, load_status


def _link_addon_or_extension(addon_info: AddonInfo) -> Path:
Expand All @@ -59,7 +72,7 @@ def _link_addon_or_extension(addon_info: AddonInfo) -> Path:
else: # addon is in external dir or is in extensions dir
_remove_duplicate_addon_links(addon_info)
load_path = _ADDONS_DEFAULT_DIR / addon_info.module_name
create_link_in_user_addon_directory(addon_info.load_dir, load_path)
create_link(addon_info.load_dir, load_path)
else:
if addon_has_bl_info(addon_info.load_dir) and is_in_any_addon_directory(addon_info.load_dir):
# this addon is compatible with legacy addons and extensions
Expand All @@ -73,7 +86,7 @@ def _link_addon_or_extension(addon_info: AddonInfo) -> Path:
_remove_duplicate_extension_links(addon_info)
os.makedirs(_EXTENSIONS_DEFAULT_DIR, exist_ok=True)
load_path = _EXTENSIONS_DEFAULT_DIR / addon_info.module_name
create_link_in_user_addon_directory(addon_info.load_dir, load_path)
create_link(addon_info.load_dir, load_path)
return load_path


Expand All @@ -95,53 +108,11 @@ def _remove_duplicate_extension_links(addon_info: AddonInfo):
existing_extension_with_the_same_target = does_extension_link_exist(addon_info.load_dir)


def _resolve_link_windows_cmd(path: Path) -> Optional[str]:
IO_REPARSE_TAG_MOUNT_POINT = "0xa0000003"
JUNCTION_INDICATOR = f"Reparse Tag Value : {IO_REPARSE_TAG_MOUNT_POINT}"
try:
output = subprocess.check_output(["fsutil", "reparsepoint", "query", str(path)])
except subprocess.CalledProcessError:
return None
output_lines = output.decode().split(os.linesep)
if not output_lines[0].startswith(JUNCTION_INDICATOR):
return None
TARGET_PATH_INDICATOR = "Print Name: "
for line in output_lines:
if line.startswith(TARGET_PATH_INDICATOR):
possible_target = line[len(TARGET_PATH_INDICATOR) :]
if os.path.exists(possible_target):
return possible_target


def _resolve_link(path: Path) -> Optional[str]:
"""Return target if is symlink or junction"""
try:
return os.readlink(str(path))
except OSError as e:
# OSError: [WinError 4390] The file or directory is not a reparse point
if sys.platform == "win32":
if e.winerror == 4390:
return None
else:
# OSError: [Errno 22] Invalid argument: '/snap/blender/5088/4.2/extensions/system/readme.txt'
if e.errno == 22:
return None
print("Warning: can not resolve link target", e)
return None
except ValueError as e:
# there are major differences in python windows junction support (3.7.0 and 3.7.9 give different errors)
if sys.platform == "win32":
return _resolve_link_windows_cmd(path)
else:
print("Warning: can not resolve link target", e)
return None


def does_addon_link_exist(development_directory: Path) -> Optional[Path]:
"""Search default addon path and return first path that links to `development_directory`"""
for file in os.listdir(_ADDONS_DEFAULT_DIR):
existing_addon_dir = Path(_ADDONS_DEFAULT_DIR, file)
target = _resolve_link(existing_addon_dir)
target = resolve_link(existing_addon_dir)
if target:
windows_being_windows = target.lstrip(r"\\?")
if Path(windows_being_windows) == Path(development_directory):
Expand All @@ -150,7 +121,7 @@ def does_addon_link_exist(development_directory: Path) -> Optional[Path]:


def does_extension_link_exist(development_directory: Path) -> Optional[Path]:
"""Search all available extension paths and return path that links to `development_directory"""
"""Search all available extension paths and return path that links to `development_directory`"""
for repo in bpy.context.preferences.extensions.repos:
if not repo.enabled:
continue
Expand All @@ -159,7 +130,7 @@ def does_extension_link_exist(development_directory: Path) -> Optional[Path]:
continue # repo dir might not exist
for file in os.listdir(repo_dir):
existing_extension_dir = Path(repo_dir, file)
target = _resolve_link(existing_extension_dir)
target = resolve_link(existing_extension_dir)
if target:
windows_being_windows = target.lstrip(r"\\?")
if Path(windows_being_windows) == Path(development_directory):
Expand All @@ -181,7 +152,7 @@ def remove_broken_addon_links():
addon_dir = _ADDONS_DEFAULT_DIR / file
if not addon_dir.is_dir():
continue
target = _resolve_link(addon_dir)
target = resolve_link(addon_dir)
if target and not os.path.exists(target):
print("INFO: Removing invalid link:", addon_dir, "->", target)
os.remove(addon_dir)
Expand All @@ -197,7 +168,7 @@ def remove_broken_extension_links():
continue
for file in os.listdir(repo_dir):
existing_extension_dir = repo_dir / file
target = _resolve_link(existing_extension_dir)
target = resolve_link(existing_extension_dir)
if target and not os.path.exists(target):
print("INFO: Removing invalid link:", existing_extension_dir, "->", target)
os.remove(existing_extension_dir)
Expand All @@ -213,6 +184,11 @@ def load(addons_to_load: List[AddonInfo]):
# but user is developing it in addon directory. Treat it as addon.
bpy.ops.preferences.addon_refresh()
addon_name = addon_info.module_name
elif addon_has_bl_info(addon_info.load_dir) and is_in_any_addon_directory(addon_info.load_dir):
# this addon is compatible with legacy addons and extensions
# but user is developing it in addon directory. Treat it as addon.
bpy.ops.preferences.addon_refresh()
addon_name = addon_info.module_name
else:
bpy.ops.extensions.repo_refresh_all()
addon_name = "bl_ext." + EXTENSIONS_REPOSITORY + "." + addon_info.module_name
Expand All @@ -224,7 +200,7 @@ def load(addons_to_load: List[AddonInfo]):
send_dict_as_json({"type": "enableFailure", "addonPath": str(addon_info.load_dir)})


def create_link_in_user_addon_directory(directory: Union[str, os.PathLike], link_path: Union[str, os.PathLike]):
def create_link(directory: Union[str, os.PathLike], link_path: Union[str, os.PathLike]):
if os.path.exists(link_path):
os.remove(link_path)

Expand All @@ -235,6 +211,23 @@ def create_link_in_user_addon_directory(directory: Union[str, os.PathLike], link
else:
os.symlink(str(directory), str(link_path), target_is_directory=True)

if KEEP_ADDON_INSTALLED:
return

def cleanup():
if not os.path.exists(link_path):
return
try:
print(f'INFO: Cleanup: remove link: "{link_path}"')
os.remove(link_path)
except PermissionError as ex:
print(
f'ERROR: Could not remove path "{link_path}" due to insufficient permission. Please remove it manually.'
)
raise ex

atexit.register(cleanup)


def is_in_any_addon_directory(module_path: Path) -> bool:
for path in addon_directories:
Expand Down
Loading