9.2 KiB
Building Blender extensions in this repository
This guide targets Blender 5.x, with Blender 5.2 as the current user target. Blender calls installable Python plug-ins add-on extensions. Use that format instead of the legacy single-file add-on format.
1. Understand the package boundary
Every extension is an independent directory under blender/:
blender/
example_tool/
blender_manifest.toml
__init__.py
operators.py # optional
properties.py # optional
ui.py # optional
wheels/ # optional bundled dependencies
The build command turns the contents of example_tool/ into a ZIP whose root
contains the manifest and __init__.py. The directory containing all extensions
is not itself a Python package.
Start with one __init__.py while a tool is small. Split it when operators,
panels, and data helpers become difficult to navigate. Multi-file extensions
must use relative imports:
from . import operators, properties, ui
Do not import the extension by a hard-coded installed name. Blender namespaces
extensions by repository, for example bl_ext.user_default.example_tool, and
that prefix changes when the package is installed elsewhere.
2. Write the manifest
A minimal repository-compatible manifest looks like this:
schema_version = "1.0.0"
id = "example_tool"
version = "0.1.0"
name = "Example Tool"
tagline = "Perform one useful Blender workflow"
maintainer = "Your Name"
type = "add-on"
blender_version_min = "5.0.0"
license = [
"SPDX:GPL-3.0-or-later",
]
Manifest rules worth catching early:
idis the stable package identity. Do not change it merely to rename the UI.versionfollows semantic versioning. Bump it when distributing an update.taglineis short, has no trailing punctuation, and describes an action.blender_version_minis the oldest version actually supported, not simply the developer's newest installed version.- Omit unused optional fields rather than setting them to empty strings/lists.
- Use SPDX-prefixed license identifiers.
Permissions
Declare capabilities that reach outside normal Blender data:
[permissions]
files = "Export processed textures selected by the user"
network = "Synchronize assets with the configured server"
Only declare what the extension uses. Permission explanations must be short and must not end in punctuation. Network permission does not override Blender's online-access preference; check it before connecting:
if not bpy.app.online_access:
self.report({"ERROR"}, "Enable Online Access in Blender preferences")
return {"CANCELLED"}
3. Implement registration, an operator, and UI
This is a small but complete extension entry point:
import bpy
from bpy.props import BoolProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
class EXAMPLE_PG_Settings(PropertyGroup):
affect_selected: BoolProperty(
name="Affect Selected",
default=True,
)
class EXAMPLE_OT_Run(Operator):
bl_idname = "object.example_run"
bl_label = "Run Example"
bl_description = "Perform the example operation on the active mesh"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return bool(
context.mode == "OBJECT"
and context.active_object
and context.active_object.type == "MESH"
)
def execute(self, context):
obj = context.active_object
if obj is None:
self.report({"ERROR"}, "Select a mesh object")
return {"CANCELLED"}
# Prefer direct data API changes here. Use context, not bpy.context.
obj["example_was_run"] = True
self.report({"INFO"}, f"Processed {obj.name}")
return {"FINISHED"}
class EXAMPLE_PT_Panel(Panel):
bl_label = "Example Tool"
bl_idname = "EXAMPLE_PT_main"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Example"
def draw(self, context):
layout = self.layout
settings = context.scene.example_tool
layout.prop(settings, "affect_selected")
layout.operator("object.example_run")
CLASSES = (
EXAMPLE_PG_Settings,
EXAMPLE_OT_Run,
EXAMPLE_PT_Panel,
)
def register():
for cls in CLASSES:
bpy.utils.register_class(cls)
bpy.types.Scene.example_tool = PointerProperty(type=EXAMPLE_PG_Settings)
def unregister():
del bpy.types.Scene.example_tool
for cls in reversed(CLASSES):
bpy.utils.unregister_class(cls)
Important details:
- Register
PropertyGroupclasses before creating pointer/collection properties that reference them. - Unregister in exact reverse dependency order.
- Prefix class names and Blender identifiers to avoid collisions.
- A panel's
draw()executes frequently. It should be cheap and should not mutate scene data. - Use
poll()both to disable invalid UI actions and to document required context. bl_options = {"REGISTER", "UNDO"}is appropriate for scene-data changes, but Undo does not reverse files, HTTP calls, or other external side effects.
4. Design safe Blender operations
Blender data often has multiple users. Editing obj.data can change every
object sharing that mesh, and editing a material can change every object using
it. When the feature should be non-destructive, copy first:
result_object = source.copy()
result_object.data = source.data.copy()
source.users_collection[0].objects.link(result_object)
When modifiers must be applied without touching the source:
depsgraph = context.evaluated_depsgraph_get()
evaluated = source.evaluated_get(depsgraph)
result_mesh = bpy.data.meshes.new_from_object(
evaluated,
preserve_all_data_layers=True,
depsgraph=depsgraph,
)
Stateful operations should follow a transaction-like structure:
active_before = context.view_layer.objects.active
selected_before = list(context.selected_objects)
temporary_object = None
success = False
try:
# Create copies and perform the operation.
success = True
finally:
# Remove temporary data and restore selection/settings.
if temporary_object is not None:
bpy.data.objects.remove(temporary_object, do_unlink=True)
Track every temporary data-block you create. Removing an object does not
automatically remove its orphaned mesh, material, or image. Check users == 0
before removing a data-block that might legitimately be shared.
Use bpy.ops only when Blender exposes no suitable data API. Operators depend
on context: editor area, mode, active object, selected objects, active material
node, render engine, or view layer. A headless test is the quickest way to find
hidden context assumptions.
5. Store files and settings correctly
An installed extension may live in a read-only system repository, and upgrades
replace its package directory. Never store user data beside __init__.py.
Use the per-extension storage API:
storage_dir = bpy.utils.extension_path_user(
__package__,
path="cache",
create=True,
)
Use Blender properties for settings that should live in a .blend file, and an
AddonPreferences subclass for user preferences that should apply across
projects. Access preferences through __package__, not a literal module name:
preferences = bpy.context.preferences.addons[__package__].preferences
Use bpy.path.abspath() for Blender paths such as //textures/output.png.
Validate empty paths and file formats before starting an expensive operation.
6. Bundle dependencies
Extensions must be self-contained. For a third-party Python dependency:
- Download/build wheels for every supported OS and architecture.
- Put them under the extension, conventionally
wheels/. - List each relative wheel path in the manifest's
wheelsarray. - Build and test the installed ZIP on every advertised platform.
Do not import from the developer's system Python, mutate Blender's bundled
Python, or invoke pip during registration. Pure-Python code may be vendored,
but keep its license and avoid top-level package-name collisions.
7. Build and install
Build all repository extensions:
./build-all.sh
Build, reinstall, and enable them for the current Blender user:
./install-all.sh
Windows Command Prompt:
install-all.bat
Select a non-default Blender executable with BLENDER_BIN. Close running
Blender instances before reinstalling; an open process retains already imported
Python modules.
For the full validation and release sequence, continue with Testing and releasing.