[maya] userSetup.py の動作・実行の仕組み

```
※記事に関しましてご注意下さい
こちらでは普段当たり前のように使用している機能・動作が「実はどうやって動いているのか」を探るべく調査した内容になります。
仕組みを知ることで利点・欠点をつかみ、未来に起こるトラブルを防ぐ・拡張した使用方法に発展させるなどを考えています。
あくまでも個人調査のメモです。間違い・勘違いが含まれている可能性もありますので予めご了承ください。
```
## 調査のきっかけ
最近（2020/6 ～）幾つかの maya 用のウィルス報告がありました。影響を受けた方も大勢いらっしゃると思います。
これらはどれもデータ・ファイルを介して最終的にアプリ動作に便乗するものです。
そして起動時に発動するスクリプトといえば、、、 userSetup.py です。

userSetup.py はウィルス話でなくとも通常の個人・プロダクション環境設定の一手法として使用されます。
これまで自分でも大体の動作として使う側に専念することが多かったため、せっかくなので userSetup.py が動く仕組みを調べようと思いました。

## 調べた結果
これまで「なんとなく」ブラックボックスとしてとらえていましたが、ものすごくシンプルな仕組みです。

```
sys.path のフォルダ以下にある userSetup.py を片っ端に execfile で実行しています。
```
です。これがすべてです。

※手元で確認できるもっとも古いバージョンが Maya 2008 ですが、そのころから基本的な実装は変わっていませんでした。
※python3 では execfile が無くなるためこの記述は変更・更新されると思います。
⇒ [Maya2022 以降のバージョンについて最後に追記しました](#maya2022-以降のバージョンについて)

## どのように書かれているか

該当する .py ファイルは、以下にあります。

```
C:\Program Files\Autodesk\Maya2008\Python\Lib\site-packages\maya\app\startup\basic.py
～
C:\Program Files\Autodesk\Maya2020\Python\Lib\site-packages\maya\app\startup\basic.py
```
必要箇所を抜き出すと以下になります。

```Python:basic.py
def setupScriptPaths():
    # ....省略....
    
    # Per-version prefs scripts dir (eg .../maya8.5/prefs/scripts)
    prefsDir = cmds.internalVar( userPrefDir=True )
    sys.path.append( os.path.join( prefsDir, 'scripts' ) )
    
    # Per-version scripts dir (eg .../maya8.5/scripts)
    scriptDir = cmds.internalVar( userScriptDir=True )
    sys.path.append( os.path.dirname(scriptDir) )
    
    # User application dir (eg .../maya/scripts)
    appDir = cmds.internalVar( userAppDir=True )
    sys.path.append( os.path.join( appDir, 'scripts' ) )

def executeUserSetup():
    # ....省略....

    try:
        for path in sys.path:
            scriptPath = os.path.join( path, 'userSetup.py' )
            if os.path.isfile( scriptPath ):
                import __main__
                execfile( scriptPath, __main__.__dict__ )
    except Exception, e:
        sys.stderr.write( "Failed to execute userSetup.py\n" )
        sys.stderr.write( str( e ) )	
```

これらの関数は basic.py 内で呼び出されて（最近のバージョンでは batch.py gui.py で呼び出されて）実行されます。

```Python:basic.py
# ....省略....

# Set up sys.path to include Maya-specific user script directories.
setupScriptPaths()

if not os.environ.has_key('MAYA_SKIP_USERSETUP_PY'):
    # Run the user's userSetup.py if it exists
    executeUserSetup()
```

※上のコードから次のことも分かります。
`MAYA_SKIP_USERSETUP_PY を環境変数に設定しておくと executeUserSetup() は実行されない`

### ★Maya2022 以降のバージョンについて（後日追記）
basic.py, batch.py, gui.py の記述は更新されていますが大枠仕組みは同様です。
※それと内緒で１つ仕組みが追加されています。(いつか余裕あればこの部分追加するかも)

## 既存の sys.path はどうなっている？
詳細はお使いの環境によりますが、ざっくりと以下の内容でした。

```
１．モジュールパスのスクリプトフォルダ（複数）
２．maya の python 的にベースとなるフォルダ（複数）
３．ユーザー環境のスクリプトフォルダ（複数）
```

手元の Maya2018 環境では確認したところ以下の通りです。（特殊なものは除外しています）

```Python:sys.path確認コード
import sys
for path in sys.path:
    if not path:
        continue
    print (path)
```
※ ↑ 変数名間違い訂正しました

```Text:実行例
C:\Program Files\Autodesk\Maya2018\plug-ins\ATF\scripts
C:\Program Files\Autodesk\Maya2018\plug-ins\MASH\scripts
C:\Program Files\Autodesk\Maya2018\plug-ins\fbx\scripts
C:\Program Files\Autodesk\Maya2018\plug-ins\camd\scripts
C:\solidangle\mtoadeploy\2018\scripts
C:\Program Files\Autodesk\Maya2018\plug-ins\substance\scripts
C:\Program Files\Autodesk\Maya2018\plug-ins\xgen\scripts

C:\Program Files\Autodesk\Maya2018\bin\python27.zip
C:\Program Files\Autodesk\Maya2018\Python\DLLs
C:\Program Files\Autodesk\Maya2018\Python\lib
C:\Program Files\Autodesk\Maya2018\Python\lib\plat-win
C:\Program Files\Autodesk\Maya2018\Python\lib\lib-tk
C:\Program Files\Autodesk\Maya2018\bin
C:\Program Files\Autodesk\Maya2018\Python
C:\Program Files\Autodesk\Maya2018\Python\lib\site-packages

C:\Program Files\Autodesk\Maya2018\bin\python27.zip\lib-tk

C:/Users/rdj/Documents/maya/2018/prefs/scripts
C:/Users/rdj/Documents/maya/2018/scripts
C:/Users/rdj/Documents/maya/scripts
```

## 最後に。意識すべき部分
以下に注意する必要がありそうです。

```
１．userSetup.py の実行順は sys.path 順である
２．Documents/maya/scripts 等のユーザーフォルダの userSetup.py は特別な扱いではなく
　　あくまでも sys.path にある userSetup.py の一つとして他と同列に実行されている
３．execfile で python スクリプトが実行されている

追記
４．終盤に実行される Documents/maya/～/scripts については
　　ここで何をするかでそれまでの設定を修正することも・覆すこともできてしまう
```

いい部分も問題部分も仕組みが公開されている、それはそれでスゴイことです。。。

## 後日追記
### ★Maya2022 以降のバージョンについて
#### Maya2022 以降では標準の python バージョンが python3 (python3.7) に変更されています。
以下いずれかの方法で python2 を指定しない限りは python3 で動作することになり、文法的にも用意するモジュール的にも注意が必要です。

- 起動オプションで `-pythonVar 2` を指定して maya を起動する
- 環境変数で `set MAYA_PYTHON_VERSION=2` を設定しておく
- ~~Maya.env に `MAYA_PYTHON_VERSION=2` を追記する~~ 　← 起動後に評価されるためこれはダメ

※userSetup.py に python2 特有の記述を行なっている場合には python3 モードで起動するとエラーが発生しますので要注意

```
import sys
print(sys.version)

# python2 で起動された場合
# 2.7.11 (default, Jul  1 2016, 02:08:48) [MSC v.1900 64 bit (AMD64)]

# python3 で起動された場合
# 3.7.7 (tags/v3.7.7:d7c567b08f, Mar 10 2020, 10:41:24) [MSC v.1900 64 bit (AMD64)]
```

#### execfile は past モジュールで置き換えられている
python2/python3 いずれで起動した場合にも execfile を互換使用するため、以下のライブラリが使用されています。

| name | url  |
|---|---|
| pypi | https://pypi.org/project/past/ |
| homepage | http://python-future.org/ |
| github | https://github.com/PythonCharmers/python-future 

実際には

```
C:\Program Files\Autodesk\Maya2022\Python27\Lib\site-packages\past
C:\Program Files\Autodesk\Maya2022\Python37\Lib\site-packages\past
```
にモジュールが準備されており、

```python:basic.py
...
from past.builtins import execfile
...
```

のようにして execfile が置き換えられ呼び出されています。