#!/usr/bin/env python # -*- coding: utf-8 -*- """ Nexus User Setup Script Automatically sets up the Nexus plugin system in Maya upon startup. Features: - Auto-loads scripts, plugins, and shelves - Sets up icon paths for UI elements - Manages command port for external connections - Clean exit with proper resource cleanup - Maya 2018+ compatible """ # Standard library imports import atexit import os import sys import re # Maya imports with error handling try: import maya.cmds as cmds import maya.mel as mel import maya.utils except ImportError as e: print(f"[Tool] ERROR: Failed to import Maya modules: {e}") raise # Silently try to open default commandPort to avoid startup error if it's already in use try: mel.eval('catchQuiet("commandPort -securityWarning -name \\"commandportDefault\\";");') except Exception: pass # ============================================================================= # Constants # ============================================================================= # Command port name COMMAND_PORT_NAME = "commandportDefault" # Log levels LOG_DEBUG = 0 LOG_INFO = 1 LOG_WARNING = 2 LOG_ERROR = 3 # ============================================================================= # Configuration # ============================================================================= # Shelves to load SHELF_NAMES = ["Nexus_Manage", "Nexus_Modeling", "Nexus_Rigging", "Nexus_Animation", "Nexus_DevTools"] # User configuration file path (optional override) USER_CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".nexus_maya_config.py") # Tool packages configuration TOOL_CONFIG = { 'scripts': [ {'name': 'animation_tools', 'path': 'animation_tools'}, {'name': 'modeling_tools', 'path': 'modeling_tools'}, {'name': 'rigging_tools', 'path': 'rigging_tools'}, {'name': 'manage_tools', 'path': 'manage_tools'}, ], 'plugins': [ {'name': 'ngskintools2.mll', 'path': 'rigging_tools/ngskintools2/plug-ins/2023'}, {'name': 'MGPicker_2023x64.mll', 'path': 'animation_tools/mgpicker/MGPicker_Program/Plug-ins'}, ], 'icons': [ {'name': 'gs_toolbox', 'path': 'modeling_tools/gs_toolbox/icons'}, {'name': 'modit', 'path': 'modeling_tools/ModIt/Icons/Theme_Classic'}, {'name': 'creaseplus', 'path': 'modeling_tools/creaseplus/icons'}, {'name': 'ngskintools2', 'path': 'rigging_tools/ngskintools2/ui/images'}, {'name': 'mgpicker', 'path': 'animation_tools/mgpicker/MGPicker_Program/Icons'}, {'name': 'atools', 'path': 'animation_tools/atools/img'}, {'name': 'dwpicker', 'path': 'animation_tools/dwpicker/icons'}, {'name': 'studiolibrary', 'path': 'animation_tools/studiolibrary/studiolibrary/resource/icons'}, {'name': 'studiolibrary_maya', 'path': 'animation_tools/studiolibrary/studiolibrarymaya/icons'}, {'name': 'springmagic', 'path': 'animation_tools/springmagic/icons'}, ], } # Maya version compatibility check MAYA_MIN_VERSION = 2018 # Debug control: set environment variable TOOL_DEBUG=1 for verbose logs TOOL_DEBUG = os.environ.get('TOOL_DEBUG', '0') == '1' # Global state - capture __file__ at module level before executeDeferred try: _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) except NameError: _SCRIPT_DIR = None _ICONS_PATHS = {} _LOADED_PLUGINS = {} _COMMAND_PORT_OPENED = False _ADDED_SCRIPT_PATHS = [] # ============================================================================= # Utility Functions # ============================================================================= def _log(msg, level=LOG_INFO): """ Print log message with level control Args: msg: Message to print level: LOG_DEBUG, LOG_INFO, LOG_WARNING, or LOG_ERROR """ if level == LOG_DEBUG and not TOOL_DEBUG: return prefix_map = { LOG_DEBUG: "[Tool:DEBUG]", LOG_INFO: "[Tool]", LOG_WARNING: "[Tool:WARNING]", LOG_ERROR: "[Tool:ERROR]" } prefix = prefix_map.get(level, "[Tool]") print(f"{prefix} {msg}") def _tool_log(msg): """Print debug message if TOOL_DEBUG is enabled""" _log(msg, LOG_DEBUG) def _tool_print(msg): """Print info message""" _log(msg, LOG_INFO) def _tool_warning(msg): """Print warning message""" _log(msg, LOG_WARNING) def _tool_error(msg): """Print error message""" _log(msg, LOG_ERROR) def _get_script_dir(): """Get the directory containing this script (with fallback)""" global _SCRIPT_DIR if _SCRIPT_DIR: return _SCRIPT_DIR # Try multiple methods to get script directory try: _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) except NameError: try: import inspect _SCRIPT_DIR = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) except Exception: # Last resort: use current working directory's scripts folder _SCRIPT_DIR = os.getcwd() return _SCRIPT_DIR def _norm_path(path): """Normalize path with forward slashes""" return os.path.normpath(path).replace('\\', '/') def _resolve_relative_path(path, base_dir): """Resolve relative path against base directory""" if os.path.isabs(path): return path return os.path.normpath(os.path.join(base_dir, path)) def _safe_mel_eval(cmd): """Execute MEL command safely, return None on error""" try: return mel.eval(cmd) except Exception: return None def _check_maya_version(): """Check if Maya version meets minimum requirements""" try: maya_version = int(cmds.about(version=True).split()[0]) if maya_version < MAYA_MIN_VERSION: _tool_warning(f"Maya {maya_version} detected. Minimum supported version is {MAYA_MIN_VERSION}") return False _tool_log(f"Maya version: {maya_version}") return True except Exception as e: _tool_warning(f"Could not determine Maya version: {e}") return True # Continue anyway def _find_file_in_paths(filename, search_paths): """ Find file in a list of search paths Args: filename: Name of file to find search_paths: List of directory paths to search Returns: Full path to file if found, None otherwise """ for p in search_paths: if not p: continue candidate = os.path.join(p, filename) if os.path.exists(candidate): return candidate return None def _validate_tool_config(): """ Validate TOOL_CONFIG structure Returns: True if valid, False otherwise """ try: required_keys = ['scripts', 'plugins', 'icons'] for key in required_keys: if key not in TOOL_CONFIG: _tool_error(f"Missing required key in TOOL_CONFIG: {key}") return False if not isinstance(TOOL_CONFIG[key], list): _tool_error(f"TOOL_CONFIG['{key}'] must be a list") return False # Validate each item has required fields for idx, item in enumerate(TOOL_CONFIG[key]): if not isinstance(item, dict): _tool_error(f"TOOL_CONFIG['{key}'][{idx}] must be a dict") return False if 'name' not in item: _tool_error(f"TOOL_CONFIG['{key}'][{idx}] missing 'name' field") return False if 'path' not in item: _tool_error(f"TOOL_CONFIG['{key}'][{idx}] missing 'path' field") return False _tool_log("TOOL_CONFIG validation passed") return True except Exception as e: _tool_error(f"Error validating TOOL_CONFIG: {e}") return False def _verify_shelf_icon_references(search_paths): """Verify that shelf icon references can be resolved""" if not TOOL_DEBUG: return # Only verify in debug mode try: for shelf_name in SHELF_NAMES: shelf_file = os.path.join(search_paths[-1] if search_paths else '', f'shelf_{shelf_name}.mel').replace('\\', '/') if not os.path.exists(shelf_file): shelf_file = os.path.join(os.path.dirname(_get_script_dir()), 'shelves', f'shelf_{shelf_name}.mel').replace('\\', '/') if not os.path.exists(shelf_file): continue try: with open(shelf_file, 'r', encoding='utf-8') as fh: data = fh.read() except: continue imgs = re.findall(r'-image\s+"([^"]+)"', data) for img in imgs: found = any(os.path.exists(os.path.join(p, img).replace('\\', '/')) for p in search_paths) if not found: _tool_warning(f"Icon '{img}' not found for shelf '{shelf_name}'") except Exception as e: _tool_log(f"Icon verification error: {e}") def _remove_shelf_config(shelf_name): """Remove shelf config from Maya prefs to force reload""" try: maya_version = cmds.about(version=True).split()[0] maya_app_dir = os.environ.get('MAYA_APP_DIR', '') if maya_app_dir: shelf_config = os.path.join(maya_app_dir, maya_version, "prefs", "shelves", f"shelf_{shelf_name}.mel") if os.path.exists(shelf_config): os.remove(shelf_config) _tool_log(f"[Tool] Removed cached shelf config: {shelf_name}") except Exception as e: _tool_log(f"[Tool] Could not remove shelf config: {e}") # ============================================================================= # Setup Functions # ============================================================================= def setup_icon_paths(): """Configure icon paths in XBMLANGPATH environment variable""" try: script_dir = _get_script_dir() icon_configs = TOOL_CONFIG.get('icons', []) if not icon_configs: _tool_log("[Tool] No icons configured") return # Build icon paths dictionary icon_paths = {} for cfg in icon_configs: name = cfg.get('name', '') path = cfg.get('path', '') if not (name and path): continue full_path = _norm_path(_resolve_relative_path(path, script_dir)) if os.path.exists(full_path): icon_paths[name] = full_path else: _tool_warning(f"Icon path not found: {full_path}") # Check if already configured global _ICONS_PATHS if _ICONS_PATHS and _ICONS_PATHS == icon_paths: _tool_log("[Tool] Icon paths already configured") return _ICONS_PATHS = icon_paths # Update XBMLANGPATH xbmlangpath = os.environ.get('XBMLANGPATH', '') paths = [_norm_path(p.strip()) for p in xbmlangpath.split(os.pathsep) if p.strip()] # Add icon paths to front for icon_path in icon_paths.values(): if icon_path not in paths: paths.insert(0, icon_path) # Set environment variable new_xbmlangpath = os.pathsep.join(paths) os.environ['XBMLANGPATH'] = new_xbmlangpath # Update MEL environment with proper escaping try: # Escape backslashes and quotes for MEL safe_path = new_xbmlangpath.replace('\\', '/').replace('"', '\\"') mel.eval(f'putenv "XBMLANGPATH" "{safe_path}";') except Exception: pass _tool_log(f"[Tool] ✓ Icon paths configured: {len(icon_paths)} package(s)") _verify_shelf_icon_references(paths) except Exception as e: _tool_error(f"Error setting up icon paths: {e}") def load_tool_shelves(): """Load Nexus shelves into Maya""" try: # Determine shelf paths shelf_paths = os.environ.get('MAYA_SHELF_PATH', '') if not shelf_paths: script_dir = _get_script_dir() shelf_paths = os.path.join(os.path.dirname(script_dir), "shelves") if not os.path.exists(shelf_paths): _tool_warning("Shelf directory not found, skipping") return shelf_path_list = [p.strip() for p in shelf_paths.split(os.pathsep) if p.strip()] # Load each shelf loaded_count = 0 for shelf_name in SHELF_NAMES: # Find shelf file shelf_file_found = None for shelf_path in shelf_path_list: shelf_file = _norm_path(os.path.join(shelf_path, f"shelf_{shelf_name}.mel")) if os.path.exists(shelf_file): shelf_file_found = shelf_file _tool_log(f"[Tool] Found shelf: {shelf_file}") break if not shelf_file_found: _tool_warning(f"Shelf not found: shelf_{shelf_name}.mel") continue # Remove existing shelf if cmds.shelfLayout(shelf_name, exists=True): try: cmds.deleteUI(shelf_name, layout=True) _tool_log(f"[Tool] Removed existing shelf: {shelf_name}") except Exception as e: _tool_log(f"[Tool] Could not remove shelf: {e}") # Remove cached config _remove_shelf_config(shelf_name) try: # Disable shelf saving _safe_mel_eval('optionVar -intValue "saveLastLoadedShelf" 0;') # Create shelf layout using Python API instead of MEL try: # Get shelf parent using safe MEL evaluation shelf_parent = mel.eval('global string $gShelfTopLevel; $temp = $gShelfTopLevel;') cmds.setParent(shelf_parent) cmds.shelfLayout(shelf_name, cellWidth=35, cellHeight=34) except Exception as e: _tool_log(f"[Tool] Error creating shelf layout: {e}") raise if not cmds.shelfLayout(shelf_name, exists=True): raise RuntimeError(f"Failed to create shelf layout: {shelf_name}") # Load shelf content with safe path handling _safe_mel_eval(f'setParent {shelf_name};') # Use forward slashes for cross-platform compatibility safe_shelf_path = shelf_file_found.replace('\\', '/') mel.eval(f'source "{safe_shelf_path}";') mel.eval(f'shelf_{shelf_name}();') # Verify loaded buttons = cmds.shelfLayout(shelf_name, query=True, childArray=True) or [] _tool_print(f"[Tool] ✓ Shelf loaded: {shelf_name} ({len(buttons)} buttons)") loaded_count += 1 # Remove config again to prevent saving _remove_shelf_config(shelf_name) except Exception as e: _tool_error(f"Error loading shelf {shelf_name}: {e}") _tool_log(f"[Tool] ✓ Shelves: {loaded_count}/{len(SHELF_NAMES)} loaded") except Exception as e: _tool_error(f"Error loading shelves: {e}") def load_tool_plugins(): """Load Nexus plugins""" try: script_dir = _get_script_dir() plugin_configs = TOOL_CONFIG.get('plugins', []) if not plugin_configs: _tool_log("[Tool] No plugins configured") return loaded_count = 0 for cfg in plugin_configs: plugin_name = cfg.get('name', '') plugin_rel_path = cfg.get('path', '') env_var = cfg.get('env_var', 'MAYA_PLUG_IN_PATH') if not plugin_name: continue _tool_log(f"[Tool] Processing plugin: {plugin_name}") # First check the configured path parameter plugin_path = None if plugin_rel_path: # Build full path: script_dir/path/plugin_name full_path = _norm_path(os.path.join(script_dir, plugin_rel_path, plugin_name)) if os.path.exists(full_path): plugin_path = full_path _tool_log(f"[Tool] Found plugin at configured path: {plugin_path}") # If the configuration path is not found, search the environment variable path. if not plugin_path: plugin_path = _find_file_in_paths(plugin_name, os.environ.get(env_var, '').split(os.pathsep)) # Finally, try the script directory and the parent plug-ins folder. if not plugin_path: search_dirs = [ script_dir, os.path.join(os.path.dirname(script_dir), 'plug-ins') ] plugin_path = _find_file_in_paths(plugin_name, search_dirs) if not plugin_path: _tool_warning(f"Plugin not found: {plugin_name}") continue # Add plugin directory to environment plugin_dir = _norm_path(os.path.dirname(plugin_path)) current_paths = [p for p in os.environ.get(env_var, '').split(os.pathsep) if p] if plugin_dir not in current_paths: os.environ[env_var] = os.pathsep.join([plugin_dir] + current_paths) # Get plugin basename plugin_basename = os.path.splitext(os.path.basename(plugin_path))[0] # Check if already loaded try: already_loaded = bool(cmds.pluginInfo(plugin_basename, query=True, loaded=True)) except Exception: already_loaded = False if already_loaded: _tool_log(f"[Tool] Plugin already loaded: {plugin_basename}") _LOADED_PLUGINS[plugin_basename] = plugin_path continue # Load plugin try: cmds.loadPlugin(plugin_path, quiet=True) _tool_print(f"[Tool] ✓ Plugin loaded: {plugin_basename}") loaded_count += 1 _LOADED_PLUGINS[plugin_basename] = plugin_path except Exception: try: # Fallback: load by basename cmds.loadPlugin(plugin_basename, quiet=True) _tool_print(f"[Tool] ✓ Plugin loaded: {plugin_basename}") loaded_count += 1 _LOADED_PLUGINS[plugin_basename] = plugin_path except Exception as e: _tool_error(f"Error loading plugin {plugin_basename}: {e}") _tool_log(f"[Tool] ✓ Plugins: {loaded_count}/{len(plugin_configs)} loaded") except Exception as e: _tool_error(f"Error loading plugins: {e}") def load_tool_scripts(): """Add Nexus script paths to sys.path""" try: script_dir = _get_script_dir() script_configs = TOOL_CONFIG.get('scripts', []) if not script_configs: _tool_log("[Tool] No scripts configured") return loaded = 0 for cfg in script_configs: name = cfg.get('name', '') path = cfg.get('path', '') if not (name and path): continue full_path = os.path.normpath(os.path.join(script_dir, path)) if not os.path.exists(full_path): _tool_warning(f"Script path not found: {full_path}") continue if full_path not in sys.path: sys.path.insert(0, full_path) _ADDED_SCRIPT_PATHS.append(full_path) _tool_log(f"[Tool] Added script path: {name} -> {full_path}") loaded += 1 _tool_log(f"[Tool] ✓ Script paths: {loaded}/{len(script_configs)} added") except Exception as e: _tool_error(f"Error loading scripts: {e}") def _parse_mod_file_line(line, maya_version, modules_dir, module_name): """ Parse a single line from .mod file Args: line: Line to parse maya_version: Current Maya version modules_dir: Modules directory path module_name: Name of the module Returns: Tuple of (in_correct_version, current_module_root) or None """ parts = line.split() if len(parts) < 4: return None # Detect current platform current_platform = 'win64' if os.name == 'nt' else ('linux' if sys.platform.startswith('linux') else 'mac') # Check for version and platform restrictions has_version_requirement = False version_matches = False platform_matches = True # Assume match if no platform specified for part in parts: if part.startswith('MAYAVERSION:'): has_version_requirement = True required_version = int(part.split(':')[1]) version_matches = (required_version == maya_version) if part.startswith('PLATFORM:'): required_platform = part.split(':')[1].lower() platform_matches = (required_platform == current_platform) # Both version and platform must match (or not be specified) if has_version_requirement: if version_matches and platform_matches: # Get the module root path (last parameter) current_module_root = parts[-1] if current_module_root.startswith('..'): current_module_root = _norm_path(os.path.join(modules_dir, current_module_root)) _tool_print(f"{module_name}: Maya {maya_version} {current_platform} matched, root: {current_module_root}") return (True, current_module_root) else: return (False, None) # If there are no version restrictions, it works for all versions (but still check platform) if platform_matches: current_module_root = parts[-1] if current_module_root.startswith('..'): current_module_root = _norm_path(os.path.join(modules_dir, current_module_root)) _tool_print(f"{module_name}: No version restriction, root: {current_module_root}") return (True, current_module_root) return (False, None) def _load_plugins_from_directory(plugins_dir, mod_file): """ Load all plugins from a directory Args: plugins_dir: Directory containing plugins mod_file: Name of .mod file for logging Returns: Number of plugins loaded """ plugins_loaded = 0 if not os.path.exists(plugins_dir): _tool_log(f"Plugin dir not found: {plugins_dir}") return 0 _tool_log(f"Checking plugin dir: {plugins_dir}") for plugin_file in os.listdir(plugins_dir): if not plugin_file.endswith(('.mll', '.so', '.bundle', '.py')): continue plugin_name = os.path.splitext(plugin_file)[0] plugin_full_path = _norm_path(os.path.join(plugins_dir, plugin_file)) # Check if already tracked if plugin_name in _LOADED_PLUGINS: _tool_log(f"Plugin already tracked: {plugin_name}") continue # Check if already loaded try: is_loaded = cmds.pluginInfo(plugin_name, query=True, loaded=True) except: is_loaded = False if not is_loaded: try: cmds.loadPlugin(plugin_full_path, quiet=False) _tool_print(f"✓ Plugin loaded: {plugin_name} (from {mod_file})") plugins_loaded += 1 _LOADED_PLUGINS[plugin_name] = plugin_full_path except Exception as e: _tool_error(f"Failed to load {plugin_name}: {e}") else: _tool_log(f"Plugin already loaded: {plugin_name}") _LOADED_PLUGINS[plugin_name] = plugin_full_path return plugins_loaded def _add_path_to_environment(env_var, path): """ Add a path to an environment variable Args: env_var: Environment variable name (e.g., 'MAYA_SCRIPT_PATH') path: Path to add Returns: True if added, False if already exists """ if not os.path.exists(path): _tool_warning(f"Path not found for {env_var}: {path}") return False # Get current paths current_value = os.environ.get(env_var, '') current_paths = [p.strip() for p in current_value.split(os.pathsep) if p.strip()] # Normalize the new path norm_path = _norm_path(path) # Check if already in path if norm_path in [_norm_path(p) for p in current_paths]: _tool_log(f"{env_var} already contains: {norm_path}") return False # Add to front of path list current_paths.insert(0, norm_path) new_value = os.pathsep.join(current_paths) os.environ[env_var] = new_value # Update MEL environment if needed if env_var in ['XBMLANGPATH', 'MAYA_SCRIPT_PATH', 'MAYA_PLUG_IN_PATH']: try: safe_value = new_value.replace('\\', '/').replace('"', '\\"') mel.eval(f'putenv "{env_var}" "{safe_value}";') except Exception as e: _tool_warning(f"Could not update MEL environment for {env_var}: {e}") # Also add PYTHONPATH to sys.path for Python imports if env_var == 'PYTHONPATH': if norm_path not in sys.path: sys.path.insert(0, norm_path) _tool_log(f"Added to sys.path: {norm_path}") _tool_print(f"✓ Added to {env_var}: {os.path.basename(norm_path)}") return True def _load_user_config(): """ Load user configuration from optional config file Allows users to override default TOOL_CONFIG settings """ global TOOL_CONFIG, SHELF_NAMES if not os.path.exists(USER_CONFIG_FILE): _tool_log(f"No user config file found at: {USER_CONFIG_FILE}") return try: _tool_print(f"Loading user configuration from: {USER_CONFIG_FILE}") # Create a namespace for user config user_namespace = {} # Execute user config file with open(USER_CONFIG_FILE, 'r', encoding='utf-8') as f: exec(f.read(), user_namespace) # Override TOOL_CONFIG if provided if 'TOOL_CONFIG' in user_namespace: user_config = user_namespace['TOOL_CONFIG'] # Merge configurations for key in ['scripts', 'plugins', 'icons']: if key in user_config: if key in TOOL_CONFIG: # Extend existing config TOOL_CONFIG[key].extend(user_config[key]) else: # Add new key TOOL_CONFIG[key] = user_config[key] _tool_print("✓ User TOOL_CONFIG loaded and merged") # Override SHELF_NAMES if provided if 'SHELF_NAMES' in user_namespace: user_shelves = user_namespace['SHELF_NAMES'] if isinstance(user_shelves, list): # Extend shelf list for shelf in user_shelves: if shelf not in SHELF_NAMES: SHELF_NAMES.append(shelf) _tool_print(f"✓ User SHELF_NAMES loaded: {user_shelves}") _tool_print("✓ User configuration applied successfully") except Exception as e: _tool_error(f"Error loading user config: {e}") import traceback traceback.print_exc() def load_project_modules(): """Load plugins defined in .mod files from the modules directory.""" try: # Get current Maya version maya_version = int(cmds.about(version=True).split()[0]) # Get project root directory script_dir = _get_script_dir() project_root = os.path.dirname(os.path.dirname(script_dir)) modules_dir = _norm_path(os.path.join(project_root, "modules")) if not os.path.exists(modules_dir): _tool_log(f"Modules directory not found: {modules_dir}") return # Find all .mod files mod_files = [f for f in os.listdir(modules_dir) if f.endswith('.mod')] if not mod_files: _tool_log(f"No .mod files found in: {modules_dir}") return _tool_print(f"Found {len(mod_files)} module(s): {', '.join(mod_files)}") # TWO-PASS APPROACH: First configure all environment variables, then load plugins # This ensures dependencies are available before plugins initialize # PASS 1: Configure all environment variables (including MAYA_PLUG_IN_PATH paths) # Track processed modules to avoid duplicates processed_modules = set() for mod_file in mod_files: mod_path = os.path.join(modules_dir, mod_file) module_name = os.path.splitext(mod_file)[0] try: current_module_root = None in_correct_version = False module_key = None with open(mod_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() # Skip comments and empty lines if not line or line.startswith('#') or line.startswith('//'): continue # Parse module definition lines (starting with +) if line.startswith('+'): result = _parse_mod_file_line(line, maya_version, modules_dir, module_name) if result: in_correct_version, current_module_root = result # Create unique key for this module version module_key = f"{module_name}:{maya_version}:{current_module_root}" # Skip if already processed if module_key in processed_modules: _tool_log(f"Skipping duplicate module definition: {module_key}") in_correct_version = False current_module_root = None continue processed_modules.add(module_key) else: in_correct_version = False current_module_root = None module_key = None # Process ALL environment variables in first pass (including MAYA_PLUG_IN_PATH to env, but don't load plugins) elif in_correct_version and current_module_root and ('+:=' in line or '+=' in line): if line.startswith('MAYA_PLUG_IN_PATH'): # Add plugin directory to MAYA_PLUG_IN_PATH environment, but don't load plugins yet plugin_path_relative = line.split('+:=')[-1].strip() if '+:=' in line else line.split('+=')[-1].strip() plugins_dir = _norm_path(os.path.join(current_module_root, plugin_path_relative)) _add_path_to_environment('MAYA_PLUG_IN_PATH', plugins_dir) elif line.startswith('MAYA_SCRIPT_PATH'): script_path_relative = line.split('+:=')[-1].strip() if '+:=' in line else line.split('+=')[-1].strip() script_path = _norm_path(os.path.join(current_module_root, script_path_relative)) _add_path_to_environment('MAYA_SCRIPT_PATH', script_path) if script_path not in sys.path: sys.path.insert(0, script_path) elif line.startswith('XBMLANGPATH'): icon_path_relative = line.split('+:=')[-1].strip() if '+:=' in line else line.split('+=')[-1].strip() icon_path = _norm_path(os.path.join(current_module_root, icon_path_relative)) _add_path_to_environment('XBMLANGPATH', icon_path) else: # Handle PATH, PYTHONPATH, and other environment variables try: env_var = line.split('+')[0].strip() rel_path = line.split('+:=')[-1].strip() if '+:=' in line else line.split('+=')[-1].strip() full_path = _norm_path(os.path.join(current_module_root, rel_path)) _add_path_to_environment(env_var, full_path) except Exception as e: _tool_log(f"Could not process environment variable line: {line}, error: {e}") except Exception as e: _tool_error(f"Error processing environment variables in {mod_file}: {e}") # PASS 2: Now load plugins with all dependencies configured _tool_log("Environment variables configured, now loading plugins...") plugins_loaded = 0 processed_modules.clear() # Reset for second pass for mod_file in mod_files: mod_path = os.path.join(modules_dir, mod_file) module_name = os.path.splitext(mod_file)[0] try: current_module_root = None in_correct_version = False module_key = None with open(mod_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#') or line.startswith('//'): continue if line.startswith('+'): result = _parse_mod_file_line(line, maya_version, modules_dir, module_name) if result: in_correct_version, current_module_root = result module_key = f"{module_name}:{maya_version}:{current_module_root}" # Skip if already processed if module_key in processed_modules: _tool_log(f"Skipping duplicate module definition for plugin loading: {module_key}") in_correct_version = False current_module_root = None continue processed_modules.add(module_key) else: in_correct_version = False current_module_root = None module_key = None # Now load plugins elif line.startswith('MAYA_PLUG_IN_PATH') and in_correct_version and current_module_root: if '+:=' in line or '+=' in line: plugin_path_relative = line.split('+:=')[-1].strip() if '+:=' in line else line.split('+=')[-1].strip() plugins_dir = _norm_path(os.path.join(current_module_root, plugin_path_relative)) plugins_loaded += _load_plugins_from_directory(plugins_dir, mod_file) except Exception as e: _tool_error(f"Error processing {mod_file}: {e}") import traceback traceback.print_exc() if plugins_loaded > 0: _tool_print(f"✓ Total {plugins_loaded} module plugin(s) loaded") else: _tool_print("No module plugins loaded") except Exception as e: _tool_error(f"Error loading modules: {e}") import traceback traceback.print_exc() def setup_command_port(): """Setup command port for external connections""" global _COMMAND_PORT_OPENED try: # Check if already open try: port_exists = bool(mel.eval(f'commandPort -q -name "{COMMAND_PORT_NAME}"')) except Exception: port_exists = False if port_exists: _tool_log("Command port already open") _COMMAND_PORT_OPENED = True return # Open command port mel.eval(f'commandPort -securityWarning -name "{COMMAND_PORT_NAME}";') _tool_log("✓ Command port opened") _COMMAND_PORT_OPENED = True except Exception as e: _tool_log(f"[Tool] Could not open command port: {e}") # ============================================================================= # Main Initialization # ============================================================================= def initialize_tool(): """Main initialization function called on Maya startup""" try: print("=" * 80) print("[Tool] Nexus Plugin System - Initializing...") print("=" * 80) # Validate configuration if not _validate_tool_config(): _tool_error("Configuration validation failed, continuing with caution...") # Check Maya version compatibility if not _check_maya_version(): _tool_warning("Running on unsupported Maya version") # First, set the icon path so that the shelf can find the icon when it loads. setup_icon_paths() # Load project modules load_project_modules() # Use Maya standard module systems for automatic loading. # Setup command port setup_command_port() # Load components in order load_tool_scripts() load_tool_plugins() load_tool_shelves() print("=" * 80) print("[Tool] Nexus Plugin System - Ready!") print("=" * 80) except Exception as e: print(f"[Tool] Initialization error: {e}") import traceback traceback.print_exc() # Defer initialization until Maya is fully loaded maya.utils.executeDeferred(initialize_tool) # ============================================================================= # Cleanup on Exit # ============================================================================= def cleanup_on_exit(): """Cleanup resources when Maya exits""" try: _tool_log("[Tool] Cleanup initiated...") # Close command port global _COMMAND_PORT_OPENED if _COMMAND_PORT_OPENED: try: if mel.eval(f'commandPort -q -name "{COMMAND_PORT_NAME}"'): mel.eval(f'commandPort -cl "{COMMAND_PORT_NAME}"') _tool_log("✓ Command port closed") except Exception as e: _tool_log(f"Could not close command port: {e}") # Unload plugins if _LOADED_PLUGINS: unloaded = 0 for plugin_basename, plugin_path in list(_LOADED_PLUGINS.items()): try: # Check if still loaded try: is_loaded = bool(cmds.pluginInfo(plugin_basename, query=True, loaded=True)) except Exception: is_loaded = False if is_loaded: try: cmds.unloadPlugin(plugin_basename, force=True) _tool_log(f"[Tool] ✓ Plugin unloaded: {plugin_basename}") unloaded += 1 except Exception as e: _tool_log(f"[Tool] Could not unload plugin {plugin_basename}: {e}") except Exception: continue if unloaded > 0: _tool_log(f"[Tool] ✓ Plugins unloaded: {unloaded}/{len(_LOADED_PLUGINS)}") # Clean up script paths from sys.path global _ADDED_SCRIPT_PATHS if _ADDED_SCRIPT_PATHS: for path in _ADDED_SCRIPT_PATHS: try: if path in sys.path: sys.path.remove(path) except Exception: pass _tool_log(f"[Tool] ✓ Script paths cleaned: {len(_ADDED_SCRIPT_PATHS)}") # Clear global state _LOADED_PLUGINS.clear() _ICONS_PATHS.clear() _ADDED_SCRIPT_PATHS.clear() _tool_log("[Tool] ✓ Cleanup complete") except Exception as e: _tool_log(f"[Tool] Cleanup error: {e}") # Register cleanup function atexit.register(cleanup_on_exit)