Skip to content

Commit 0ce52c6

Browse files
committed
Preferences: Allow add-on to report and apply pip dependencies updates
1 parent fb59713 commit 0ce52c6

File tree

2 files changed

+160
-106
lines changed

2 files changed

+160
-106
lines changed

mitsuba-blender/__init__.py

+125-105
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,32 @@
1818

1919
import os
2020
import sys
21-
import subprocess
2221

2322
from . import (
2423
engine, nodes, properties, operators, ui
2524
)
2625

26+
from .utils import pip_ensure, pip_has_package, pip_install_package, pip_is_package_up_to_date
27+
2728
def get_addon_preferences(context):
2829
return context.preferences.addons[__name__].preferences
2930

30-
def init_mitsuba(context):
31-
# Make sure we can load mitsuba from blender
31+
def get_addon_version_string():
32+
return f'{".".join(str(e) for e in bl_info["version"])}{bl_info["warning"] if "warning" in bl_info else ""}'
33+
34+
def get_mitsuba_version_string():
35+
import mitsuba
36+
return mitsuba.__version__
37+
38+
def get_addon_info_string():
39+
return f'mitsuba-blender v{get_addon_version_string()} registered (with mitsuba v{get_mitsuba_version_string()})'
40+
41+
def init_mitsuba():
42+
# Make sure we can load Mitsuba from Blender
3243
try:
3344
should_reload_mitsuba = 'mitsuba' in sys.modules
3445
import mitsuba
35-
# If mitsuba was already loaded and we change the path, we need to reload it, since the import above will be ignored
46+
# If Mitsuba was already loaded and we change the path, we need to reload it, since the import above will be ignored
3647
if should_reload_mitsuba:
3748
import importlib
3849
importlib.reload(mitsuba)
@@ -44,29 +55,26 @@ def init_mitsuba(context):
4455
except ModuleNotFoundError:
4556
return False
4657

47-
def try_register_mitsuba(context):
58+
def register_addon(context):
4859
prefs = get_addon_preferences(context)
49-
prefs.mitsuba_dependencies_status_message = ''
60+
prefs.status_message = ''
5061

51-
could_init_mitsuba = False
5262
if prefs.using_mitsuba_custom_path:
53-
update_additional_custom_paths(prefs, context)
54-
could_init_mitsuba = init_mitsuba(context)
63+
prefs.update_additional_custom_paths(context)
64+
could_init_mitsuba = init_mitsuba()
5565
prefs.has_valid_mitsuba_custom_path = could_init_mitsuba
5666
if could_init_mitsuba:
57-
import mitsuba
58-
prefs.mitsuba_dependencies_status_message = f'Found custom Mitsuba v{mitsuba.__version__}.'
67+
prefs.status_message = f'Found custom Mitsuba v{get_mitsuba_version_string()}.'
5968
else:
60-
prefs.mitsuba_dependencies_status_message = 'Failed to load custom Mitsuba. Please verify the path to the build directory.'
61-
elif prefs.has_pip_package:
62-
could_init_mitsuba = init_mitsuba(context)
69+
prefs.status_message = 'Failed to load custom Mitsuba. Please verify the path to the build directory.'
70+
elif prefs.is_mitsuba_installed:
71+
could_init_mitsuba = init_mitsuba()
6372
if could_init_mitsuba:
64-
import mitsuba
65-
prefs.mitsuba_dependencies_status_message = f'Found pip Mitsuba v{mitsuba.__version__}.'
73+
prefs.status_message = f'Found pip Mitsuba v{get_mitsuba_version_string()}.'
6674
else:
67-
prefs.mitsuba_dependencies_status_message = 'Failed to load Mitsuba package.'
75+
prefs.status_message = 'Failed to load Mitsuba package.'
6876
else:
69-
prefs.mitsuba_dependencies_status_message = 'Mitsuba dependencies not installed.'
77+
prefs.status_message = 'Mitsuba dependencies not installed.'
7078

7179
prefs.is_mitsuba_initialized = could_init_mitsuba
7280

@@ -79,7 +87,7 @@ def try_register_mitsuba(context):
7987

8088
return could_init_mitsuba
8189

82-
def try_unregister_mitsuba():
90+
def unregister_addon():
8391
'''
8492
Try unregistering Addon classes.
8593
This may fail if Mitsuba wasn't found, hence the try catch guard
@@ -94,108 +102,107 @@ def try_unregister_mitsuba():
94102
except RuntimeError:
95103
return False
96104

97-
def try_reload_mitsuba(context):
98-
try_unregister_mitsuba()
99-
if try_register_mitsuba(context):
105+
def reload_addon(context):
106+
unregister_addon()
107+
if register_addon(context):
100108
# Save user preferences
101109
bpy.ops.wm.save_userpref()
102110

103-
def ensure_pip():
104-
result = subprocess.run([sys.executable, '-m', 'ensurepip'], capture_output=True)
105-
return result.returncode == 0
106-
107-
def check_pip_dependencies(context):
108-
prefs = get_addon_preferences(context)
109-
result = subprocess.run([sys.executable, '-m', 'pip', 'show', 'mitsuba'], capture_output=True)
110-
prefs.has_pip_package = result.returncode == 0
111-
112-
def clean_additional_custom_paths(self, context):
113-
# Remove old values from system PATH and sys.path
114-
if self.additional_python_path in sys.path:
115-
sys.path.remove(self.additional_python_path)
116-
if self.additional_path and self.additional_path in os.environ['PATH']:
117-
items = os.environ['PATH'].split(os.pathsep)
118-
items.remove(self.additional_path)
119-
os.environ['PATH'] = os.pathsep.join(items)
120-
121-
def update_additional_custom_paths(self, context):
122-
build_path = bpy.path.abspath(self.mitsuba_custom_path)
123-
if len(build_path) > 0:
124-
clean_additional_custom_paths(self, context)
125-
126-
# Add path to the binaries to the system PATH
127-
self.additional_path = build_path
128-
if self.additional_path not in os.environ['PATH']:
129-
os.environ['PATH'] += os.pathsep + self.additional_path
130-
131-
# Add path to python libs to sys.path
132-
self.additional_python_path = os.path.join(build_path, 'python')
133-
if self.additional_python_path not in sys.path:
134-
# NOTE: We insert in the first position here, so that the custom path
135-
# supersede the pip version
136-
sys.path.insert(0, self.additional_python_path)
137-
138-
class MITSUBA_OT_install_pip_dependencies(Operator):
139-
bl_idname = 'mitsuba.install_pip_dependencies'
140-
bl_label = 'Install Mitsuba pip dependencies'
141-
bl_description = 'Use pip to install the add-on\'s required dependencies'
111+
class MITSUBA_OT_download_package_dependencies(Operator):
112+
bl_idname = 'mitsuba.download_package_dependencies'
113+
bl_label = 'Download the latest package dependencies'
114+
bl_description = 'Use pip to download the add-on\'s latest required dependencies'
142115

143116
@classmethod
144117
def poll(cls, context):
145118
prefs = get_addon_preferences(context)
146-
return not prefs.has_pip_package
119+
return not prefs.is_mitsuba_installed or not prefs.is_mitsuba_uptodate
147120

148121
def execute(self, context):
149-
result = subprocess.run([sys.executable, '-m', 'pip', 'install', 'mitsuba'], capture_output=True)
150-
if result.returncode != 0:
151-
self.report({'ERROR'}, f'Failed to install Mitsuba with return code {result.returncode}.')
122+
if not pip_install_package('mitsuba'):
123+
self.report({'ERROR'}, 'Failed to download Mitsuba package with pip.')
152124
return {'CANCELLED'}
153125

154126
prefs = get_addon_preferences(context)
155-
prefs.has_pip_package = True
156-
157-
try_reload_mitsuba(context)
127+
if prefs.is_mitsuba_initialized and not prefs.is_mitsuba_uptodate:
128+
# If the package was updated, require a restart to use the new version.
129+
prefs.is_restart_required = True
130+
else:
131+
reload_addon(context)
132+
133+
prefs.is_mitsuba_installed = True
134+
prefs.is_mitsuba_uptodate = True
158135

159136
return {'FINISHED'}
160137

161-
def update_using_mitsuba_custom_path(self, context):
162-
if self.is_mitsuba_initialized:
163-
self.require_restart = True
164-
if self.using_mitsuba_custom_path:
165-
update_mitsuba_custom_path(self, context)
166-
else:
167-
clean_additional_custom_paths(self, context)
168-
169-
def update_mitsuba_custom_path(self, context):
170-
if self.is_mitsuba_initialized:
171-
self.require_restart = True
172-
if self.using_mitsuba_custom_path and len(self.mitsuba_custom_path) > 0:
173-
update_additional_custom_paths(self, context)
174-
if not self.is_mitsuba_initialized:
175-
try_reload_mitsuba(context)
176-
177138
class MitsubaPreferences(AddonPreferences):
178139
bl_idname = __name__
179140

180141
is_mitsuba_initialized : BoolProperty(
181142
name = 'Is Mitsuba initialized',
182143
)
183144

184-
has_pip_package : BoolProperty(
185-
name = 'Has pip dependencies installed',
145+
is_mitsuba_installed : BoolProperty(
146+
name = 'Is the Mitsuba package installed',
186147
)
187148

188-
mitsuba_dependencies_status_message : StringProperty(
189-
name = 'Mitsuba dependencies status message',
190-
default = '',
149+
is_mitsuba_uptodate : BoolProperty(
150+
name = 'Is the Mitsuba package up-to-date',
191151
)
192152

193-
require_restart : BoolProperty(
194-
name = 'Require a Blender restart',
153+
is_restart_required : BoolProperty(
154+
name = 'Is a Blender restart required',
155+
)
156+
157+
status_message : StringProperty(
158+
name = 'Add-on status message',
159+
default = '',
195160
)
196161

197162
# Advanced settings
198163

164+
def clean_additional_custom_paths(self, context):
165+
# Remove old values from system PATH and sys.path
166+
if self.additional_python_path in sys.path:
167+
sys.path.remove(self.additional_python_path)
168+
if self.additional_path and self.additional_path in os.environ['PATH']:
169+
items = os.environ['PATH'].split(os.pathsep)
170+
items.remove(self.additional_path)
171+
os.environ['PATH'] = os.pathsep.join(items)
172+
173+
def update_additional_custom_paths(self, context):
174+
build_path = bpy.path.abspath(self.mitsuba_custom_path)
175+
if len(build_path) > 0:
176+
self.clean_additional_custom_paths(context)
177+
178+
# Add path to the binaries to the system PATH
179+
self.additional_path = build_path
180+
if self.additional_path not in os.environ['PATH']:
181+
os.environ['PATH'] += os.pathsep + self.additional_path
182+
183+
# Add path to python libs to sys.path
184+
self.additional_python_path = os.path.join(build_path, 'python')
185+
if self.additional_python_path not in sys.path:
186+
# NOTE: We insert in the first position here, so that the custom path
187+
# supersede the pip version
188+
sys.path.insert(0, self.additional_python_path)
189+
190+
def update_mitsuba_custom_path(self, context):
191+
if self.is_mitsuba_initialized:
192+
self.is_restart_required = True
193+
if self.using_mitsuba_custom_path and len(self.mitsuba_custom_path) > 0:
194+
self.update_additional_custom_paths(context)
195+
if not self.is_mitsuba_initialized:
196+
reload_addon(context)
197+
198+
def update_using_mitsuba_custom_path(self, context):
199+
if self.is_mitsuba_initialized:
200+
self.is_restart_required = True
201+
if self.using_mitsuba_custom_path:
202+
self.update_mitsuba_custom_path(context)
203+
else:
204+
self.clean_additional_custom_paths(context)
205+
199206
using_mitsuba_custom_path : BoolProperty(
200207
name = 'Using custom Mitsuba path',
201208
update = update_using_mitsuba_custom_path,
@@ -229,18 +236,23 @@ def draw(self, context):
229236
layout = self.layout
230237

231238
row = layout.row()
232-
if self.require_restart:
233-
self.mitsuba_dependencies_status_message = 'A restart is required to apply the changes.'
239+
if self.is_restart_required:
240+
self.status_message = 'A restart is required to apply the changes.'
234241
row.alert = True
235242
icon = 'ERROR'
236-
elif self.has_pip_package or self.has_valid_mitsuba_custom_path:
243+
elif self.is_mitsuba_initialized:
237244
icon = 'CHECKMARK'
238245
else:
239246
icon = 'CANCEL'
240247
row.alert = True
241-
row.label(text=self.mitsuba_dependencies_status_message, icon=icon)
248+
row.label(text=self.status_message, icon=icon)
242249

243-
layout.operator(MITSUBA_OT_install_pip_dependencies.bl_idname, text='Install dependencies using pip')
250+
if not self.is_mitsuba_installed:
251+
download_operator_text = 'Install Mitsuba'
252+
else:
253+
download_operator_text = 'Update Mitsuba'
254+
255+
layout.operator(MITSUBA_OT_download_package_dependencies.bl_idname, text=download_operator_text)
244256

245257
box = layout.box()
246258
box.label(text='Advanced Settings')
@@ -249,27 +261,35 @@ def draw(self, context):
249261
box.prop(self, 'mitsuba_custom_path')
250262

251263
classes = (
252-
MITSUBA_OT_install_pip_dependencies,
264+
MITSUBA_OT_download_package_dependencies,
253265
MitsubaPreferences,
254266
)
255267

256268
def register():
257269
for cls in classes:
258270
register_class(cls)
259271

272+
if not pip_ensure():
273+
raise RuntimeError('Cannot activate mitsuba-blender add-on. Python pip module cannot be initialized.')
274+
260275
context = bpy.context
261276
prefs = get_addon_preferences(context)
262-
prefs.require_restart = False
277+
prefs.is_mitsuba_initialized = False
278+
prefs.is_mitsuba_uptodate = False
279+
prefs.is_restart_required = False
280+
prefs.has_valid_mitsuba_custom_path = False
281+
prefs.is_mitsuba_installed = pip_has_package('mitsuba')
263282

264-
if not ensure_pip():
265-
raise RuntimeError('Cannot activate mitsuba-blender add-on. Python pip module cannot be initialized.')
283+
if prefs.is_mitsuba_installed:
284+
prefs.is_mitsuba_uptodate = pip_is_package_up_to_date('mitsuba')
266285

267-
check_pip_dependencies(context)
268-
if try_register_mitsuba(context):
269-
import mitsuba
270-
print(f'mitsuba-blender v{".".join(str(e) for e in bl_info["version"])}{bl_info["warning"] if "warning" in bl_info else ""} registered (with mitsuba v{mitsuba.__version__})')
286+
if register_addon(context):
287+
print(get_addon_info_string())
271288

272289
def unregister():
273290
for cls in classes:
274291
unregister_class(cls)
275-
try_unregister_mitsuba()
292+
unregister_addon()
293+
294+
if __name__ == '__main__':
295+
register()

mitsuba-blender/utils/__init__.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11

22
import bpy
33

4+
import sys
5+
import subprocess
6+
47
def init_empty_scene(bl_context, name='Scene', clear_all_scenes=False):
58
''' Create an empty Blender scene with a specific name.
69
@@ -97,4 +100,35 @@ def init_empty_collection(bl_scene, name='Collection'):
97100
bl_collection = bpy.data.collections.new(name)
98101
# Link the collection to the scene
99102
bl_scene.collection.children.link(bl_collection)
100-
return bl_collection
103+
return bl_collection
104+
105+
#####################
106+
## PIP Utilities ##
107+
#####################
108+
109+
def pip_ensure():
110+
''' Ensure that pip is available in the executing Python environment. '''
111+
result = subprocess.run([sys.executable, '-m', 'ensurepip'], capture_output=True)
112+
return result.returncode == 0
113+
114+
def pip_has_package(package: str):
115+
''' Check if the executing Python environment has a specified package. '''
116+
result = subprocess.run([sys.executable, '-m', 'pip', 'show', package], capture_output=True)
117+
return result.returncode == 0
118+
119+
def pip_install_package(package: str):
120+
''' Install a specified package in the executing Python environment. '''
121+
result = subprocess.run([sys.executable, '-m', 'pip', 'install', '--upgrade', package], capture_output=True)
122+
return result.returncode == 0
123+
124+
def pip_is_package_up_to_date(package: str):
125+
''' Check if a given package is up-to-date. '''
126+
result = subprocess.run([sys.executable, '-m', 'pip', 'list', '--uptodate'], capture_output=True)
127+
if result.returncode != 0:
128+
return False
129+
for line in result.stdout.splitlines():
130+
line = line.decode('utf-8')
131+
parts = line.split(' ')
132+
if parts[0] == package:
133+
return True
134+
return False

0 commit comments

Comments
 (0)