foundation.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. # coding:utf-8
  2. #!/usr/bin/python
  3. #
  4. # Copyright (c) Contributors to the Open 3D Engine Project.
  5. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  6. #
  7. # SPDX-License-Identifier: Apache-2.0 OR MIT
  8. #
  9. #
  10. # -------------------------------------------------------------------------
  11. """! @brief
  12. Module Documentation:
  13. < DCCsi >:: foundation.py
  14. Running this module installs the DCCsi python requirements.txt for other python
  15. interpreters (like Maya)
  16. It installs based on the python version into a location (such as):
  17. '<o3de>/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/Lib/3.x'
  18. This is to ensure that we are not modifying the users DCC tools install directly.
  19. For this script to function on windows you may need Administrator privileges.
  20. ^ You only have to start with Admin rights if you are running foundation.py
  21. to install/update packages and other functions that write to disk.
  22. Suggestion: we could move the package location and be more cross-platform:
  23. from: '<o3de>/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/Lib/3.x'
  24. to (windows): '/Users/<Username>/AppData/Local/Programs/'
  25. Open an admin elevated cmd prompt here:
  26. '<o3de>/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface'
  27. The following would execute this script, the default behavior is to check
  28. the o3de python and install the requirements.txt for that python version,
  29. >python.cmd foundation.py
  30. Suggestion: document additional usage (how to install for Maya 2022 py3.7, etc.)
  31. """
  32. # -------------------------------------------------------------------------
  33. # standard imports
  34. import subprocess
  35. import sys
  36. import os
  37. import site
  38. import timeit
  39. import inspect
  40. import traceback
  41. from pathlib import Path
  42. import logging as _logging
  43. # -------------------------------------------------------------------------
  44. # -------------------------------------------------------------------------
  45. # os.environ['PYTHONINSPECT'] = 'True'
  46. _START = timeit.default_timer() # start tracking
  47. # global scope
  48. _MODULENAME = 'foundation'
  49. _LOGGER = _logging.getLogger(_MODULENAME)
  50. _LOGGER.debug('Initializing: {}.'.format({_MODULENAME}))
  51. # -------------------------------------------------------------------------
  52. # -------------------------------------------------------------------------
  53. # Local access
  54. _MODULE_PATH = Path(__file__) # this script
  55. _PATH_DCCSIG = Path(_MODULE_PATH.parent) # dccsi
  56. os.environ['PATH_DCCSIG'] = _PATH_DCCSIG.as_posix()
  57. site.addsitedir(_PATH_DCCSIG.as_posix()) # python path
  58. os.chdir(_PATH_DCCSIG.as_posix()) # cwd
  59. # local imports from dccsi
  60. import azpy.config_utils
  61. # these are just defaults and are meant to be replaced by info for the target python.exe
  62. _SYS_VER_MAJOR = sys.version_info.major
  63. _SYS_VER_MINOR = sys.version_info.minor
  64. # the default will be based on the python executable running this module
  65. # this value should be replaced with the sys,version of the target python
  66. # for example mayapy, or blenders python, etc.
  67. _PATH_DCCSI_PYTHON_LIB = Path(f'{_PATH_DCCSIG}\\3rdParty\\Python\\Lib\\'
  68. f'{_SYS_VER_MAJOR}.x\\'
  69. f'{_SYS_VER_MAJOR}.{_SYS_VER_MINOR}.x\\site-packages').resolve()
  70. _PATH_DCCSI_PYTHON_LIB.touch(exist_ok=True) # make sure it's there
  71. # this is the shared default requirements.txt file to install for python 3.6.x+
  72. _DCCSI_PYTHON_REQUIREMENTS = Path(_PATH_DCCSIG, 'requirements.txt')
  73. # this will default to the python interpreter running this script (probably o3de)
  74. # this should be replaced by the target interpreter python exe, like mayapy.exe
  75. _PYTHON_EXE = Path(sys.executable)
  76. # -------------------------------------------------------------------------
  77. # -------------------------------------------------------------------------
  78. def check_pip(python_exe=_PYTHON_EXE):
  79. """Check if pip is installed and log what version"""
  80. python_exe = Path(python_exe)
  81. if python_exe.exists():
  82. result = subprocess.call([python_exe.as_posix(), "-m", "pip", "--version"])
  83. _LOGGER.info(f'foundation.check_pip(), result: {result}')
  84. return result
  85. else:
  86. _LOGGER.error(f'python_exe does not exist: {python_exe}')
  87. return 1
  88. # -------------------------------------------------------------------------
  89. # -------------------------------------------------------------------------
  90. def ensurepip(python_exe=_PYTHON_EXE, upgrade=False):
  91. """Will use ensurepip method to ensure pip is installed"""
  92. # note: this doesn't work with python 3.10 which is the version o3de is on
  93. # luckily o3de comes with working pip
  94. # if this errors out with an exception and "ValueError: bad marshal data (unknown type code)"
  95. # you should try to install pip using foundation.install_pip() method
  96. result = 0
  97. python_exe = Path(python_exe)
  98. if python_exe.exists():
  99. if upgrade:
  100. result = subprocess.call([python_exe.as_posix(), "-m", "ensurepip", "--upgrade"])
  101. _LOGGER.info(f'foundation.ensurepip(python_exe, upgrade=True), result: {result}')
  102. else:
  103. result = subprocess.call([python_exe.as_posix(), "-m", "ensurepip"])
  104. _LOGGER.info(f'foundation.ensurepip(python_exe), result: {result}')
  105. else:
  106. _LOGGER.error(f'python_exe does not exist: {python_exe}')
  107. return 0
  108. return result
  109. # -------------------------------------------------------------------------
  110. # -------------------------------------------------------------------------
  111. _GET_PIP_PY37_URL = "https://bootstrap.pypa.io/get-pip.py"
  112. _GET_PIP_PY27_URL = "https://bootstrap.pypa.io/pip/2.7/get-pip.py"
  113. # version to download (DL)
  114. if sys.version_info.major >= 3 and sys.version_info.minor >= 7:
  115. DL_URL = _GET_PIP_PY37_URL
  116. elif sys.version_info.major < 3:
  117. DL_URL = _GET_PIP_PY27_URL
  118. # temp dir to store in:
  119. _PIP_DL_LOC = Path(_PATH_DCCSIG) / '__tmp__'
  120. if not _PIP_DL_LOC.exists():
  121. try:
  122. _PIP_DL_LOC.mkdir(parents=True)
  123. except Exception as e:
  124. _LOGGER.error(f'error: {e}, could not .mkdir(): {PIP_DL_LOC.as_posix()}')
  125. # default file location to store it:
  126. _PIP_DL_LOC = _PIP_DL_LOC / 'get-pip.py'
  127. try:
  128. _PIP_DL_LOC.touch(mode=0o666, exist_ok=True)
  129. except Exception as e:
  130. _LOGGER.error(f'error: {e}, could not .touch(): {PIP_DL_LOC.as_posix()}')
  131. def download_getpip(url=DL_URL, file_store=_PIP_DL_LOC):
  132. """Attempts to download the get - pip.py script"""
  133. import requests
  134. # ensure what is passed in is a Path object
  135. file_store = Path(file_store)
  136. file_store = Path.joinpath(file_store)
  137. try:
  138. file_store.exists()
  139. except FileExistsError as e:
  140. try:
  141. file_store.touch()
  142. except FileExistsError as e:
  143. _LOGGER.error(f'Could not make file: {file_store}')
  144. try:
  145. _get_pip = requests.get(url)
  146. except Exception as e:
  147. _LOGGER.error(f'could not request: {url}')
  148. try:
  149. file = open(file_store.as_posix(), 'wb').write(_get_pip.content)
  150. return file
  151. except IOError as e:
  152. _LOGGER.error(f'could not write: {file_store.as_posix()}')
  153. return None
  154. # -------------------------------------------------------------------------
  155. # -------------------------------------------------------------------------
  156. def install_pip(python_exe=_PYTHON_EXE, download=True, upgrade=True, getpip=_PIP_DL_LOC):
  157. """Installs pip via get - pip.py"""
  158. result = 0
  159. if download:
  160. getpip = download_getpip()
  161. if not getpip:
  162. return result
  163. python_exe = Path(python_exe)
  164. if python_exe.exists():
  165. python_exe = python_exe.as_posix()
  166. result = subprocess.call([python_exe, "-m", getpip])
  167. _LOGGER.info(f'result: {result}')
  168. else:
  169. _LOGGER.error(f'python_exe does not exist: {python_exe}')
  170. return 0
  171. if upgrade:
  172. python_exe = python_exe.as_posix()
  173. result = subprocess.call([python_exe, "-m", "pip", "install", "--upgrade", "pip"])
  174. _LOGGER.info(f'result: {result}')
  175. return result
  176. return result
  177. # -------------------------------------------------------------------------
  178. # -------------------------------------------------------------------------
  179. # version of requirements.txt to install
  180. if sys.version_info.major >= 3 and sys.version_info.minor >= 7:
  181. _REQUIREMENTS = _DCCSI_PYTHON_REQUIREMENTS
  182. elif sys.version_info.major == 2 and sys.version_info.minor >= 7:
  183. _LOGGER.warning('Python 2.7 is end of life, we recommend using tools that operate py3.7 or higher')
  184. _REQUIREMENTS = Path(_PATH_DCCSIG,
  185. 'Tools',
  186. 'Resources',
  187. 'py27',
  188. 'requirements.txt').as_posix()
  189. else:
  190. _REQUIREMENTS = None
  191. _LOGGER.error(f'Unsupported version: {sys.version_info}')
  192. def install_requirements(python_exe=_PYTHON_EXE,
  193. requirements=_REQUIREMENTS,
  194. target_loc=_PATH_DCCSI_PYTHON_LIB.as_posix()):
  195. """Installs the DCCsi requirements.txt"""
  196. python_exe = Path(python_exe)
  197. requirements = Path(requirements)
  198. target_loc = Path(target_loc)
  199. if python_exe.exists():
  200. # install required packages
  201. inst_cmd = [python_exe.as_posix(), "-m", "pip", "install",
  202. "-r", requirements.as_posix(), "-t", target_loc.as_posix()]
  203. result = subprocess.call(inst_cmd)
  204. return result
  205. else:
  206. _LOGGER.error(f'python_exe does not exist: {python_exe}')
  207. return 0
  208. # -------------------------------------------------------------------------
  209. # -------------------------------------------------------------------------
  210. def install_pkg(python_exe=_PYTHON_EXE,
  211. pkg_name='pathlib',
  212. target_loc=_PATH_DCCSI_PYTHON_LIB.as_posix()):
  213. """Installs a pkg for DCCsi"""
  214. python_exe = Path(python_exe)
  215. pkg_name = Path(pkg_name)
  216. target_loc = Path(target_loc)
  217. if python_exe.exists():
  218. inst_cmd = [python_exe.as_posix(), "-m", "pip", "install", pkg_name.as_posix(),
  219. "-t", target_loc.as_posix()]
  220. result = subprocess.call(inst_cmd)
  221. return result
  222. else:
  223. _LOGGER.error(f'python_exe does not exist: {python_exe}')
  224. return 0
  225. # -------------------------------------------------------------------------
  226. # -------------------------------------------------------------------------
  227. def run_command() -> 'subprocess.CompletedProcess[str]':
  228. """Run some subprocess that captures output as ``str``"""
  229. return subprocess.CompletedProcess(args=[], returncode=0, stdout='')
  230. # -------------------------------------------------------------------------
  231. # -------------------------------------------------------------------------
  232. def set_version(ver_major=sys.version_info.major,
  233. ver_minor=sys.version_info.minor):
  234. global _SYS_VER_MAJOR
  235. global _SYS_VER_MINOR
  236. global _PATH_DCCSI_PYTHON_LIB
  237. _SYS_VER_MAJOR = ver_major
  238. _SYS_VER_MINOR = ver_minor
  239. _PATH_DCCSI_PYTHON_LIB = Path(STR_PATH_DCCSI_PYTHON_LIB.format(_PATH_DCCSIG,
  240. _SYS_VER_MAJOR,
  241. _SYS_VER_MINOR))
  242. return _PATH_DCCSI_PYTHON_LIB
  243. # -------------------------------------------------------------------------
  244. # -------------------------------------------------------------------------
  245. def get_version(_PYTHON_EXE):
  246. _PYTHON_EXE = Path(_PYTHON_EXE)
  247. if _PYTHON_EXE.exists():
  248. # this will switch to run the specified dcc tools python exe and determine version
  249. _COMMAND = [_PYTHON_EXE.as_posix(), "--version"]
  250. _process = subprocess.Popen(_COMMAND, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  251. _out, _err = _process.communicate()
  252. _out = _out.decode("utf-8") # decodes byte string to string
  253. _out = _out.replace("\r\n", "") # clean
  254. _LOGGER.info(f'Python Version is: {_out}')
  255. _ver = _out.split(" ")[-1] # split by space, take version
  256. _ver = _ver.split('.') # split by . to list
  257. return _ver
  258. else:
  259. _LOGGER.error(f'Python exe does not exist: {_PYTHON_EXE.as_posix()}')
  260. return None
  261. # -------------------------------------------------------------------------
  262. ###########################################################################
  263. # Main Code Block, runs this script as main (testing)
  264. # -------------------------------------------------------------------------
  265. if __name__ == '__main__':
  266. """Run this file as main(external command line)"""
  267. STR_CROSSBAR = f"{'-' * 74}"
  268. _DCCSI_GDEBUG = False
  269. _DCCSI_DEV_MODE = False
  270. # default loglevel to info unless set
  271. _DCCSI_LOGLEVEL = _logging.INFO
  272. if _DCCSI_GDEBUG:
  273. # override loglevel if running debug
  274. _DCCSI_LOGLEVEL = _logging.DEBUG
  275. FRMT_LOG_LONG = "[%(name)s][%(levelname)s] >> %(message)s (%(asctime)s; %(filename)s:%(lineno)d)"
  276. # configure basic logger
  277. # note: not using a common logger to reduce cyclical imports
  278. _logging.basicConfig(level=_DCCSI_LOGLEVEL,
  279. format=FRMT_LOG_LONG,
  280. datefmt='%m-%d %H:%M')
  281. _LOGGER = _logging.getLogger(_MODULENAME)
  282. _LOGGER.info(STR_CROSSBAR)
  283. _LOGGER.debug('Initializing: {}.'.format({_MODULENAME}))
  284. _LOGGER.debug('_DCCSI_GDEBUG: {}'.format(_DCCSI_GDEBUG))
  285. _LOGGER.debug('_DCCSI_DEV_MODE: {}'.format(_DCCSI_DEV_MODE))
  286. _LOGGER.debug('_DCCSI_LOGLEVEL: {}'.format(_DCCSI_LOGLEVEL))
  287. import argparse
  288. parser = argparse.ArgumentParser(
  289. description='O3DE DCCsi Setup (aka Foundation). Will install DCCsi python package dependencies, for various DCC tools.',
  290. epilog="It is suggested to use '-py' or '--python_exe' to pass in the python exe for the target dcc tool.")
  291. parser.add_argument('-gd', '--global-debug',
  292. type=bool,
  293. required=False,
  294. help='Enables global debug flag.')
  295. parser.add_argument('-dm', '--developer-mode',
  296. type=bool,
  297. required=False,
  298. help='Enables dev mode for early auto attaching debugger.')
  299. parser.add_argument('-sd', '--set-debugger',
  300. type=str,
  301. required=False,
  302. default='WING',
  303. help='(NOT IMPLEMENTED) Default debugger: WING, others: PYCHARM and VSCODE.')
  304. parser.add_argument('-py', '--python_exe',
  305. type=str,
  306. required=False,
  307. help='The python interpreter you want to run in the subprocess')
  308. parser.add_argument('-cp', '--check_pip',
  309. required=False,
  310. default=True,
  311. help='Checks for pip')
  312. parser.add_argument('-ep', '--ensurepip',
  313. required=False,
  314. help='Uses ensurepip, to make sure pip is installed')
  315. parser.add_argument('-ip', '--install_pip',
  316. required=False,
  317. help='Attempts install pip via download of get-pip.py')
  318. parser.add_argument('-ir', '--install_requirements',
  319. required=False,
  320. default=True,
  321. help='Exits python')
  322. parser.add_argument('-ex', '--exit',
  323. type=bool,
  324. required=False,
  325. help='Exits python. Do not exit if you want to be in interactive interpreter after config')
  326. args = parser.parse_args()
  327. from azpy.shared.utils.arg_bool import arg_bool
  328. # easy overrides
  329. if arg_bool(args.global_debug, desc="args.global_debug"):
  330. from azpy.constants import ENVAR_DCCSI_GDEBUG
  331. _DCCSI_GDEBUG = True
  332. _LOGGER.setLevel(_logging.DEBUG)
  333. _LOGGER.info(f'Global debug is set, {ENVAR_DCCSI_GDEBUG}={_DCCSI_GDEBUG}')
  334. if arg_bool(args.developer_mode, desc="args.developer_mode"):
  335. _DCCSI_DEV_MODE = True
  336. azpy.config_utils.attach_debugger() # attempts to start debugger
  337. if not args.python_exe:
  338. _LOGGER.warning("It is suggested to use arg '-py' or '--python_exe' to pass in the python exe for the target dcc tool.")
  339. if args.python_exe:
  340. _PYTHON_EXE = Path(args.python_exe)
  341. _LOGGER.info(f'Target py exe is: {_PYTHON_EXE}')
  342. if _PYTHON_EXE.exists():
  343. _py_version = get_version(_PYTHON_EXE)
  344. # then we can change the version dependant target folder for pkg install
  345. _PATH_DCCSI_PYTHON_LIB = set_version(_py_version[0], _py_version[1])
  346. if _PATH_DCCSI_PYTHON_LIB.exists():
  347. _LOGGER.info(f'Requirements, install target: {_PATH_DCCSI_PYTHON_LIB}')
  348. else:
  349. _PATH_DCCSI_PYTHON_LIB.touch()
  350. _LOGGER.info(f'.touch(): {_PATH_DCCSI_PYTHON_LIB}')
  351. else:
  352. _LOGGER.error(f'This py exe does not exist:{_PYTHON_EXE}')
  353. _LOGGER.info(f'Make sure to wrap your path to the exe in quotes, like:')
  354. _LOGGER.info(f'.\python foundation.py -py="C:\Program Files\Autodesk\Maya2023\bin\mayapy.exe"')
  355. sys.exit()
  356. # this will verify pip is installed for the target python interpreter/env
  357. if arg_bool(args.check_pip, desc='args.check_pip'):
  358. _LOGGER.info(f'calling foundation.check_pip()')
  359. result = check_pip(_PYTHON_EXE)
  360. if result != 0:
  361. _LOGGER.warning(f'check_pip(), Invalid result: { result }')
  362. if arg_bool(args.ensurepip, desc='args.ensurepip'):
  363. _LOGGER.info(f'calling foundation.ensurepip()')
  364. ensurepip(_PYTHON_EXE)
  365. if arg_bool(args.install_pip, desc='args.install_pip'):
  366. _LOGGER.info(f'calling foundation.install_pip()')
  367. install_pip(_PYTHON_EXE)
  368. # installing the requirements.txt is enabled by default
  369. if arg_bool(args.install_requirements, desc='args.check_pip'):
  370. _LOGGER.info(f'calling foundation.install_requirements( {_PYTHON_EXE}, target_loc = {_PATH_DCCSI_PYTHON_LIB.as_posix()} )')
  371. install_requirements(_PYTHON_EXE, target_loc = _PATH_DCCSI_PYTHON_LIB.as_posix())
  372. # -- DONE ----
  373. _LOGGER.info(STR_CROSSBAR)
  374. _LOGGER.info('O3DE DCCsi {0}.py took: {1} sec'.format(_MODULENAME, timeit.default_timer() - _START))
  375. if args.exit:
  376. import sys
  377. # return
  378. sys.exit()
  379. else:
  380. # custom prompt
  381. sys.ps1 = "[{}]>>".format(_MODULENAME)
  382. # --- END -----------------------------------------------------------------