Skip to content

Commit 5a36cc5

Browse files
authored
Merge pull request #2778 from alicevision/dev/loadPluginsConfig
[core] plugins: Load plugin's configuration file upon its initialisation
2 parents aab34a2 + e8570d8 commit 5a36cc5

File tree

3 files changed

+106
-20
lines changed

3 files changed

+106
-20
lines changed

meshroom/core/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ def initPlugins():
435435
# Set the ProcessEnv for each plugin
436436
if plugins:
437437
for plugin in plugins:
438-
plugin.processEnv = processEnvFactory(f)
438+
plugin.processEnv = processEnvFactory(f, plugin.configEnv)
439439

440440
# Rez plugins (with a RezProcessEnv)
441441
rezPlugins = initRezPlugins()
@@ -452,6 +452,6 @@ def initRezPlugins():
452452
# Set the ProcessEnv for Rez plugins
453453
if plugins:
454454
for plugin in plugins:
455-
plugin.processEnv = processEnvFactory(path, "rez", name)
455+
plugin.processEnv = processEnvFactory(path, plugin.configEnv, envType="rez", uri=name)
456456

457457
return rezPlugins

meshroom/core/attribute.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,8 @@ def getEvalValue(self):
397397
Return the value. If it is a string, expressions will be evaluated.
398398
"""
399399
if isinstance(self.value, str):
400-
substituted = Template(self.value).safe_substitute(os.environ)
400+
env = self.node.nodePlugin.configFullEnv if self.node.nodePlugin else os.environ
401+
substituted = Template(self.value).safe_substitute(env)
401402
try:
402403
varResolved = substituted.format(**self.node._cmdVars)
403404
return varResolved

meshroom/core/plugins.py

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

3+
import glob
34
import importlib
5+
import json
46
import logging
57
import os
68
import re
@@ -9,7 +11,6 @@
911
from enum import Enum
1012
from inspect import getfile
1113
from pathlib import Path
12-
import glob
1314

1415
from meshroom.common import BaseObject
1516
from meshroom.core import desc
@@ -62,19 +63,24 @@ class ProcessEnv(BaseObject):
6263
6364
Args:
6465
folder: the source folder for the process.
66+
configEnv: the dictionary containing the environment variables defined in a configuration file
67+
for the process to run.
6568
envType: (optional) the type of process environment.
6669
uri: (optional) the Unique Resource Identifier to activate the environment.
6770
"""
6871

69-
def __init__(self, folder: str, envType: ProcessEnvType = ProcessEnvType.DIRTREE, uri: str = ""):
72+
def __init__(self, folder: str, configEnv: dict[str, str],
73+
envType: ProcessEnvType = ProcessEnvType.DIRTREE, uri: str = ""):
7074
super().__init__()
7175
self._folder: str = folder
76+
self._configEnv: dict[str: str] = configEnv
7277
self._processEnvType: ProcessEnvType = envType
7378
self.uri: str = uri
79+
self._env: dict = None
7480

7581
def getEnvDict(self) -> dict:
7682
""" Return the environment dictionary if it has been modified, None otherwise. """
77-
return None
83+
return self._env
7884

7985
def getCommandPrefix(self) -> str:
8086
""" Return the prefix to the command line that will be executed by the process. """
@@ -88,8 +94,8 @@ def getCommandSuffix(self) -> str:
8894
class DirTreeProcessEnv(ProcessEnv):
8995
"""
9096
"""
91-
def __init__(self, folder: str):
92-
super().__init__(folder, ProcessEnvType.DIRTREE)
97+
def __init__(self, folder: str, configEnv: dict[str: str]):
98+
super().__init__(folder, configEnv, envType=ProcessEnvType.DIRTREE)
9399

94100
venvLibPaths = glob.glob(f'{folder}/lib*/python[0-9].[0-9]*/site-packages', recursive=False)
95101

@@ -115,22 +121,30 @@ def __init__(self, folder: str):
115121
extraLibPaths.append(os.path.join(path, directory))
116122
self.libPaths = self.libPaths + extraLibPaths
117123

118-
def getEnvDict(self) -> dict:
119-
env = os.environ.copy()
120-
env["PYTHONPATH"] = os.pathsep.join([f"{_MESHROOM_ROOT}"] + self.pythonPaths + [f"{os.getenv('PYTHONPATH', '')}"])
121-
env["LD_LIBRARY_PATH"] = f"{os.pathsep.join(self.libPaths)}{os.pathsep}{os.getenv('LD_LIBRARY_PATH', '')}"
122-
env["PATH"] = f"{os.pathsep.join(self.binPaths)}{os.pathsep}{os.getenv('PATH', '')}"
124+
# Setup the environment dictionary
125+
self._env = os.environ.copy()
126+
self._env["PYTHONPATH"] = os.pathsep.join(
127+
[f"{_MESHROOM_ROOT}"] + self.pythonPaths + [f"{os.getenv('PYTHONPATH', '')}"])
128+
self._env["LD_LIBRARY_PATH"] = f"{os.pathsep.join(self.libPaths)}{os.pathsep}{os.getenv('LD_LIBRARY_PATH', '')}"
129+
self._env["PATH"] = f"{os.pathsep.join(self.binPaths)}{os.pathsep}{os.getenv('PATH', '')}"
130+
131+
for k, val in self._configEnv.items():
132+
# Preserve user-defined environment variables:
133+
# manually set environment variable values take precedence over config file defaults.
134+
if k in self._env:
135+
continue
136+
137+
self._env[k] = val
123138

124-
return env
125139

126140

127141
class RezProcessEnv(ProcessEnv):
128142
"""
129143
"""
130-
def __init__(self, folder: str, uri: str = ""):
144+
def __init__(self, folder: str, configEnv: dict[str: str], uri: str = ""):
131145
if not uri:
132146
raise RuntimeError("Missing name of the Rez environment needs to be provided.")
133-
super().__init__(folder, ProcessEnvType.REZ, uri)
147+
super().__init__(folder, configEnv, envType=ProcessEnvType.REZ, uri=uri)
134148

135149
def resolveRezSubrequires(self) -> list[str]:
136150
"""
@@ -187,10 +201,10 @@ def getCommandSuffix(self):
187201
return "'"
188202

189203

190-
def processEnvFactory(folder: str, envType: str = "dirtree", uri: str = "") -> ProcessEnv:
204+
def processEnvFactory(folder: str, configEnv: dict[str: str], envType: str = "dirtree", uri: str = "") -> ProcessEnv:
191205
if envType == "dirtree":
192-
return DirTreeProcessEnv(folder)
193-
return RezProcessEnv(folder, uri=uri)
206+
return DirTreeProcessEnv(folder, configEnv)
207+
return RezProcessEnv(folder, configEnv, uri=uri)
194208

195209

196210
class NodePluginStatus(Enum):
@@ -215,6 +229,9 @@ class Plugin(BaseObject):
215229
to its corresponding NodePlugin object
216230
templates: dictionary mapping the name of templates (.mg files) associated to the plugin
217231
with their absolute paths
232+
configEnv: the environment variables and their values, as described in the plugin's
233+
configuration file
234+
configFullEnv: the static merge of os.environ and configEnv, with os.environ taking precedence
218235
processEnv: the environment required for the nodes' processes to be correctly executed
219236
"""
220237

@@ -226,9 +243,12 @@ def __init__(self, name: str, path: str):
226243

227244
self._nodePlugins: dict[str: NodePlugin] = {}
228245
self._templates: dict[str: str] = {}
229-
self._processEnv: ProcessEnv = ProcessEnv(path)
246+
self._configEnv: dict[str: str] = {}
247+
self._configFullEnv: dict[str: str] = {}
248+
self._processEnv: ProcessEnv = ProcessEnv(path, self._configEnv)
230249

231250
self.loadTemplates()
251+
self.loadConfig()
232252

233253
@property
234254
def name(self):
@@ -263,6 +283,19 @@ def processEnv(self, processEnv: ProcessEnv):
263283
""" Set the environment required to successfully execute processes. """
264284
self._processEnv = processEnv
265285

286+
@property
287+
def configEnv(self):
288+
"""
289+
Return the dictionary containing the environment variables and their values
290+
provided in the plugin's configuration file.
291+
"""
292+
return self._configEnv
293+
294+
@property
295+
def configFullEnv(self):
296+
""" Return the fusion of the os.environ dictionary with the configEnv dictionary. """
297+
return self._configFullEnv
298+
266299
def addNodePlugin(self, nodePlugin: NodePlugin):
267300
"""
268301
Add a node plugin to the current plugin object and assign it as its containing plugin.
@@ -299,6 +332,52 @@ def loadTemplates(self):
299332
if file.endswith(".mg"):
300333
self._templates[os.path.splitext(file)[0]] = os.path.join(self.path, file)
301334

335+
def loadConfig(self):
336+
"""
337+
Load the plugin's configuration file if it exists and saves all its environment variables
338+
and their values, if they are valid.
339+
The configuration file is expected to be named "config.json", located at the top-level of
340+
the plugin.
341+
"""
342+
try:
343+
with open(os.path.join(self.path, "config.json")) as config:
344+
content = json.load(config)
345+
for entry in content:
346+
# An entry is expected to be formatted as follows:
347+
# { "key": "key_of_var", "type": "type_of_value", "value": "var_value" }
348+
# If "type" is not provided, it is assumed to be "string"
349+
k = entry.get("key", None)
350+
t = entry.get("type", None)
351+
val = entry.get("value", None)
352+
353+
if not k or not val:
354+
logging.warning(f"Invalid entry in configuration file for {self.name}: {entry}.")
355+
continue
356+
357+
if t == "path":
358+
if os.path.isabs(val):
359+
resolvedPath = Path(val).resolve()
360+
else:
361+
resolvedPath = Path(os.path.join(self.path, val)).resolve()
362+
363+
if resolvedPath.exists():
364+
val = resolvedPath.as_posix()
365+
else:
366+
logging.warning(f"{k}: {resolvedPath.as_posix()} does not exist "
367+
f"(path before resolution: {val}).")
368+
369+
self._configEnv[k] = str(val)
370+
371+
except FileNotFoundError:
372+
logging.debug(f"No configuration file 'config.json' was found for {self.name}.")
373+
except json.JSONDecodeError as err:
374+
logging.error(f"Malformed JSON in the configuration file for {self.name}: {err}")
375+
except IOError as err:
376+
logging.error(f"Error while accessing the configuration file for {self.name}: {err}")
377+
378+
# If both dictionaries have identical keys, os.environ overwrites existing values from _configEnv
379+
self._configFullEnv = self._configEnv | os.environ
380+
302381
def containsNodePlugin(self, name: str) -> bool:
303382
"""
304383
Return whether the node plugin "name" is part of the plugin, independently from its
@@ -431,6 +510,12 @@ def commandSuffix(self) -> str:
431510
""" Return the command suffix for the NodePlugin's execution. """
432511
return self.processEnv.getCommandSuffix()
433512

513+
@property
514+
def configFullEnv(self) -> dict[str: str]:
515+
""" Return the plugin's full environment dictionary. """
516+
if self.plugin:
517+
return self.plugin.configFullEnv
518+
return {}
434519

435520
class NodePluginManager(BaseObject):
436521
"""

0 commit comments

Comments
 (0)