Files
Nexus/2023/scripts/userSetup.py
2026-01-22 00:06:13 +08:00

1059 lines
41 KiB
Python

#!/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)