Automating Script Install in Maya

Automating Script Install in Maya

In this post, I will cover the design of a tool installer script for Maya.

Preface

I am going start a new series that details in some of the key implementations of Swift shelf. Xwift is a shelf that contains specialized scripts when I was producing my animated film. I want to write some blogs to document them, in case there’s anyone out there that needs some inspiration.

Everything is developed and tested using Maya 2022.

Swift is not (well, not yet) open-source and therefore I will not share the whole script in my post. However, after reading my posts I believe you can implement your own, given some time and effort.

Goal

We want a script that helps install scripts into Maya so maya will automatically load all the custom made tools when starting up.

Algorithm

The algorithm is simple and the script needs to do two things:

  1. Modify (or create) a file called UserSetup.py under a directory that Maya checks every time when starting up, and this UserSetup.py will load our tools.
  2. Install (overwrite if necessary) all the scripts, plug-ins, and icons to appropriate directories that Maya loads when start.

Cross-platform Incompatiblities

Directory Difference

Interestingly, you are supposed to put scripts in different places with different OS. Which… further complicates the development. Therefore, I decided to develop two separate scripts that does the identical things but customized for Windows and macOS (I hate linux so it is out of the question). For example:

On Windows, scripts are placed under C:\Users\USER_NAME\Documents\maya\2022\scripts, icons are placed under C:\Users\USER_NAME\Documents\maya\2022\prefs\icons.

On macOS, scripts are placed under /Library/Preferences/Autodesk/Maya/2022/scripts/, icons are placed under /Library/Preferences/Autodesk/Maya/2022/prefs/icons.

Furthermore, if your system language is Simplified Chinese, the implementation of Maya in macOS, unlike Windows, can only go with system language, and for each language of Maya other than Windows Maya uses a different directory for custom tools. So even if you use other system language I think this problem will persist.

In this case if you want cross-language support, scripts are placed under /Library/Preferences/Autodesk/Maya/2022/zh_CN/scripts/, icons are placed under /Library/Preferences/Autodesk/Maya/2022/zh_CN/prefs/icons.

Slash and Backslash

Complicated enough? There’s more. You might already realized the directory in windows uses backslash, namely, \. On macOS and presumably Linux, file paths are separated by slash, namely, /. If you recall, we are writing scripts in Python, and that’s when backslash becomes super troublesome, because backslash is supposed to be used for escape symbols. Therefore in Python, \\ is what represents \, while / works as-is in Windows.

When manipulating file paths in Windows, this soon becomes extremely intolerable, so I wrote a little helper function:

# win_support: Convert filepath acquired by os to appropriate windows file pathformat.
# Takes in a str filepath and returns a str with all the \ replaced with /

def win_support(filepath):
    return filepath.replace('\\', '/')

Now everytime when you get a file path from python, pass that string through this function and this simple hack can save some lives.

Stupid? Absolutely. But does it work? Yes.


Preparation

Get and smartly assemble directories

Introduction

Now let’s get some important file locations sorted out. Like I mentioned above, these are places where Maya check for things.

Regardless of system versions, you can see a pattern of where most of the things are located. Take Windows as an example:

  • Script and MEL: C:\Users\USER_NAME\Documents\maya\2022\scripts
  • Plug-in: C:\Users\USER_NAME\Documents\maya\2022\plug-ins
  • Icon: C:\Users\USER_NAME\Documents\maya\2022\prefs\icons

Think of it this way:

  • Script and MEL: MAYA_APP_DIR\LATEST_MAYA_VERSION\scripts
  • Plug-in: MAYA_APP_DIR\LATEST_MAYA_VERSION\plug-ins
  • Icon: MAYA_APP_DIR\LATEST_MAYA_VERSION\prefs\icons

Oh! All I need is a function that can get MAYA_APP_DIR and LATEST_MAYA_VERSION, then I can assemble all the directories above!

How to get MAYA_APP_DIR from system

Well, turns out, using the os package, there is a very handy line of code that will get you to the User folder of your computer. That is, os.getenv("USERPROFILE") will take you to C:\Users\USER_NAME\. Let’s get MAYA_APP_DIR using this tool.

USER = win_support(os.getenv("USERPROFILE"))
MAYA_APP_DIR = "{0}/Documents/maya/".format(USER)

How to get LATEST_MAYA_VERSION from system

Well, all we need, is navigate to the directory from above, get all the versions of Maya as numbers, and find the max. Yes, I know, older version of Maya like from a decade ago has version labeled as “maya20XX” and this method won’t work but it’s too old. I think most people nowadays at least uses Maya 2017 so should be fine.

# get_latest_version: Get the latest version of Maya.
# Takes in a str MAYA_FOLDER and returns a str of VERSION in Maya, e.g. "2022"
def get_latest_version(MAYA_APP_DIR):
    version_folders = []
    for x in os.listdir(MAYA_APP_DIR):
        try:
            folder = int(x)
            version_folders += [folder]
        except: pass
    return str(max(version_folders))

Assemble everything

With the above implemented, everything else is easy. Using a simple {0}{1}.format(_, _) trick will make your code look clean and neat.

# MAYA_SCRIPT_FOLDER: Where maya stores all its scripts
MAYA_SCRIPT_FOLDER = "{0}{1}/scripts/".format(MAYA_APP_DIR,MAYA_VERSION)
# MAYA_SCRIPT_CACHE
MAYA_SCRIPT_CACHE = "{0}{1}/scripts/__pycache__".format(MAYA_APP_DIR,MAYA_VERSION) 
# MAYA_PLUGIN_FOLDER: Where maya stores all its scripts
MAYA_PLUGIN_FOLDER = "{0}{1}/plug-ins/".format(MAYA_APP_DIR,MAYA_VERSION)  
# MAYA_ICON_FOLDER: Create a /xwift/ folder in the Maya icon folder
MAYA_ICON_FOLDER = "{0}{1}/prefs/icons/xwift/".format(MAYA_APP_DIR,MAYA_VERSION)  

Setting up userSetup.py

What is userSetup.py?

Maya will execute everything in this script when start up, when placed in the right place.

Where should I place it?

In windows, custom startup file is stored under C:\Users\USER_NAME\Documents\maya\scripts\userSetup.py.

In macOS, custom startup file is stored under /Library/Preferences/Autodesk/Maya/scripts/userSetup.py.

What should I write in it?

I am getting a bit ahead of myself here, but here’s what’s contained in my userSetup.py.

# start Xwift

from maya import cmds

if not cmds.about(batch=True):
cmds.evalDeferred("import xwift_shelf; xwift_shelf.xwiftshelf()")

# end Xwift

Let me explain. When developing my shelf, I looked up Vasil Shotarov’s blog on how to make a Python shelf for maya, and I developed a modified version of his shelf. The entire shelf is contained in a file called xwift_shelf.py, and has a class named xwiftshelf that can be called using xwiftshelf().

Therefore, this script’s logic is as follows:

from maya import cmds so I can execute Maya commands, then load the xwiftshelf() in xwift_shelf.py (Which will then load everything else), defer it a bit so this process doesn’t collide with other important Maya startup processes.

What should I do with it?

All there is left for you to write a function that moves the userSetup.py to the Maya directory above or append it to the end of an already existed userSetup.py.

Show me the code.

Here’s a fancy version I implemented that copies a pre-written userSetup.py if such a file is not present, and ask the user to do it manually if such a file exist. The reason why I chose to do it is because I don’t want it to append the same thing over and over again during development, because I need to re-install my scripts many, many times.

def installUserSetup():
    xwiftUserSetupPath = os.path.dirname(os.path.abspath(__file__)) + "\\userSetup.py"
    if os.path.exists(xwiftUserSetupPath):
        print("[✓] [CHKDIR] xwift UserSetup file path at: " + xwiftUserSetupPath)
    else:
        print("[⍻] [CHKDIR] WARNING: Missing xwift UserSetup file path at: " + xwiftUserSetupPath)
    USER = win_support(os.getenv("USERPROFILE"))
    MAYA_MAIN_SCRIPTS_FOLDER = "{0}/Documents/maya/".format(USER)
    USER_SETUP = MAYA_MAIN_SCRIPTS_FOLDER + "scripts/userSetup.py"
    if os.path.exists(USER_SETUP):
        print("[⍻] [CHKDIR] WARNING: UserSetup file already exist at " + USER_SETUP + " .")
        popup_msg = "There exists an UserSetup.py file under \n" + USER_SETUP + " \n\n\nYou may need to copy everything in the UserSetup.py."
        ctypes.windll.user32.MessageBoxW(0, popup_msg, "Action Needed", 1)
    else:
        shutil.copyfile(xwiftUserSetupPath, USER_SETUP)
        print("[✓] [CHKDIR] Copied new UserSetup file to: " + USER_SETUP)

Getting the folder where the current script is located

This is a easy step yet important, because all other operations run in relative to this install script.

In my repository, I have my file organized like this (I omitted stuff that isn’t important):

xwift
    icons
        icon1.png, ...
    plug-ins
        plug-in1.mll, ...
    scripts
        script1.py 
        script2.mel, ...
    install_script_macos.py
    install_script_windows.py

No matter where you are in your computer, you can use the following line to get the location of the current install_script_OS.py:

# xwift_FOLDER: aka the folder this script is located
xwift_FOLDER = os.path.dirname(os.path.abspath(__file__))  

Assembling path of all the files of your custom toolbox

Now, let’s make the paths for all the paths of different things that our toolset need, using the file hierarchy above.

You will first need a little function to help you filter out the name of files that you want with a specific extension.

# filter_ext: filters out files in a directory with the same extention and returns a list of filenames that contains the list of filenames.
def filter_ext (directory, ext):
    file_list = []
    for basename in os.listdir(directory):
        if basename.endswith(ext):
            file_list.append(basename)
    return file_list

Then for each type of file, generate a list of file names with the given extension.

# SETUP_FILE = The shelf itself.
SETUP_FILE = xwift_FOLDER + "/scripts/xwift_shelf.py"

# SCRIPT_LIST: A list of str of scripts in /scripts folder.
PYTHON_LIST = filter_ext(xwift_FOLDER + "/scripts", ".py")
MEL_LIST = filter_ext(xwift_FOLDER + "/scripts", ".mel")
SCRIPT_LIST = PYTHON_LIST + MEL_LIST

# PLUGIN_LIST: List of plugins of ".mll" files
MLL_LIST = filter_ext(xwift_FOLDER + "/plug-ins", ".mll")
PLUGIN_LIST = MLL_LIST

# ICON_LIST: A list of str of icons in /icons folder.
PNG_LIST = filter_ext(xwift_FOLDER + "/icons", ".png")
JPG_LIST = filter_ext(xwift_FOLDER + "/icons", ".jpg")
ICON_LIST = PNG_LIST + JPG_LIST

Installation

Step 1: Install userSetup

Simple. Just run the installUserSetup() detailed above.

Step 2: Check if the folders are created

Normally, some of the folders, for example, plug-ins, isn’t created. When you try copy something into a non-existent folder, python will complain. Therefore, you need a simple function to check if the folders exists. If not, create it.

# chk_dir: Checks if the given directory exists, if not, create one.
def chk_dir(folder, target):
    if not os.path.isdir(target):
        print("[⍻] [CHKDIR] " + folder + " folder does not exist. ")
        os.makedirs(target)
        print("[✓] [CHKDIR] Created script folder under: ", target)
        return False
    if os.path.isdir(target):
        print("[✓] [CHKDIR] "+ folder + " folder already exists under: ", target)
        return True

Then, simply check if all the folders exist.

# Check if the Maya script folder exists, if not, create one.
chk_dir("Script", MAYA_SCRIPT_FOLDER)
chk_dir("Icon", MAYA_ICON_FOLDER)
chk_dir("Plug-In", MAYA_PLUGIN_FOLDER)

Step 3: Delete Maya Cache (New in 2022)

This step is not necessary, and is a new feature in 2022. However I find it a good habit.

Different from Maya 2020 and below, Maya 2022 will semi-compile every python script when launch. That is, before launching Maya:

|documents
    |maya
        |2022
            |scripts
                |your_script.py


After you launch Maya:

|documents
    |maya
        |2022
            |scripts
                |your_script.py
                |__pycache__
                    |your_script.pyc


And the way to do it is simple, simply recursively delete the folder using shutil.rmtree().

try:
    shutil.rmtree(MAYA_SCRIPT_CACHE)
    print("[✓] [CACHE ] Successfully cleared Python cache at: " + MAYA_SCRIPT_CACHE)
except OSError as e:
    print("[⍻] [CACHE ] WARNING: %s : %s" % (MAYA_SCRIPT_CACHE, e.strerror))
    print("[⍻] [CACHE ] WARNING: You are using Maya < 2020, or haven't restart since last install Xwift.")

Step 4: Let’s make wrapper functions!

Obviously, it would be tedious to hard-code every step of copying. Instead, let’s make a little helper function that helps us do it.

# install_element: helper function for install_script, install_icon, etc.
def install_element(element_name, target_folder, category, maya_path):
    target_path = os.path.join(maya_path, element_name)
    setup_file = xwift_FOLDER + target_folder + element_name
    shutil.copy(setup_file, target_path)
    print(category + " Installed " + element_name + " into: " + target_path)

Then, to install something, simply make wrapper functions using the helper function. Note I am using \\ to avoid the escaping symbol error.

# install_script: wrapper function for install_shelf. Installs a script into maya's .\scripts folder.
def install_script(script_name):
    install_element(script_name, "\\scripts\\", "[✓] [SCRIPT]", MAYA_SCRIPT_FOLDER)

# install_plugin: wrapper function for install_shelf.
def install_plugin(plugin_name):
    install_element(plugin_name, "\\plug-ins\\", "[✓] [PLUGIN]", MAYA_PLUGIN_FOLDER)

# install_icon: wrapper function for install_shelf. 
def install_icon(icon_name):
    install_element(icon_name, "\\icons\\", "[✓] [ ICON ]", MAYA_ICON_FOLDER)

Step 5: Do this to every single file!

Now that we harnessed the power to deliver any file to wherever we want, let’s do this to all files, thus completing the algorithm.

# Copy setup file into the Maya preferences folder
try:
    # Install all required elements
    for script in SCRIPT_LIST: install_script(script)
    for icon in ICON_LIST: install_icon(icon)
    for plugin in PLUGIN_LIST: install_plugin(plugin)
    return "[✓] [FINISH] All required files installed successfully!"
# Print out errors
except IOError as e: return "[X] [FINISH] I/O Error:\n" + str(e)

Conclusion

Whoa! That’s a lot of writing. I hope my words are understandable. I had given away 90% of all the code required to write such a script, and I hope it is a useful read. If you want to quickly say hi just shoot me a message using the contact portal.

Automating Script Install in Maya
Older post

Building Personal Website with Jekyll (6)

Newer post

Fix broken NURBS surface in Maya

Automating Script Install in Maya