├── .gitignore ├── LICENSE ├── README.md └── startup └── __BLENDER_STUDIO_SCRIPTS__.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Frieder Erdmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blender_studio_scripts 2 | Add support for multiple script locations 3 | 4 | ## Usage 5 | - Set `BLENDER_USER_SCRIPTS` to the location of this repository (or your own structure with the `startup` directory contents copied over). 6 | - Set `BLENDER_STUDIO_SCRIPTS`, which can contain many paths (like C:\my_studio;D:\my_project;E:\yet_another_Blender_scripts_folder). 7 | 8 | Contributions welcome! 9 | -------------------------------------------------------------------------------- /startup/__BLENDER_STUDIO_SCRIPTS__.py: -------------------------------------------------------------------------------- 1 | """BLENDER STUDIO SCRIPTS 2 | 3 | This file is meant to be in /startup/ (e.g. of the location defined in BLENDER_USER_SCRIPTS) 4 | 5 | It parses the environment variable BLENDER_STUDIO_SCRIPTS, which supports multiple paths 6 | 7 | Currently supported 8 | - Users can still locally install addons 9 | - Multiple addon locations 10 | - Multiple module locations 11 | - Multiple startup script locations 12 | """ 13 | 14 | import os 15 | import sys 16 | import pkgutil 17 | import importlib 18 | import inspect 19 | from typing import List 20 | from types import ModuleType 21 | 22 | 23 | BLENDER_STUDIO_SCRIPTS = "BLENDER_STUDIO_SCRIPTS" 24 | SEPARATOR = ";" 25 | STARTUP_MODULES = [] 26 | 27 | 28 | def fix_default_addon_path(): 29 | """This method patches the default install location from the addons preferences 30 | to install to the user's app data rather than to BLENDER_USER_SCRIPTS (our entry point) 31 | """ 32 | import bpy 33 | import _bpy 34 | old_function = bpy.utils.user_resource 35 | 36 | real_default = os.path.join(_bpy.resource_path("USER"), 'scripts', 'addons') 37 | 38 | def patched_function(resource_type, *, path="", create=False): 39 | current_frame = inspect.currentframe() 40 | caller_frame = inspect.getouterframes(current_frame, 2) 41 | outer_frame = caller_frame[1] 42 | method_name = outer_frame.function 43 | file_path = os.path.normpath(outer_frame.filename) 44 | compare = os.path.normpath(os.path.join('scripts', 'startup', 'bl_operators', 'userpref.py')) 45 | if method_name == 'execute' and file_path.endswith(compare): 46 | return real_default 47 | return old_function(resource_type, path=path, create=create) 48 | 49 | bpy.utils.user_resource = patched_function 50 | 51 | 52 | def add_module_path(base_path: str): 53 | module_path = os.path.join(base_path, "modules") 54 | if os.path.exists(module_path) and os.path.isdir(module_path): 55 | sys.path.append(module_path) 56 | 57 | 58 | def add_addon_paths(base_paths: List[str]): 59 | if not base_paths: 60 | return 61 | 62 | import addon_utils 63 | standard_paths = addon_utils.paths 64 | 65 | def patched_paths(): 66 | addon_paths = standard_paths() 67 | for base_path in base_paths: 68 | addon_path = os.path.join(base_path, "addons") 69 | if os.path.exists(addon_path) and os.path.isdir(addon_path): 70 | addon_paths.append(addon_path) 71 | return addon_paths 72 | 73 | addon_utils.paths = patched_paths 74 | 75 | 76 | def import_startup_scripts(base_path: str) -> List[ModuleType]: 77 | startup = os.path.join(base_path, "startup") 78 | 79 | if os.path.exists(startup) and os.path.isdir(startup): 80 | sys.path.insert(0, startup) 81 | 82 | module_list = list(pkgutil.iter_modules([startup])) 83 | modules = [] 84 | 85 | for module_info in module_list: 86 | module = importlib.import_module(module_info.name) 87 | modules.append(module) 88 | 89 | return modules 90 | 91 | 92 | def register_startup_scripts(): 93 | for module in STARTUP_MODULES: 94 | if hasattr(module, 'register'): 95 | module.register() 96 | 97 | 98 | def get_studio_paths(): 99 | env_variable = os.environ.get(BLENDER_STUDIO_SCRIPTS, "") 100 | 101 | if not env_variable: 102 | return [] 103 | 104 | return [entry.strip() for entry in env_variable.split(SEPARATOR)] 105 | 106 | 107 | fix_default_addon_path() 108 | 109 | base_paths = get_studio_paths() 110 | 111 | add_addon_paths(base_paths) 112 | 113 | for base_path in base_paths: 114 | add_module_path(base_path) 115 | 116 | for base_path in base_paths: 117 | STARTUP_MODULES += import_startup_scripts(base_path) 118 | 119 | 120 | def register(): 121 | register_startup_scripts() 122 | --------------------------------------------------------------------------------