Import internal exe modules from an external subprocess module (pyinstaller)

I’d like to import internal exe modules from an external process. I can do this fine from an IDE, but when I package the project with pyinstaller, and then run it, the modules can’t be found.

Here’s a high level illustration of the workflow:

Importing maya_app from within the external userSetup.py returns an error: ImportError: No modules named maya_app

The whole workflow works when I run it from an IDE (because it’s just normal python), but when I launch Maya from bundle.exe Maya doesn’t seem to retain any knowledge of the modules.

One special thing I do during the subprocess.Popen is insert Maya’s expected values for PYTHONPATH and PYTHONHOME into my environment variables because my tool is Python37 and Maya is Python27. This actually works and Maya uses its own python packages, but maybe it’s wiping the knowledge of my modules? It’s unclear how I’d test this.

I found this post on stack overflow but I couldn’t get it to work for my setup.

Is this even possible? Is there a way for Maya to inherit the modules during the subprocess.Popen? I’d like to solve this without exposing the code to the user in the exe directory, but it seems like that’s the only solution at this point.

Code:

launch_maya.py

import os
import sys
import subprocess

from PySide2 import QtWidgets


class Widget(QtWidgets.QWidget):
	def __init__(self, parent=None):
		super(Widget, self).__init__(parent)

		# GUI
		btn_launch = QtWidgets.QPushButton('launch maya')
		btn_launch.clicked.connect(self.on_launch)

		# Layout
		main_layout = QtWidgets.QHBoxLayout(self)
		main_layout.addWidget(btn_launch)
		self.setLayout(main_layout)

		# Root path exe vs ide
		if getattr(sys, 'frozen', False):
			self.root_path = sys._MEIPASS
		else:
			self.root_path = os.path.join(os.path.dirname(os.path.realpath(__file__)))

	def _set_app_envs(self):

		_envs = os.environ.copy()
		_envs['MAYA_SCRIPT_PATH'] = os.path.join(self.root_path, 'scripts').replace('\\', '/')

		# Python path envs
		_python_path_list = [
			'C:\\Program Files\\Autodesk\\Maya2020\\Python\\Lib\\site-packages',
			'C:\\Program Files\\Autodesk\\Maya2020\\Python\\DLLs',
			os.path.join(self.root_path, 'scripts').replace('\\', '/'),
			os.path.join(self.root_path, 'internal_source', 'maya_app')
		]

		# PYTHONPATH exe vs ide
		if getattr(sys, 'frozen', False):

			_envs['PYTHONPATH'] = os.pathsep.join(_python_path_list)
			_envs['PYTHONHOME'] = 'C:\\Program Files\\Autodesk\\Maya2020\\bin'

		else:
			_envs['PYTHONPATH'] += os.pathsep + os.pathsep.join(_python_path_list)

		return _envs

	def on_launch(self):

		# Maya file path
		file_path_abs = '{}/scenes/test.mb'.format(self.root_path).replace('\\', '/')
		print(file_path_abs)
		app_exe = r'C:/Program Files/Autodesk/Maya2020/bin/maya.exe'

		_envs = self._set_app_envs()

		if os.path.exists(file_path_abs):
			proc = subprocess.Popen(
				[app_exe, file_path_abs],
				env=_envs,
				stdin=subprocess.PIPE,
				stdout=subprocess.PIPE,
				stderr=subprocess.STDOUT,
				shell=True,
				creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
			)


if __name__ == "__main__":
	app = QtWidgets.QApplication(sys.argv)
	window = Widget()
	window.resize(400, 400)
	window.show()
	sys.exit(app.exec_())

bundle.spec

# -*- mode: python ; coding: utf-8 -*-
block_cipher = None

added_files = [
         ('./scenes', 'scenes'),
         ('./scripts', 'scripts')
         ]

a = Analysis(['launch_maya.py'],
             pathex=[
             'D:/GitStuff/mb-armada/example_files/exe_bundle',
             'D:/GitStuff/mb-armada/dependencies/Qt.py',
             'D:/GitStuff/mb-armada/venv/Lib/site-packages',
             ],
             binaries=[],
             datas=added_files,
             hiddenimports=['internal_source', 'internal_source.maya_app'],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='bundle',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='bundle')

maya_app.py

import os
import sys
import subprocess

from PySide2 import QtWidgets


class MainWidget(QtWidgets.QWidget):
	def __init__(self, parent=None):
		super(MainWidget, self).__init__(parent)

		# GUI
		btn_launch = QtWidgets.QPushButton('say hey')
		btn_launch.clicked.connect(self.on_say_hey)

		# Layout
		main_layout = QtWidgets.QHBoxLayout(self)
		main_layout.addWidget(btn_launch)
		self.setLayout(main_layout)
		print('I should be alive')

	def on_say_hey(self):
		print('hey')


if __name__ == "__main__":
	app = QtWidgets.QApplication(sys.argv)
	window = MainWidget()
	window.resize(100, 100)
	window.show()
	sys.exit(app.exec_())

userSetup.py

import os
import sys
import maya.cmds as mc


print('hey')
def tweak_launch(*args):

	print('Startup sequence running...')
	os.environ['mickey'] = '--------ebae--------'
	print(os.environ['mickey'])

	root_path = os.getenv('_MMM_ROOT_PATH')
	main_app_path = os.path.join(root_path, 'internal_source')

	if not root_path in sys.path:
		sys.path.append(main_app_path)

	from internal_source import maya_app

	w = maya_app.MainWidget()
	w.show()
	print('window should be up')


mc.evalDeferred("tweak_launch()")

First thing I would do, is in your userSetup.py, import sys, and print out everything on sys.path.
If your path is missing, then yeah something is probably still stomping on some environment variable or another. If the pass is there, then the import error might actually be because something in your maya_app.py is failing and the error is just being converted to an ImportError (this is pretty common, and sadly python2 tends to eat the context making it tricky to track down)

Are you just trying to start maya on a separate process and run some script?

@max_wiklund Basically. When I run my tool from an IDE everything works fine because the paths still exist. So the sys.path.append() in my userSetup.py can see and add the path like normal.

Running my tool from an exe breaks when it gets to the Maya part because the module paths are bundled inside the exe- they don’t actually exist in the typical way anymore. Launching Maya from my exe app doesn’t seem to remember anything about the modules and any paths that are passed into the environment variable during the subprocess.Popen won’t actually resolve due to them not existing.

@bob.w I can add paths to the env var and sys.path will print them out, but those paths are bundled into the exe and I’m not sure if it’s possible to retrieve the module paths once you’re in an external script.

For example here is bundled path to internal_source that was printed from sys.path:

D:\\GitStuff\\mb-armada\\example_files\\exe_bundle\\dist\\bundle\\internal_source

A solution I can think of is to just expose all of the necessary source code during the bundle, but that’s not ideal