diff --git a/setup.py b/setup.py index 2f9b968..501a717 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,13 @@ if platform.system() == 'Windows': import winreg +def is_configure_root() -> bool: + """ + Returns True if matlab folders should be configured at build time + """ + return os.environ.get('MATLAB_ENGINE_CONFIGURE_ROOT', 'true').lower() in ('true', '1') + + class _MatlabFinder(build_py): """ Private class that finds MATLAB on user's computer prior to package installation. @@ -379,34 +386,36 @@ def run(self): self.set_platform_and_arch() self.set_python_version() - if self.platform == 'Windows': - matlab_root = self.get_matlab_root_from_windows_reg() - else: - if self.unix_default_install_exists(): - matlab_root = self.DEFAULT_INSTALLS[self.platform] + if is_configure_root(): + if self.platform == 'Windows': + matlab_root = self.get_matlab_root_from_windows_reg() else: - path_dirs = self._create_path_list() - matlab_root = self.search_path_for_directory_unix(self.arch, path_dirs) - err_msg = self._err_msg_if_bad_matlab_root(matlab_root) - if err_msg: - if self.platform == 'Darwin': - if self.found_matlab_with_wrong_arch_in_default_install: - raise RuntimeError( - self.wrong_arch_in_default_install.format( - path1=self.found_matlab_with_wrong_arch_in_default_install, - matlab_arch=self._get_alternate_arch(), - python_arch=self.arch, - next_steps=self.next_steps)) - if self.found_matlab_with_wrong_arch_in_path: - raise RuntimeError( - self.wrong_arch_in_path.format( - path1=self.found_matlab_with_wrong_arch_in_path, - matlab_arch=self._get_alternate_arch(), - python_arch=self.arch, - next_steps=self.next_steps)) - raise RuntimeError(err_msg) + if self.unix_default_install_exists(): + matlab_root = self.DEFAULT_INSTALLS[self.platform] + else: + path_dirs = self._create_path_list() + matlab_root = self.search_path_for_directory_unix(self.arch, path_dirs) + err_msg = self._err_msg_if_bad_matlab_root(matlab_root) + if err_msg: + if self.platform == 'Darwin': + if self.found_matlab_with_wrong_arch_in_default_install: + raise RuntimeError( + self.wrong_arch_in_default_install.format( + path1=self.found_matlab_with_wrong_arch_in_default_install, + matlab_arch=self._get_alternate_arch(), + python_arch=self.arch, + next_steps=self.next_steps)) + if self.found_matlab_with_wrong_arch_in_path: + raise RuntimeError( + self.wrong_arch_in_path.format( + path1=self.found_matlab_with_wrong_arch_in_path, + matlab_arch=self._get_alternate_arch(), + python_arch=self.arch, + next_steps=self.next_steps)) + raise RuntimeError(err_msg) + + self.write_text_file(matlab_root) - self.write_text_file(matlab_root) build_py.run(self) @@ -427,7 +436,7 @@ def run(self): package_dir={'': 'src'}, packages=find_packages(where="src"), cmdclass={'build_py': _MatlabFinder}, - package_data={'': ['_arch.txt']}, + package_data={'': ['_arch.txt']} if is_configure_root() else {}, zip_safe=False, project_urls={ 'Documentation': 'https://www.mathworks.com/help/matlab/matlab-engine-for-python.html', diff --git a/src/matlab/__init__.py b/src/matlab/__init__.py index 6b3133b..83358cd 100644 --- a/src/matlab/__init__.py +++ b/src/matlab/__init__.py @@ -4,6 +4,7 @@ import platform import sys import pkgutil +from . import _utils __path__ = pkgutil.extend_path(__path__, __name__) package_folder = os.path.dirname(os.path.realpath(__file__)) @@ -35,13 +36,7 @@ def add_dirs_to_path(bin_dir, engine_dir, extern_dir): sys.path.insert(0, engine_dir) sys.path.insert(0, extern_dir) -arch_file = os.path.join(package_folder, 'engine', '_arch.txt') -if not os.path.isfile(arch_file): - raise RuntimeError("The MATLAB Engine for Python install is corrupted. Please try to re-install.") - -with open(arch_file, 'r') as root: - [arch, bin_folder, engine_folder, extern_bin] = [line.strip() for line in root.readlines()] - +arch, bin_folder, engine_folder, extern_bin = _utils.get_path_info() add_dirs_to_path(bin_folder, engine_folder, extern_bin) diff --git a/src/matlab/_utils.py b/src/matlab/_utils.py new file mode 100644 index 0000000..c16786a --- /dev/null +++ b/src/matlab/_utils.py @@ -0,0 +1,58 @@ +import functools +import os +import platform +import shutil +from typing import Optional, NamedTuple + + +def _get_platform_arch() -> str: + system_name = platform.system() + + if system_name == 'Windows': + return 'win64' + if system_name == 'Linux': + return 'glnxa64' + if system_name == 'Darwin': + if platform.mac_ver()[-1] == 'arm64': + return 'maca64' + return 'maci64' + + raise RuntimeError(f"{system_name} is not a supported platform.") + + +def _get_matlab_root() -> Optional[str]: + """Probe matlab root directory""" + matlab_command = shutil.which('matlab') + if not matlab_command: + return None + matlab_bin_dir = os.path.dirname(matlab_command) + matlab_root = os.path.normpath(os.path.join(matlab_bin_dir, os.pardir)) + return matlab_root + + +class MatlabPathInfo(NamedTuple): + arch: str + bin_folder: str + engine_folder: str + extern_bin: str + + +@functools.cache +def get_path_info() -> MatlabPathInfo: + package_folder = os.path.dirname(os.path.realpath(__file__)) + arch_file = os.path.join(package_folder, 'engine', '_arch.txt') + if os.path.isfile(arch_file): + with open(arch_file, 'r') as root: + [arch, bin_folder, engine_folder, extern_bin] = [line.strip() for line in root.readlines() if line.strip()] + return MatlabPathInfo(arch, bin_folder, engine_folder, extern_bin) + + matlab_root = _get_matlab_root() + if matlab_root: + arch = _get_platform_arch() + bin_folder = os.path.join(matlab_root, 'bin', arch) + engine_folder = os.path.join(matlab_root, 'extern', 'engines', 'python', 'dist', 'matlab', 'engine', arch) + extern_bin = os.path.join(matlab_root, 'extern', 'bin', arch) + if os.path.isdir(bin_folder) and os.path.isdir(engine_folder) and os.path.isdir(extern_bin): + return MatlabPathInfo(arch, bin_folder, engine_folder, extern_bin) + + raise RuntimeError("The MATLAB Engine for Python install is corrupted or matlab is not available. Please try to re-install.") diff --git a/src/matlab/engine/__init__.py b/src/matlab/engine/__init__.py index 72ef0e5..3c22d85 100644 --- a/src/matlab/engine/__init__.py +++ b/src/matlab/engine/__init__.py @@ -27,6 +27,8 @@ import threading import warnings +from .. import _utils + # UPDATE_IF_PYTHON_VERSION_ADDED_OR_REMOVED : search for this string in codebase # when support for a Python version must be added or removed @@ -50,8 +52,6 @@ 'is %s' % _version) -_module_folder = os.path.dirname(os.path.realpath(__file__)) -_arch_filename = os.path.join(_module_folder, "_arch.txt") success = False firstExceptionMessage = '' secondExceptionMessage = '' @@ -65,21 +65,14 @@ if firstExceptionMessage: try: - _arch_file = open(_arch_filename,'r') - _lines = _arch_file.readlines() - [_arch, _bin_dir,_engine_dir, _extern_bin_dir] = [x.rstrip() for x in _lines if x.rstrip() != ""] - _arch_file.close() - sys.path.insert(0,_engine_dir) - sys.path.insert(0,_extern_bin_dir) - _envs = {'win32': 'PATH', 'win64': 'PATH'} - if _arch in _envs: - if _envs[_arch] in os.environ: - _env = os.environ[_envs[_arch]] - os.environ[_envs[_arch]] = _bin_dir + os.pathsep + os.environ[_envs[_arch]] + _path_info = _utils.get_path_info() + if _path_info.arch in _envs: + if _envs[_path_info.arch] in os.environ: + os.environ[_envs[_path_info.arch]] = _path_info.bin_folder + os.pathsep + os.environ[_envs[_path_info.arch]] else: - os.environ[_envs[_arch]] = _bin_dir - os.add_dll_directory(_bin_dir) + os.environ[_envs[_path_info.arch]] = _path_info.bin_folder + os.add_dll_directory(_path_info.bin_folder) if _PYTHONVERSION != '3_9' or _PYTHONVERSION != '3_10': pythonengine = importlib.import_module("matlabengineforpython_abi3") else: