Last active
July 22, 2024 10:50
-
-
Save unwave/24f0ee4bfc9cc0af1bf35eda5d54d49f to your computer and use it in GitHub Desktop.
pip install dependencies into .\_deps
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import importlib | |
import importlib.util | |
import os | |
import sys | |
import typing | |
import bpy | |
ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) | |
if bpy.app.version < (2, 91, 0): | |
PYTHON_BINARY = bpy.app.binary_path_python | |
else: | |
PYTHON_BINARY = sys.executable | |
BLENDER_EXECUTABLE = bpy.app.binary_path | |
STATUS_DLL_NOT_FOUND = 3221225781 | |
""" The WindowsApps' rights restriction affects DLLs discovery in PATH """ | |
def pip_fallback(modules_to_install, directory): | |
""" A fallback to install using a Blender's executable. """ | |
from pip._internal import main | |
return main(['install', '--upgrade', *modules_to_install, "--target", directory, '--verbose']) | |
def ensurepip_fallback(): | |
""" A fallback to install using a Blender's executable. """ | |
import ensurepip | |
ensurepip.bootstrap(verbosity=1) | |
def get_python_expr(func, *args, **kwargs): | |
""" Does not work with `shell=True`. """ | |
import inspect | |
import json | |
import textwrap | |
expr = [textwrap.dedent(inspect.getsource(func))] | |
args_json = repr(json.dumps(args)) | |
kwargs_json = repr(json.dumps(kwargs)) | |
if args and kwargs: | |
expr.append('import json') | |
expr.append(f'args = json.loads({args_json})') | |
expr.append(f'kwargs = json.loads({kwargs_json})') | |
expr.append(f'{func.__name__}(*args, **kwargs)') | |
elif args: | |
expr.append('import json') | |
expr.append(f'args = json.loads({args_json})') | |
expr.append(f'{func.__name__}(*args)') | |
elif kwargs: | |
expr.append('import json') | |
expr.append(f'kwargs = json.loads({kwargs_json})') | |
expr.append(f'{func.__name__}(**kwargs)') | |
else: | |
expr.append(f'{func.__name__}()') | |
return '\n'.join(expr) | |
def get_terminal_width(fallback = 80): | |
try: | |
value = int(os.environ['COLUMNS']) | |
except Exception: | |
try: | |
value = os.get_terminal_size(sys.__stdout__.fileno()).columns | |
except Exception: | |
value = fallback | |
return value | |
def print_separator(*values: object, sep: str = ' '): | |
width = get_terminal_width() - 1 | |
text = sep.join((str(value) for value in values)) | |
if text: | |
text = ' ' + text + ' ' | |
text_len = len(text) | |
rest_of_width = width - text_len | |
half_rest_of_width = int(rest_of_width / 2) | |
print('=' * half_rest_of_width, text, '=' * (width - (half_rest_of_width + text_len)), sep='', flush=True) | |
def get_os_environ(): | |
env = os.environ.copy() | |
PATH = env['PATH'] | |
paths = PATH.split(os.pathsep) | |
def add_to_PATH(path): | |
if not os.path.exists(path): | |
return | |
path = os.path.realpath(path) | |
if path in paths: | |
return | |
paths.insert(0, path) | |
blender_dir = os.path.dirname(BLENDER_EXECUTABLE) | |
# vcruntime140.dll | |
blender_crt = os.path.join(blender_dir, 'blender.crt') | |
add_to_PATH(blender_crt) | |
add_to_PATH(blender_dir) | |
env['PATH'] = os.pathsep.join(paths) | |
return env | |
def get_site_packages_directory(root_dir: str = None): | |
""" If `root_dir` is `None` then `ROOT_DIR` is used. """ | |
if root_dir is None: | |
root_dir = ROOT_DIR | |
version = sys.version_info | |
return os.path.join(root_dir, '_deps', f"v{version[0]}{version[1]}") | |
def get_missing_site_packages(packages: typing.List[typing.Tuple[str, str]], directory = get_site_packages_directory()): | |
""" Returns a list of missing packages in `packages`. """ | |
directory = os.path.abspath(directory) | |
if not directory in sys.path and os.path.exists(directory): | |
sys.path.append(directory) | |
return [package for package in packages if not importlib.util.find_spec(package[0])] | |
def ensure_site_packages(packages: typing.List[typing.Tuple[str, str]], directory = get_site_packages_directory(), forced = False): | |
""" | |
`packages`: list of tuples (<import name>, <pip name>) | |
`directory`: a folder for site packages, will be created if does not exist and added to `sys.path` | |
`forced`: ignore installed | |
""" | |
if not packages: | |
return | |
directory = os.path.abspath(directory) | |
os.makedirs(directory, exist_ok = True) | |
if not directory in sys.path: | |
sys.path.append(directory) | |
if forced: | |
modules_to_install = [module[1] for module in packages] | |
else: | |
modules_to_install = [module[1] for module in packages if not importlib.util.find_spec(module[0])] | |
if not modules_to_install: | |
return | |
print_separator('START ensure_site_packages') | |
import subprocess | |
import traceback | |
env = get_os_environ() | |
env['PYTHONPATH'] = directory + os.pathsep + env.get('PYTHONPATH', '') | |
env['PYTHONPATH'].strip(os.pathsep) | |
# all new blender versions have pip but some old one don't | |
# ensurepip might be not present but pip still available (v2.82) | |
if not importlib.util.find_spec('pip'): | |
print_separator('ensurepip') | |
try: | |
# will default to --user if cannot install into normal location | |
subprocess.run([PYTHON_BINARY, '-m', 'ensurepip', '--verbose'], check=True, env=env) | |
except subprocess.CalledProcessError as e: | |
if e.returncode == STATUS_DLL_NOT_FOUND: | |
subprocess.run([BLENDER_EXECUTABLE, '--factory-startup', '-b', '--python-expr', get_python_expr(ensurepip_fallback)], check=True, env=env) | |
else: | |
traceback.print_exc() | |
except Exception: | |
traceback.print_exc() | |
# some packages like opencv can require a newer pip version | |
# updating pip might fail but the existing pip could suffice | |
print_separator('upgrade pip') | |
try: | |
subprocess.run([PYTHON_BINARY, '-m', 'pip', 'install', '--upgrade', 'pip', "--target", directory, '--verbose'], check=True, env=env) | |
except subprocess.CalledProcessError as e: | |
if e.returncode == STATUS_DLL_NOT_FOUND: | |
subprocess.run([BLENDER_EXECUTABLE, '--factory-startup', '-b', '--python-expr', get_python_expr(pip_fallback, ['pip'], directory)], check=True, env=env) | |
else: | |
traceback.print_exc() | |
except Exception: | |
traceback.print_exc() | |
print_separator('install dependencies') | |
try: | |
subprocess.run([PYTHON_BINARY, '-s', '-m', 'pip', 'install', '--upgrade', *modules_to_install, "--target", directory, '--verbose'], check=True, env=env) | |
except subprocess.CalledProcessError as e: | |
if e.returncode != STATUS_DLL_NOT_FOUND: | |
raise e | |
subprocess.run([BLENDER_EXECUTABLE, '--factory-startup', '-b', '--python-expr', get_python_expr(pip_fallback, modules_to_install, directory)], check=True, env=env) | |
importlib.invalidate_caches() | |
missing_packages = [package for package in packages if not importlib.util.find_spec(package[0])] | |
if missing_packages: | |
raise Exception(f'Fail to install dependencies: {missing_packages}') | |
print_separator('END ensure_site_packages') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment