├── .github └── workflows │ └── BuildReleases.yml ├── .gitignore ├── ReadMe.md └── __init__.py /.github/workflows/BuildReleases.yml: -------------------------------------------------------------------------------- 1 | # Build a release containing .zip of the (filtered) contents of the repository 2 | # when a new tag is pushed with a semantic versioning format. 3 | name: Build Releases 4 | 5 | on: 6 | push: 7 | tags: ["v[0-9]+.[0-9]+.[0-9]+"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Check-out the repository under: $GITHUB_WORKSPACE/ 14 | # Note: While this makes zipping the repository with a root folder easier, 15 | # all 'gh' commands must be executed after cd'ing into the sub-directory. 16 | - uses: actions/checkout@v3 17 | with: 18 | path: ${{ github.event.repository.name }} 19 | 20 | # Create a zip of the repository. 21 | - name: Zip Repository (excludes .git*) 22 | run: | 23 | # Zip the contents underneath a root directory (the repository's name) as 24 | # required by Blender for /__init__.py style add-ons. 25 | # See below for single-file add-ons that do not need a root directory. 26 | zip -r ${{ github.event.repository.name }}.zip \ 27 | ${{ github.event.repository.name }} \ 28 | -x "${{ github.event.repository.name }}/.git*" 29 | 30 | # If a root folder is not desired, use the following and comment out the above. 31 | #cd ${{ github.event.repository.name }} 32 | #zip -r ../${{ github.event.repository.name }}.zip . -x ".git*" 33 | 34 | # Create a new release using the tag name or commit id. 35 | - name: Create versioned build with filtered zip file. 36 | run: | 37 | # Change into the repository directory since it was checked out to a sub folder. 38 | cd ${{ github.event.repository.name }} 39 | # Create the versioned release and add the built zip file. 40 | # To add a custom release-notes file, add: -F 41 | gh release create ${{github.ref_name}} --generate-notes ../${{ github.event.repository.name }}.zip 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows 2 | Thumbs.db 3 | Thumbs.db.meta 4 | 5 | # MacOS 6 | *.DS_Store 7 | 8 | # VS / VS Code 9 | .vs 10 | .vscode 11 | 12 | # Blender backups 13 | *.blend[1-9] 14 | 15 | # Python cache files (Krita plugin backups.) 16 | __pycache__ -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Blender Add-on: Python Debugger 2 | 3 | Allows debugging of Blender Python add-ons using Visual Studio Code or Visual Studio 2019 v16.6 or later. 4 | 5 | Finding and fixing bugs in Blender add-ons can be a bit painful without a proper debugger. This add-on (inspired by the [Blender Debugger for VS Code](https://github.com/AlansCodeLog/blender-debugger-for-vscode) add-on) installs the [debugpy](https://github.com/microsoft/debugpy) package and adds a menu item to start a debug server inside Blender on a specific port. Visual Studio Code or Visual Studio 2019+ can then be used to attach to it to set breakpoints, inspect local variables, or evaluate custom expressions. 6 | 7 | ## Installation 8 | 9 | * Download the latest release from [here](https://github.com/hextantstudios/hextant_python_debugger/releases/latest/download/hextant_python_debugger.zip) or clone it using Git to your custom Blender `...\scripts\addons\` folder. 10 | * From Blender's Main Menu: 11 | * *Edit / Preferences* 12 | * Click the *Install* button and select the downloaded zip file. 13 | * Check the check-box next to the add-on to activate it. 14 | * In the add-on's Preferences section 15 | * Click *Install debugpy* to install the `debugpy` package. 16 | * If this fails, open Blender's console from *Window/Toggle System Console* and see if there are additional error messages. 17 | * Note: Before uninstalling the add-on you may wish to click *Uninstall debugpy*. 18 | * *Server Port:* The default port (`5678`) should be fine, but it can be changed if needed as long as the same value is used when connecting from Visual Studio. 19 | 20 | ## Setup a Custom *Scripts* Folder for Your Add-on 21 | 22 | While it is not essential to do so, it is a bit easier to develop a new add-on in you own custom scripts folder. This can be configured in Blender: 23 | 24 | * *Edit / Preferences / File Paths / Scripts* - Set to a folder on your drive. 25 | * ex: `C:\blender-scripts\` 26 | 27 | * Create a sub-folder underneath this named `addons\`. (Required by Blender.) 28 | * Finally, create a folder for your add-on in the `addons\` folder (or place your Python file here if using only only a single file). 29 | * ex: `C:\blender-scripts\addons\my_blender_addon\...` 30 | 31 | * Your add-on should now show up in Blender's preferences. The drop-down there can be set to *User* to show all add-ons in this folder. 32 | 33 | ## Debug a Blender Add-on from Visual Studio Code 34 | 35 | To debug your Blender add-on using Visual Studio Code, a few things need to be done initially. In Visual Studio Code: 36 | 37 | * Enable the Python extension for Visual Studio Code: 38 | 39 | * Click *File / Preferences / Extensions* 40 | * Search for Python and install the extension by Microsoft. 41 | * Note: While you may get a warning about Python not being installed. This can be ignored as it is not required for remote debugging. If you wish, you can install it from the *Download* link provided, [python.org](https://www.python.org/downloads/), your OS's package manager, or the [Microsoft Store](https://apps.microsoft.com/store/search/python?hl=en-us&gl=US). 42 | 43 | * Click *File / Open Folder* and open the folder containing your add-on. 44 | 45 | * Click *Run / Add Configuration* 46 | 47 | * Select a debug configuration: *Remote Attach* 48 | 49 | * Host name: `localhost` 50 | 51 | * Port number: `5678` - Or use the value set in the Blender add-on's preferences. 52 | 53 | * This will create and open a `.vscode/launch.json` file in your add-on projects folder. 54 | 55 | * Copy and paste the `localRoot` value to the `remoteRoot` and save and close the file: 56 | ```json 57 | "localRoot": "${workspaceFolder}", 58 | "remoteRoot": "${workspaceFolder}" 59 | ``` 60 | 61 | You should now be able to debug your add-on as needed by doing the following: 62 | 63 | * From Blender's main menu, click: *{Blender Icon} / System / Start Debug Server* 64 | * Note that this only needs to be done once per Blender execution. If the menu appears disabled, the `debugpy` package needs to be installed from the add-on's preferences. 65 | * From Visual Studio Code: 66 | * If not already open, click *File / Open Folder* and open the folder containing your add-on. 67 | * Press `F5` to connect to Blender 68 | * An error showing `connect ECONNREFUSED 127.0.0.1:5678` usually means the debug server has not been started. 69 | * To set a breakpoint in an add-on file, click to the left of the desired line (or use the `F9` hotkey). When Blender executes that line, Visual Studio Code should highlight it and populate the *Call Stack* and *Variables* window. The *Watch* window can be used to view custom expressions. 70 | * The debug toolbar can be used to control stepping or `F10` steps over a line and `F11` steps into one. `F5` continues execution. 71 | * After making a change and saving the file, the add-on will need to be reloaded in Blender. See my [Reload Add-on](https://github.com/hextantstudios/hextant_reload_addon) add-on for more information about how to do this quickly and properly. 72 | * Note: To exclude the auto-generated `__pycache__` folder (that is created when Blender compiles the add-on) from the *Explorer* file view and find-in-file searches in Visual Studio Code: 73 | * Click *File / Preferences / Settings* 74 | * Click *User / Text Editor / Files / Exclude* (or search for "Files/Exclude") 75 | * Add: `**/__pycache__` 76 | 77 | ## Debug a Blender Add-on from Visual Studio 2019 or 2022 78 | 79 | To debug your Blender Add-on from Visual Studio 2019 v16.6 or later, you will need to intially: 80 | 81 | * Open the *Visual Studio Installer* application and install the *Python development* workload. 82 | * Note that *only* the *Python language support* option is needed for remote debugging and others Python options can be un-checked. 83 | 84 | You should now be able to debug your add-on as needed by doing the following: 85 | 86 | * From Blender's main menu, click: *{Blender Icon} / System / Start Debug Server* 87 | * Note that this only needs to be done once per Blender execution. If the menu appears disabled, the `debugpy` package needs to be installed from the add-on's preferences. 88 | * From Visual Studio: 89 | * Click *File / Open / Folder* and open the folder containing your add-on. 90 | * Click *Debug / Attach to Process* (`Ctrl + Alt + P`) 91 | * Connection type: *Python remote (debugpy)* 92 | * Connection target: `localhost:5678` (*press enter*) 93 | * It should now show in the Processes list, click *Attach*. 94 | * To set a breakpoint in an add-on file, click to the left of the desired line (or use the `F9` hotkey). When Blender executes that line, Visual Studio should highlight it and populate the *Call Stack* and *Locals* window. The *Watch* window can be used to view custom expressions. 95 | * The debug toolbar can be used to control stepping or `F10` steps over a line and `F11` steps into one. `F5` continues execution. 96 | * After making a change and saving the file, the add-on will need to be reloaded in Blender. See my [Reload Add-on](https://github.com/hextantstudios/hextant_reload_addon) add-on for more information about how to do this quickly and properly. 97 | * Note: To exclude the auto-generated `__pycache__` folder (that is created when Blender compiles the add-on) from the *Solution Explorer* and searches in Visual Studio perform *one* of the following: 98 | * Click the *Show All Files* button in the *Solution Explorer* and open `VSWorkspaceSettings.json` 99 | * Add `__pycache__` to the `"ExcludedItems"` array. 100 | * *or* add `__pycache__` to your `.gitignore` file if using Git. 101 | 102 | ## Known Issues 103 | 104 | * *None currently.* 105 | 106 | ## License 107 | 108 | This work is licensed under [GNU General Public License Version 3](https://download.blender.org/release/GPL3-license.txt). -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 by Hextant Studios. https://HextantStudios.com 2 | # This work is licensed under GNU General Public License Version 3. 3 | # License: https://download.blender.org/release/GPL3-license.txt 4 | 5 | # Inspired by: https://github.com/AlansCodeLog/blender-debugger-for-vscode 6 | 7 | # Notes: 8 | # * As of 5/3/2022 debugpy provides no methods to stop the server or check if one is 9 | # still listening. 10 | 11 | bl_info = { 12 | "name": "Python Debugger", 13 | "author": "Hextant Studios", 14 | "version": (1, 0, 0), 15 | "blender": (3, 0, 0), 16 | "location": "Click 'Install debugpy' below. Main Menu / Blender Icon / System / Start Python Debugger", 17 | "description": "Starts debugpy and listens for connections from a remote debugger such " \ 18 | "as Visual Studio Code or Visual Studio 2019 v16.6+.", 19 | "doc_url": "https://github.com/hextantstudios/hextant_python_debugger", 20 | "category": "Development", 21 | } 22 | 23 | import bpy, sys, os, site, subprocess, importlib 24 | from bpy.types import Operator, AddonPreferences 25 | from bpy.props import IntProperty 26 | 27 | # The global debugpy module (imported when the server is started). 28 | debugpy = None 29 | 30 | # Returns true if debugpy has been installed. 31 | def is_debugpy_installed() -> bool: 32 | try: 33 | # Blender does not add the user's site-packages/ directory by default. 34 | sys.path.append(site.getusersitepackages()) 35 | return importlib.util.find_spec('debugpy') is not None 36 | finally: 37 | sys.path.remove(site.getusersitepackages()) 38 | 39 | # 40 | # Addon Preferences 41 | # 42 | 43 | # Preferences to select the addon package name, etc. 44 | class DebugPythonPreferences(AddonPreferences): 45 | bl_idname = __package__ 46 | 47 | port: IntProperty(name="Server Port", default=5678, min=1024, max=65535, 48 | description="The port number the debug server will listen on. This must match the " + 49 | "port number configured in the debugger application.") 50 | 51 | def draw(self, context): 52 | installed = is_debugpy_installed() 53 | layout = self.layout 54 | layout.use_property_split = True 55 | 56 | if installed: 57 | layout.prop(self, 'port') 58 | layout.operator(UninstallDebugpy.bl_idname) 59 | else: 60 | layout.operator(InstallDebugpy.bl_idname) 61 | 62 | 63 | # 64 | # Operators 65 | # 66 | 67 | # Installs debugpy package into Blender's Python distribution. 68 | class InstallDebugpy(Operator): 69 | """Installs debugpy package into Blender's Python distribution.""" 70 | bl_idname = "script.install_debugpy" 71 | bl_label = "Install debugpy" 72 | 73 | def execute(self, context): 74 | python = os.path.abspath(sys.executable) 75 | self.report({'INFO'}, "Installing 'debugpy' package.") 76 | # Verify 'pip' package manager is installed. 77 | try: 78 | context.window.cursor_set('WAIT') 79 | subprocess.call([python, "-m", "ensurepip"]) 80 | # Upgrade 'pip'. This shouldn't be needed. 81 | # subprocess.call([python, "-m", "pip", "install", "--upgrade", "pip", "--yes"]) 82 | except Exception: 83 | self.report({'ERROR'}, "Failed to verify 'pip' package manager installation.") 84 | return {'FINISHED'} 85 | finally: 86 | context.window.cursor_set('DEFAULT') 87 | 88 | # Install 'debugpy' package. 89 | try: 90 | context.window.cursor_set('WAIT') 91 | subprocess.call([python, "-m", "pip", "install", "debugpy"]) 92 | except Exception: 93 | self.report({'ERROR'}, "Failed to install 'debugpy' package.") 94 | return {'FINISHED'} 95 | finally: 96 | context.window.cursor_set('DEFAULT') 97 | 98 | self.report({'INFO'}, "Successfully installed 'debugpy' package.") 99 | return {'FINISHED'} 100 | 101 | 102 | # Uninstalls debugpy package into Blender's Python distribution. 103 | class UninstallDebugpy(Operator): 104 | """Uninstalls debugpy package from Blender's Python distribution.""" 105 | bl_idname = "script.uninstall_debugpy" 106 | bl_label = "Uninstall debugpy" 107 | 108 | def execute(self, context): 109 | python = os.path.abspath(sys.executable) 110 | self.report({'INFO'}, "Uninstalling 'debugpy' package.") 111 | 112 | # Uninstall 'debugpy' package. 113 | try: 114 | context.window.cursor_set('WAIT') 115 | subprocess.call([python, "-m", "pip", "uninstall", "debugpy", "--yes"]) 116 | except Exception: 117 | self.report({'ERROR'}, "Failed to uninstall 'debugpy' package.") 118 | return {'FINISHED'} 119 | finally: 120 | context.window.cursor_set('DEFAULT') 121 | 122 | self.report({'INFO'}, "Successfully uninstalled 'debugpy' package.") 123 | return {'FINISHED'} 124 | 125 | 126 | # Starts the debug server for Python scripts. 127 | class StartDebugServer(Operator): 128 | """Starts the remote debug server (debugpy) for Python scripts. 129 | Note: debugpy must be installed from the add-on's preferences.""" 130 | bl_idname = "script.start_debug_server" 131 | bl_label = "Start Debug Server" 132 | 133 | @classmethod 134 | def poll(cls, context): 135 | return is_debugpy_installed() 136 | 137 | def execute(self, context): 138 | addon_prefs = context.preferences.addons[__package__].preferences 139 | 140 | # Import the debugpy package. 141 | global debugpy 142 | if not debugpy: 143 | try: 144 | sys.path.append(site.getusersitepackages()) 145 | debugpy = importlib.import_module('debugpy') 146 | except: 147 | self.report({'ERROR'}, "Failed to import debugpy! " + 148 | "Verify that debugpy has been installed from the add-on's preferences.") 149 | return {'FINISHED'} 150 | finally: 151 | sys.path.remove(site.getusersitepackages()) 152 | 153 | # Start debugpy listening. Note: Always try as there is no way to query if debugpy 154 | # is already listening. 155 | # Get the desired port to listen on. 156 | port = addon_prefs.port 157 | 158 | try: 159 | debugpy.listen(port) 160 | except: 161 | self.report({'WARNING'}, 162 | f"Remote python debugger failed to start (or already started) on port {port}.") 163 | return {'FINISHED'} 164 | 165 | self.report({'INFO'}, f"Remote python debugger started on port {port}.") 166 | return {'FINISHED'} 167 | 168 | 169 | # 170 | # Menu Items 171 | # 172 | 173 | # Draw the main menu entry for: {Blender}/System/Start Remote Debugger 174 | def start_remote_debugger_menu(self, context): 175 | self.layout.operator(StartDebugServer.bl_idname, icon='SCRIPT') 176 | 177 | # 178 | # Registration 179 | # 180 | 181 | _classes = (DebugPythonPreferences, InstallDebugpy, UninstallDebugpy, StartDebugServer) 182 | _register, _unregister = bpy.utils.register_classes_factory(_classes) 183 | 184 | def register(): 185 | _register() 186 | # Add a System menu entry to start the server. 187 | bpy.types.TOPBAR_MT_blender_system.prepend(start_remote_debugger_menu) 188 | 189 | def unregister(): 190 | _unregister() 191 | # Remove System menu entry 192 | bpy.types.TOPBAR_MT_blender_system.remove(start_remote_debugger_menu) 193 | 194 | --------------------------------------------------------------------------------