11from __future__ import annotations
22
3+ import glob
34import importlib
5+ import json
46import logging
57import os
68import re
911from enum import Enum
1012from inspect import getfile
1113from pathlib import Path
12- import glob
1314
1415from meshroom .common import BaseObject
1516from 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:
8894class 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
127141class 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
196210class 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
435520class NodePluginManager (BaseObject ):
436521 """
0 commit comments