__init__.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  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. <DCCsi>/__init__.py
  13. Allow DccScriptingInterface Gem to be the parent python pkg
  14. """
  15. DCCSI_DOCS_URL = "https://github.com/o3de/o3de/blob/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/readme.md"
  16. DCCSI_INFO = {
  17. 'name': 'O3DE_DCCSI_GEM',
  18. "description": "DccScriptingInterface",
  19. 'status': 'prototype',
  20. 'version': (0, 0, 1),
  21. 'platforms': ({
  22. 'include': ('win'), # windows
  23. 'exclude': ('darwin', # mac
  24. 'linux') # linux, linux_x64
  25. }),
  26. "doc_url": DCCSI_DOCS_URL
  27. }
  28. import timeit
  29. __MODULE_START = timeit.default_timer() # start tracking
  30. # -------------------------------------------------------------------------
  31. # standard imports
  32. import os
  33. import sys
  34. import site
  35. import json
  36. import warnings
  37. from pathlib import Path
  38. import logging as _logging
  39. # -------------------------------------------------------------------------
  40. # global scope
  41. _PACKAGENAME = 'DCCsi'
  42. STR_CROSSBAR = f"{('-' * 74)}"
  43. FRMT_LOG_LONG = "[%(name)s][%(levelname)s] >> %(message)s (%(asctime)s; %(filename)s:%(lineno)d)"
  44. FRMT_LOG_SHRT = "[%(asctime)s][%(name)s][%(levelname)s] >> %(message)s"
  45. # allow package to capture warnings
  46. _logging.captureWarnings(capture=True)
  47. # set this manually if you want to raise exceptions/warnings
  48. DCCSI_STRICT = False
  49. # set manually to allow this module to test PySide2 imports
  50. DCCSI_TEST_PYSIDE = False
  51. # default loglevel to info unless set
  52. ENVAR_DCCSI_LOGLEVEL = 'DCCSI_LOGLEVEL'
  53. DCCSI_LOGLEVEL = int(os.getenv(ENVAR_DCCSI_LOGLEVEL, _logging.INFO))
  54. # configure basic logger since this is the top-level module
  55. # note: not using a common logger to reduce cyclical imports
  56. _logging.basicConfig(level=DCCSI_LOGLEVEL,
  57. format=FRMT_LOG_LONG,
  58. datefmt='%m-%d %H:%M')
  59. _LOGGER = _logging.getLogger(_PACKAGENAME)
  60. _LOGGER.debug(STR_CROSSBAR)
  61. _LOGGER.debug(f'Initializing: {_PACKAGENAME}')
  62. __all__ = ['globals', # global state module
  63. 'config', # dccsi core config.py
  64. 'constants', # global dccsi constants
  65. 'foundation', # set up dependency pkgs for DCC tools
  66. 'return_sys_version', # util
  67. 'azpy', # shared pure python api
  68. 'Editor', # O3DE editor scripts
  69. 'Tools' # DCC and IDE tool integrations
  70. ]
  71. # be careful when pulling from this __init__.py module
  72. # avoid cyclical imports, this module should not import from sub-modules
  73. # we need to set up basic access to the DCCsi
  74. _MODULE_PATH = Path(__file__)
  75. _LOGGER.debug(f'_MODULE_PATH: {_MODULE_PATH}')
  76. # -------------------------------------------------------------------------
  77. # -------------------------------------------------------------------------
  78. def pathify_list(path_list):
  79. """Make list path objects"""
  80. new_path_list = list()
  81. for i, p in enumerate(path_list):
  82. p = Path(p)
  83. path_list[i] = p.resolve()
  84. if p not in new_path_list:
  85. new_path_list.append(p)
  86. return new_path_list
  87. def add_site_dir(site_dir):
  88. """Add a site package and moves to front of sys.path"""
  89. site_dir = Path(site_dir).resolve()
  90. _prev_sys_paths = pathify_list(list(sys.path)) # copy
  91. # if passing a Path object cast to string value
  92. site.addsitedir(str(site_dir))
  93. # new entries have precedence, front of sys.path
  94. for p in pathify_list(list(sys.path)):
  95. if p not in _prev_sys_paths:
  96. sys.path.remove(str(p))
  97. sys.path.insert(0, str(p))
  98. return site_dir
  99. def get_key_value(in_dict: dict, in_key: str):
  100. for key, value in in_dict.items():
  101. if in_key == value:
  102. return key
  103. return None
  104. # -------------------------------------------------------------------------
  105. # -------------------------------------------------------------------------
  106. # add gems parent, dccsi lives under:
  107. # < o3de >\Gems\AtomLyIntegration\TechnicalArt
  108. PATH_O3DE_TECHART_GEMS = _MODULE_PATH.parents[1].resolve()
  109. PATH_O3DE_TECHART_GEMS = add_site_dir(PATH_O3DE_TECHART_GEMS)
  110. # there may be other TechArt gems in the future
  111. # or the dccsi maybe split up
  112. ENVAR_PATH_DCCSIG = 'PATH_DCCSIG'
  113. # < o3de >\Gems\AtomLyIntegration\TechnicalArt\< dccsi >
  114. # the default location
  115. PATH_DCCSIG = _MODULE_PATH.parents[0].resolve()
  116. # this allows the dccsi gem location to be overridden in the external env
  117. PATH_DCCSIG = Path(os.getenv(ENVAR_PATH_DCCSIG, str(PATH_DCCSIG)))
  118. # validate (if envar is bad)
  119. try:
  120. PATH_DCCSIG.resolve(strict=True)
  121. add_site_dir(PATH_DCCSIG)
  122. _LOGGER.debug(f'{ENVAR_PATH_DCCSIG} is: {PATH_DCCSIG.as_posix()}')
  123. except NotADirectoryError as e:
  124. # this should not happen, unless envar override could not resolve
  125. _LOGGER.warning(f'{ENVAR_PATH_DCCSIG} failed: {PATH_DCCSIG.as_posix()}')
  126. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  127. PATH_DCCSIG = None
  128. if DCCSI_STRICT:
  129. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  130. raise e
  131. # < dccsi >\3rdParty bootstrapping area for installed site-packages
  132. # dcc tools on a different version of python will import from here.
  133. # path string constructor
  134. ENVAR_PATH_DCCSI_PYTHON_LIB = 'PATH_DCCSI_PYTHON_LIB'
  135. # a str path constructor for the dccsi 3rdPary site-dir
  136. STR_DCCSI_PYTHON_LIB = (f'{PATH_DCCSIG.as_posix()}' +
  137. f'\\3rdParty\\Python\\Lib' +
  138. f'\\{sys.version_info[0]}.x' +
  139. f'\\{sys.version_info[0]}.{sys.version_info[1]}.x' +
  140. f'\\site-packages')
  141. # build path
  142. PATH_DCCSI_PYTHON_LIB = Path(STR_DCCSI_PYTHON_LIB)
  143. # allow location to be set/overridden from env
  144. PATH_DCCSI_PYTHON_LIB = Path(os.getenv(ENVAR_PATH_DCCSI_PYTHON_LIB,
  145. str(PATH_DCCSI_PYTHON_LIB)))
  146. # validate, because we checked envar
  147. try:
  148. PATH_DCCSI_PYTHON_LIB.resolve(strict=True)
  149. add_site_dir(PATH_DCCSI_PYTHON_LIB)
  150. _LOGGER.debug(f'{ENVAR_PATH_DCCSI_PYTHON_LIB} is: {PATH_DCCSI_PYTHON_LIB.as_posix()}')
  151. except NotADirectoryError as e:
  152. _LOGGER.warning(f'{ENVAR_PATH_DCCSI_PYTHON_LIB} does not exist:' +
  153. f'{PATH_DCCSI_PYTHON_LIB.as_Posix()}')
  154. _LOGGER.warning(f'Pkg dependencies may not be available for import')
  155. _LOGGER.warning(f'Try using foundation.py to install pkg dependencies for the target python runtime')
  156. _LOGGER.info(f'>.\python foundation.py -py="c:\path\to\som\python.exe"')
  157. PATH_DCCSI_PYTHON_LIB = None
  158. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  159. PATH_DCCSIG = None
  160. if DCCSI_STRICT:
  161. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  162. raise e
  163. _LOGGER.debug(STR_CROSSBAR)
  164. # -------------------------------------------------------------------------
  165. # -------------------------------------------------------------------------
  166. # a developer file for setting envars and overrides
  167. PATH_ENV_DEV = Path(PATH_DCCSIG, 'Tools', 'Dev', 'Windows', 'Env_Dev.bat')
  168. # a local settings file to overrides envars in config.py
  169. DCCSI_SETTINGS_LOCAL_FILENAME = 'settings.local.json'
  170. PATH_DCCSI_SETTINGS_LOCAL = Path.joinpath(PATH_DCCSIG,
  171. DCCSI_SETTINGS_LOCAL_FILENAME).resolve()
  172. if PATH_DCCSI_SETTINGS_LOCAL.exists():
  173. _LOGGER.info(f'local settings exists: {PATH_DCCSI_SETTINGS_LOCAL.as_posix()}')
  174. else:
  175. _LOGGER.info(f'does not exist: {PATH_DCCSI_SETTINGS_LOCAL.as_posix()}')
  176. # the o3de manifest data
  177. TAG_USER_O3DE = '.o3de'
  178. SLUG_MANIFEST_FILENAME = 'o3de_manifest.json'
  179. USER_HOME = Path.home()
  180. _user_home_parts = os.path.split(USER_HOME)
  181. if str(_user_home_parts[1].lower()) == 'documents':
  182. USER_HOME = _user_home_parts[0]
  183. _LOGGER.debug(f'user home CORRECTED: {USER_HOME}')
  184. O3DE_MANIFEST_PATH = Path(USER_HOME,
  185. TAG_USER_O3DE,
  186. SLUG_MANIFEST_FILENAME).resolve()
  187. O3DE_USER_HOME = Path(USER_HOME, TAG_USER_O3DE)
  188. # {user_home}\.o3de\registry\bootstrap.setreg
  189. SLUG_BOOTSTRAP_FILENAME = 'bootstrap.setreg'
  190. O3DE_BOOTSTRAP_PATH = Path(O3DE_USER_HOME, 'Registry', SLUG_BOOTSTRAP_FILENAME)
  191. # -------------------------------------------------------------------------
  192. # -------------------------------------------------------------------------
  193. # top level constants, need for basic wing debugging
  194. # str slug for the default wing type
  195. # in the future, add support for wing personal and maybe wing 101 versions
  196. SLUG_DCCSI_WING_TYPE = 'Wing Pro'
  197. # the default supported version of wing pro is 8
  198. SLUG_DCCSI_WING_VERSION_MAJOR = int(8)
  199. # resolves the windows program install directory
  200. ENVAR_PROGRAMFILES_X86 = 'PROGRAMFILES(X86)'
  201. PATH_PROGRAMFILES_X86 = os.environ[ENVAR_PROGRAMFILES_X86]
  202. # resolves the windows program install directory
  203. ENVAR_PROGRAMFILES_X64 = 'PROGRAMFILES'
  204. PATH_PROGRAMFILES_X64 = os.environ[ENVAR_PROGRAMFILES_X64]
  205. # path string constructor, dccsi default WINGHOME location
  206. PATH_WINGHOME = (f'{PATH_PROGRAMFILES_X86}' +
  207. f'\\{SLUG_DCCSI_WING_TYPE} {SLUG_DCCSI_WING_VERSION_MAJOR}')
  208. # path string constructor, userhome where wingstubdb.py can live
  209. PATH_WING_APPDATA = (f'{USER_HOME}' +
  210. f'\\AppData' +
  211. f'\\Roaming' +
  212. f'\\{SLUG_DCCSI_WING_TYPE} {str(SLUG_DCCSI_WING_VERSION_MAJOR)}')
  213. # -------------------------------------------------------------------------
  214. # -------------------------------------------------------------------------
  215. # paths we can retrieve easily from o3de
  216. O3DE_LOG_FOLDER = None # empty containers
  217. O3DE_CACHE = None
  218. ENVAR_PATH_O3DE_PROJECT = 'PATH_O3DE_PROJECT' # project path
  219. ENVAR_O3DE_PROJECT = 'O3DE_PROJECT' # project name
  220. PATH_O3DE_PROJECT = None
  221. ENVAR_O3DE_DEV = 'O3DE_DEV'
  222. O3DE_DEV = None
  223. ENVAR_PATH_O3DE_BIN = 'PATH_O3DE_BIN'
  224. PATH_O3DE_BIN = None
  225. # envar for the 3rdParty
  226. ENVAR_PATH_O3DE_3RDPARTY = 'PATH_O3DE_3RDPARTY'
  227. # this needs to be a path, it's being called that way
  228. # the default for installed builds is C:\Users\<user name>\.o3de\3rdParty
  229. PATH_O3DE_3RDPARTY = Path(O3DE_USER_HOME, '3rdParty').resolve()
  230. if not PATH_O3DE_3RDPARTY.exists():
  231. _LOGGER.warning(f'Default o3de 3rdparty does not exist: {PATH_O3DE_3RDPARTY.as_posix()}')
  232. _LOGGER.warning(f'The engine may not be installed, or you need to run it. The o3de user home needs to be initialized (start o3de.exe).')
  233. try:
  234. import azlmbr.paths
  235. _LOGGER.debug(f'log: {azlmbr.paths.log}')
  236. O3DE_LOG_FOLDER = Path(azlmbr.paths.log)
  237. _LOGGER.debug(f'products: {azlmbr.paths.products}')
  238. O3DE_CACHE = Path(azlmbr.paths.products)
  239. _LOGGER.debug(f'projectroot: {azlmbr.paths.projectroot}')
  240. PATH_O3DE_PROJECT = Path(azlmbr.paths.projectroot)
  241. _LOGGER.debug(f'engroot: {azlmbr.paths.engroot}')
  242. O3DE_DEV = Path(azlmbr.paths.engroot)
  243. O3DE_DEV = add_site_dir(O3DE_DEV)
  244. _LOGGER.debug(f'executableFolder: {azlmbr.paths.executableFolder}')
  245. PATH_O3DE_BIN = Path(azlmbr.paths.executableFolder)
  246. PATH_O3DE_BIN = add_site_dir(PATH_O3DE_BIN)
  247. except ImportError as e:
  248. _LOGGER.warning(f'{e}')
  249. _LOGGER.warning(f'Not running o3do, no: azlmbr.paths')
  250. _LOGGER.warning(f'Using fallbacks ...')
  251. # if DCCSI_STRICT:
  252. # _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  253. # raise e
  254. # -------------------------------------------------------------------------
  255. # -------------------------------------------------------------------------
  256. # next early fallback, check if dev set in settings.local.json
  257. # the try/except block causes bootstrap to fail in o3de editor
  258. # if the settings.local.json file doesn't exist
  259. # even though we are not raising the exception
  260. DCCSI_SETTINGS_DATA = None
  261. if not PATH_DCCSI_SETTINGS_LOCAL.exists():
  262. _LOGGER.warning(f'O3DE DCCsi settings does not exist: {PATH_DCCSI_SETTINGS_LOCAL}')
  263. _LOGGER.info(f'You may want to generate: {PATH_DCCSI_SETTINGS_LOCAL}')
  264. _LOGGER.info(f'Open a CMD at root of DccScriptingInterface, then run:')
  265. _LOGGER.info(f'>.\python config.py')
  266. _LOGGER.info(f'Now open in text editor: {PATH_DCCSI_SETTINGS_LOCAL}')
  267. # we don't actually want to clear this var to none. code below reports about it.
  268. #PATH_DCCSI_SETTINGS_LOCAL = None
  269. elif PATH_DCCSI_SETTINGS_LOCAL.exists():
  270. try:
  271. with open(PATH_DCCSI_SETTINGS_LOCAL, "r") as data:
  272. DCCSI_SETTINGS_DATA = json.load(data)
  273. except IOError as e:
  274. _LOGGER.warning(f'Cannot read manifest: {PATH_DCCSI_SETTINGS_LOCAL} ')
  275. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  276. if DCCSI_SETTINGS_DATA:
  277. _LOGGER.debug(f'O3DE {DCCSI_SETTINGS_LOCAL_FILENAME} found: {PATH_DCCSI_SETTINGS_LOCAL}')
  278. for k, v in DCCSI_SETTINGS_DATA.items():
  279. if k == ENVAR_O3DE_DEV:
  280. O3DE_DEV = Path(v)
  281. elif k == ENVAR_PATH_O3DE_BIN:
  282. PATH_O3DE_BIN = Path(v)
  283. elif k == ENVAR_PATH_O3DE_3RDPARTY:
  284. PATH_O3DE_3RDPARTY = Path(v)
  285. else:
  286. pass
  287. # -------------------------------------------------------------------------
  288. # -------------------------------------------------------------------------
  289. # o3de manifest data
  290. try:
  291. # the manifest doesn't exist until you run o3de.exe for the first time
  292. O3DE_MANIFEST_PATH.resolve(strict=True)
  293. except FileExistsError as e:
  294. _LOGGER.warning(f'O3DE Manifest does not exist: {O3DE_MANIFEST_PATH}')
  295. _LOGGER.warning(f'Make sure the engine is installed, and run O3DE.exe')
  296. _LOGGER.warning(f'Or build from source and and register the engine ')
  297. _LOGGER.warning(f'CMD > c:\\path\\to\\o3de\\scripts\\o3de register --this-engine')
  298. O3DE_MANIFEST_PATH = None
  299. O3DE_MANIFEST_DATA = None
  300. if O3DE_MANIFEST_PATH:
  301. try:
  302. with open(O3DE_MANIFEST_PATH, "r") as data:
  303. O3DE_MANIFEST_DATA = json.load(data)
  304. except IOError as e:
  305. _LOGGER.warning(f'Cannot read manifest: {O3DE_MANIFEST_PATH} ')
  306. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  307. # -------------------------------------------------------------------------
  308. # -------------------------------------------------------------------------
  309. # default o3de engine location
  310. # a suggestion would be for us to refactor from _O3DE_DEV to _O3DE_ROOT
  311. # dev is a legacy Lumberyard concept, as the engine sandbox folder was /dev
  312. _LOGGER.debug(STR_CROSSBAR)
  313. _LOGGER.debug(f'---- Finding {ENVAR_O3DE_DEV}')
  314. if not O3DE_DEV:
  315. # fallback 1, check the .o3de\o3de_manifest.json
  316. # most end users likely only have one engine install?
  317. if O3DE_MANIFEST_DATA:
  318. _LOGGER.debug(f'O3DE Manifest found: {O3DE_MANIFEST_PATH}')
  319. # is it possible to have an active manifest but not have this key?
  320. # I assume that would mainly happen only if manually edited?
  321. # if this returns None, section 'key' doesn't exist
  322. ENGINES = get_key_value(O3DE_MANIFEST_DATA, 'engines')
  323. if ENGINES:
  324. if len(ENGINES) < 1:
  325. _LOGGER(f'no engines in o3de manifest')
  326. # what if there are multiple engines? We don't know which to use
  327. elif len(ENGINES) == 1: # there can only be one
  328. O3DE_DEV = Path(ENGINES[0])
  329. else:
  330. _LOGGER.warning(f'Manifest defines more then one engine: {O3DE_DEV.as_posix()}')
  331. _LOGGER.warning(f'Not sure which to use? We suggest...')
  332. _LOGGER.warning(f"Put 'set {ENVAR_O3DE_DEV}=c:\\path\\to\\o3de' in: {PATH_ENV_DEV}")
  333. _LOGGER.warning(f"And '{ENVAR_O3DE_DEV}:c:\\path\\to\\o3de' in: {PATH_DCCSI_SETTINGS_LOCAL}")
  334. try:
  335. O3DE_DEV.resolve(strict=True) # make sure the found engine exists
  336. _LOGGER.debug(f'O3DE Manifest {ENVAR_O3DE_DEV} is: {O3DE_DEV.as_posix()}')
  337. except NotADirectoryError as e:
  338. _LOGGER.warning(f'Manifest engine does not exist: {O3DE_DEV.as_posix()}')
  339. _LOGGER.warning(f'Make sure the engine is installed, and run O3DE.exe')
  340. _LOGGER.warning(f'Or build from source and and register the engine ')
  341. _LOGGER.warning(f'CMD > c:\\path\\to\\o3de\\scripts\\o3de register --this-engine')
  342. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  343. O3DE_DEV = None
  344. # obvious fallback 2, assume root from dccsi, just walk up ...
  345. # unless the dev has the dccsi somewhere else this should work fine
  346. # or if we move the dccsi out of the engine later we can refactor
  347. if not O3DE_DEV:
  348. O3DE_DEV = PATH_DCCSIG.parents[3]
  349. try:
  350. O3DE_DEV.resolve(strict=True)
  351. _LOGGER.debug(f'{ENVAR_O3DE_DEV} is: {O3DE_DEV.as_posix()}')
  352. except NotADirectoryError as e:
  353. _LOGGER.warning(f'{ENVAR_O3DE_DEV} does not exist: {O3DE_DEV.as_posix()}')
  354. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  355. O3DE_DEV = None
  356. # but it's always best to just be explicit?
  357. # fallback 3, check if a dev set it in env and allow override
  358. O3DE_DEV = os.getenv(ENVAR_O3DE_DEV, str(O3DE_DEV))
  359. if O3DE_DEV: # could still end up None?
  360. O3DE_DEV = Path(O3DE_DEV)
  361. try:
  362. O3DE_DEV.resolve(strict=True)
  363. O3DE_DEV = add_site_dir(O3DE_DEV)
  364. _LOGGER.info(f'Final {ENVAR_O3DE_DEV} is: {O3DE_DEV.as_posix()}')
  365. except NotADirectoryError as e:
  366. _LOGGER.warning(f'{ENVAR_O3DE_DEV} may not exist: {O3DE_DEV.as_posix()}')
  367. O3DE_DEV = None
  368. if DCCSI_STRICT:
  369. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  370. raise e
  371. try:
  372. O3DE_DEV.resolve(strict=True)
  373. # this shouldn't be possible because of fallback 1,
  374. # unless a bad envar override didn't validate above?
  375. except Exception as e:
  376. _LOGGER.warning(f'{ENVAR_O3DE_DEV} not defined: {O3DE_DEV}')
  377. _LOGGER.warning(f'Put "set {ENVAR_O3DE_DEV}=C:\\path\\to\\o3de" in: {PATH_ENV_DEV}')
  378. _LOGGER.warning(f'And "{ENVAR_O3DE_DEV}":"C:\\path\\to\\o3de" in: {PATH_DCCSI_SETTINGS_LOCAL}')
  379. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  380. if DCCSI_STRICT:
  381. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  382. raise e
  383. # -------------------------------------------------------------------------
  384. # -------------------------------------------------------------------------
  385. # o3de game project
  386. # this is a problem of multiple projects
  387. # we can get the local project root if we are running in a o3de exe
  388. O3DE_PROJECT = None # str name of the project
  389. _LOGGER.debug(STR_CROSSBAR)
  390. _LOGGER.debug(f'---- Finding {ENVAR_PATH_O3DE_PROJECT}')
  391. # 2, we could also check the .o3de\o3de_manifest.json?
  392. if not PATH_O3DE_PROJECT:
  393. # but there could be many projects ...
  394. # Let's assume many users will only have one project
  395. if O3DE_MANIFEST_DATA:
  396. PROJECTS = O3DE_MANIFEST_DATA['projects']
  397. if len(PROJECTS) == 1: # there can only be one
  398. PATH_O3DE_PROJECT = PROJECTS[0]
  399. else:
  400. _LOGGER.warning(f'Manifest defines more then one project')
  401. _LOGGER.warning(f'Not sure which to use? We suggest...')
  402. _LOGGER.warning(f'Put "set {ENVAR_PATH_O3DE_PROJECT}=C:\\path\\to\\o3de\\project" in: {PATH_ENV_DEV}')
  403. _LOGGER.warning(f'And "{ENVAR_PATH_O3DE_PROJECT}":"C:\\path\\to\\o3de\\project" in: {PATH_DCCSI_SETTINGS_LOCAL}')
  404. # 3, we can see if a global project is set as the default
  405. if not PATH_O3DE_PROJECT:
  406. O3DE_BOOTSTRAP_DATA = None
  407. if O3DE_BOOTSTRAP_PATH.resolve().exists:
  408. try:
  409. with open(O3DE_BOOTSTRAP_PATH, "r") as data:
  410. O3DE_BOOTSTRAP_DATA = json.load(data)
  411. except IOError as e:
  412. _LOGGER.warning(f'Cannot read bootstrap: {O3DE_BOOTSTRAP_PATH} ')
  413. _LOGGER.error(f'{e}')
  414. if DCCSI_STRICT:
  415. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  416. raise e
  417. else:
  418. _LOGGER.warning(f'o3de engine Bootstrap does not exist: {O3DE_BOOTSTRAP_PATH}')
  419. if O3DE_BOOTSTRAP_DATA:
  420. try:
  421. PATH_O3DE_PROJECT = Path(O3DE_BOOTSTRAP_DATA['Amazon']['AzCore']['Bootstrap']['project_path'])
  422. except KeyError as e:
  423. _LOGGER.warning(f'Bootstrap key error: {e}')
  424. _LOGGER.error(f'{e}')
  425. PATH_O3DE_PROJECT = None
  426. if PATH_O3DE_PROJECT: # we got one
  427. try:
  428. PATH_O3DE_PROJECT.resolve(strict=True)
  429. except NotADirectoryError as e:
  430. _LOGGER.warning(f'Project does not exist: {PATH_O3DE_PROJECT}')
  431. _LOGGER.error(f'{e}')
  432. PATH_O3DE_PROJECT = None
  433. # 4, we can fallback to the DCCsi as the project
  434. # 5, be explicit, and allow it to be set/overridden from the env
  435. PATH_O3DE_PROJECT = os.getenv(ENVAR_PATH_O3DE_PROJECT, str(PATH_DCCSIG))
  436. if PATH_O3DE_PROJECT: # could still end up None?
  437. # note: if you are a developer, and launching via the dccsi windows .bat files
  438. # the DccScriptingInterface gets set in the env as the default project
  439. # which would in this case override the other options above
  440. # when running from a cli in that env, or a IDE launched in that env
  441. try:
  442. PATH_O3DE_PROJECT = Path(PATH_O3DE_PROJECT).resolve(strict=True)
  443. except NotADirectoryError as e:
  444. _LOGGER.warning(f'{e}')
  445. _LOGGER.warning(f'envar {ENVAR_O3DE_PROJECT} may not exist: {ENVAR_O3DE_PROJECT}')
  446. _LOGGER.warning(f'Put "set {ENVAR_O3DE_PROJECT}=C:\\path\\to\\o3de" in : {PATH_ENV_DEV}')
  447. _LOGGER.warning(f'And "{ENVAR_O3DE_PROJECT}":"C:\\path\\to\\o3de" in: {PATH_DCCSI_SETTINGS_LOCAL}')
  448. PATH_O3DE_PROJECT = PATH_DCCSIG # ultimate fallback
  449. # log the final results
  450. _LOGGER.debug(f'Default {ENVAR_PATH_O3DE_PROJECT}: {PATH_O3DE_PROJECT.as_posix()}')
  451. # the projects name, suggestion we should extend this to actually retrieve
  452. # the project name, from a root data file such as project.json or gem.json
  453. O3DE_PROJECT = str(os.getenv(ENVAR_O3DE_PROJECT,
  454. PATH_O3DE_PROJECT.name))
  455. _LOGGER.info(f'Default {ENVAR_O3DE_PROJECT}: {O3DE_PROJECT}')
  456. # -------------------------------------------------------------------------
  457. # -------------------------------------------------------------------------
  458. # default o3de engine bin folder
  459. _LOGGER.debug(STR_CROSSBAR)
  460. _LOGGER.debug(f'---- Finding {ENVAR_PATH_O3DE_BIN}')
  461. # suggestion, create a discovery.py module for the dccsi to find o3de
  462. # \bin could vary a lot in location, between end users and developers...
  463. # engine could be built from source, with unknown 'build' folder location
  464. # because users can name that folder and configure cmake in a number of ways
  465. # also could be a project centric build, which is problematic if we don't
  466. # know the project folder location (block above might derive a default fallback)
  467. # if the user is a dev building from source, and the have some kind of 'build'
  468. # folder under the engine root, we can use this search method (but can be slow)
  469. # import DccScriptingInterface.azpy.config_utils as dccsi_config_utils
  470. # PATH_O3DE_BUILD = dccsi_config_utils.get_o3de_build_path(O3DE_DEV,'CMakeCache.txt')
  471. # and should result in something like: C:\depot\o3de-dev\build
  472. # but pre-built installer builds don't have this file (search fails)
  473. # the next problem is what is the path down from that root to executables?
  474. # the profile executable path for the nightly installer is a default like:
  475. # C:\O3DE\0.0.0.0\bin\Windows\profile\Default
  476. # when I create a build folder, and build engine-centric it's something like:
  477. # C:\depot\o3de-dev\build\bin\profile
  478. # There are a lot of ways we could search and derive.
  479. # We could search and walk down ... what a mess
  480. # o3de provides this method (used above), which standalone tools can't access
  481. # import azlmbr.paths
  482. # _LOGGER.debug(f'engroot: {azlmbr.paths.engroot}')
  483. # fallback 1, # the easiest check is to derive if we are running o3de
  484. if not PATH_O3DE_BIN:
  485. # executable, such as Editor.exe or MaterialEditor.exe
  486. O3DE_EDITOR = Path(sys.executable) # executable path
  487. if O3DE_EDITOR.stem.lower() in {"editor",
  488. "materialeditor",
  489. "materialcanvas",
  490. "passcanvas",
  491. "assetprocessor",
  492. "assetbuilder"}:
  493. PATH_O3DE_BIN = O3DE_EDITOR.parent
  494. # honestly though, I could see this failing in edge cases?
  495. # good enough for now I think...
  496. PATH_O3DE_BIN = add_site_dir(PATH_O3DE_BIN)
  497. elif O3DE_EDITOR.stem.lower() == "python":
  498. pass # we don't know which python, could be a DCC python?
  499. else:
  500. pass # anything else we don't handle here
  501. # (because we are looking for known o3de things)
  502. # all this extra work, is because we want to support the creation
  503. # of standalone external Qt/PySide tools. So we want access to the build
  504. # to load Qt DLLs and such (another example is compiled openimageio)
  505. # A second way would be to check the known default installer location
  506. # Could that still result in multiple installs for developers?
  507. # Projects are usually coded against an engine version
  508. # nightly: C:\O3DE\0.0.0.0
  509. # last stable: C:\O3DE\22.05.0
  510. # pass, leave that to discovery.py (grep a pattern?)
  511. # A third way would be to check the OS registry for custom install location
  512. # this might be the best robust solution for multi-platform?
  513. # pass, this is a prototype leave that to discovery.py
  514. # still has the multi-install/multi-engine problem
  515. # A fourth way, would be to look at o3de data
  516. # if we know the project, check the project.json
  517. # the project.json can define which registered engine to use
  518. # such as 'o3de' versus 'o3de-sdk', providing multi-engine support
  519. # check the .o3de\o3de_manifest.json and retrieve engine path?
  520. # the next way, for now the best way, is to just explicitly set it somewhere
  521. # fallback 2, check if a dev set it in env and allow override
  522. # this works well for a dev in IDE on windows, by setting in Env_Dev.bat
  523. # "DccScriptingInterface\Tools\Dev\Windows\Env_Dev.bat" or
  524. # "DccScriptingInterface\Tools\IDE\Wing\Env_Dev.bat"
  525. if not PATH_O3DE_BIN:
  526. PATH_O3DE_BIN = os.environ.get(ENVAR_PATH_O3DE_BIN)
  527. # if nothing else worked, try using the slow search method
  528. # this will re-trigger this init because of import
  529. # so it's better to move this check into config.py
  530. # if not PATH_O3DE_BIN and O3DE_DEV:
  531. # import DccScriptingInterface.azpy.config_utils as dccsi_config_utils
  532. # PATH_O3DE_BIN = dccsi_config_utils.get_o3de_build_path(O3DE_DEV, 'CMakeCache.txt')
  533. if PATH_O3DE_BIN:
  534. PATH_O3DE_BIN = Path(PATH_O3DE_BIN)
  535. try:
  536. PATH_O3DE_BIN = PATH_O3DE_BIN.resolve(strict=True)
  537. add_site_dir(PATH_O3DE_BIN)
  538. except NotADirectoryError as e:
  539. _LOGGER.warning(f'{ENVAR_PATH_O3DE_BIN} may not exist: {PATH_O3DE_BIN.as_posix()}')
  540. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  541. PATH_O3DE_BIN = None
  542. if DCCSI_STRICT:
  543. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  544. raise e
  545. try:
  546. PATH_O3DE_BIN.resolve(strict=True)
  547. os.add_dll_directory(f'{PATH_O3DE_BIN}')
  548. except Exception as e:
  549. _LOGGER.warning(f'{ENVAR_PATH_O3DE_BIN} not defined: {PATH_O3DE_BIN}')
  550. _LOGGER.warning(f'Put "set {ENVAR_PATH_O3DE_BIN}=C:\\path\\to\\o3de" in: {PATH_ENV_DEV}')
  551. _LOGGER.warning(f'And "{ENVAR_PATH_O3DE_BIN}":"C:\\path\\to\\o3de" in: {PATH_DCCSI_SETTINGS_LOCAL}')
  552. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  553. PATH_O3DE_BIN = None
  554. if DCCSI_STRICT:
  555. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  556. raise e
  557. else:
  558. _LOGGER.warning(f'some modules functionality may fail if no {ENVAR_PATH_O3DE_BIN} is defined')
  559. _LOGGER.info(f'Final {ENVAR_PATH_O3DE_BIN} is: {str(PATH_O3DE_BIN)}')
  560. # -------------------------------------------------------------------------
  561. # -------------------------------------------------------------------------
  562. # QtForPython (PySide2) is a DCCsi Gem dependency
  563. # Can't be initialized in the dev env, because Wing 9 is a Qt5 app
  564. # and this causes interference, so replicate here
  565. if not PATH_O3DE_3RDPARTY:
  566. # default path, matches o3de installer builds
  567. try:
  568. PATH_O3DE_3RDPARTY = USER_HOME.joinpath(f'{TAG_USER_O3DE}',
  569. '3rdparty').resolve(strict=True)
  570. except NotADirectoryError as e:
  571. _LOGGER.warning(f'The engine may not be built yet ...')
  572. _LOGGER.warning(f'Or User is not using an installer build ...')
  573. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  574. PATH_O3DE_3RDPARTY = None
  575. # attempt to get from env
  576. PATH_O3DE_3RDPARTY = os.environ.get(ENVAR_PATH_O3DE_3RDPARTY)
  577. try:
  578. PATH_O3DE_3RDPARTY = Path(PATH_O3DE_3RDPARTY).resolve(strict=True)
  579. except Exception as e:
  580. _LOGGER.warning(f'{ENVAR_PATH_O3DE_3RDPARTY} will not resolve: {PATH_O3DE_3RDPARTY}')
  581. _LOGGER.warning(f'Put "set {ENVAR_PATH_O3DE_3RDPARTY}=C:\\path\\to\\o3de" in: {PATH_ENV_DEV}')
  582. _LOGGER.warning(f'And "{ENVAR_PATH_O3DE_3RDPARTY}":"C:\\path\\to\\o3de" in: {PATH_DCCSI_SETTINGS_LOCAL}')
  583. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  584. if DCCSI_STRICT:
  585. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  586. raise e
  587. _LOGGER.info(f'Final {ENVAR_PATH_O3DE_3RDPARTY} is: {str(PATH_O3DE_3RDPARTY)}')
  588. # -------------------------------------------------------------------------
  589. # -------------------------------------------------------------------------
  590. # first check if we can get from o3de
  591. ENVAR_QT_PLUGIN_PATH = 'QT_PLUGIN_PATH'
  592. if DCCSI_TEST_PYSIDE:
  593. try:
  594. import azlmbr
  595. import azlmbr.bus
  596. params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, 'GetQtBootstrapParameters')
  597. QT_PLUGIN_PATH = Path(params.qtPluginsFolder)
  598. except:
  599. QT_PLUGIN_PATH = None
  600. if not QT_PLUGIN_PATH:
  601. # fallback, not future proof without editing file
  602. # path to PySide could change! (this is a prototype)
  603. # modify to be a grep?
  604. # path constructor
  605. QT_PLUGIN_PATH = Path(PATH_O3DE_3RDPARTY,
  606. 'packages',
  607. 'pyside2-5.15.2.1-py3.10-rev3-windows',
  608. 'pyside2',
  609. 'lib',
  610. 'site-packages')
  611. try:
  612. QT_PLUGIN_PATH = QT_PLUGIN_PATH.resolve(strict=True)
  613. os.environ[ENVAR_QT_PLUGIN_PATH] = str(QT_PLUGIN_PATH)
  614. add_site_dir(QT_PLUGIN_PATH)
  615. except Exception as e:
  616. _LOGGER.warning(f'{ENVAR_QT_PLUGIN_PATH} will not resolve: {QT_PLUGIN_PATH}')
  617. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  618. if DCCSI_STRICT:
  619. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  620. raise e
  621. try:
  622. import azlmbr
  623. import azlmbr.bus
  624. params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, 'GetQtBootstrapParameters')
  625. O3DE_QT_BIN = Path(params.qtBinaryFolder)
  626. except:
  627. O3DE_QT_BIN = PATH_O3DE_BIN
  628. if len(str(O3DE_QT_BIN)) and sys.platform.startswith('win'):
  629. path = os.environ['PATH']
  630. new_path = ''
  631. new_path += str(O3DE_QT_BIN) + os.pathsep
  632. new_path += path
  633. os.environ['PATH'] = new_path
  634. add_site_dir(O3DE_QT_BIN)
  635. try:
  636. import PySide2
  637. from PySide2.QtWidgets import QPushButton
  638. _LOGGER.info('PySide2 bootstrapped PATH for Windows.')
  639. except ImportError as e:
  640. _LOGGER.warning('Cannot import PySide2.')
  641. _LOGGER.error(f'{e} , traceback =', exc_info=True)
  642. if DCCSI_STRICT:
  643. _LOGGER.exception(f'{e} , traceback =', exc_info=True)
  644. raise e
  645. # -------------------------------------------------------------------------
  646. # -------------------------------------------------------------------------
  647. # This will fail importing the DccScriptingInterface in DCC tools
  648. # that do not have python pkgs dependencies installed (requirements.txt)
  649. # the user can do this with foundation.py
  650. try:
  651. from dynaconf import LazySettings
  652. except ImportError as e:
  653. _LOGGER.error(f'Could not import dynaconf')
  654. _LOGGER.info(f'Most likely python package dependencies are not installed for target runtime')
  655. _LOGGER.info(f'Py EXE is: {sys.executable}')
  656. _LOGGER.info(f'The Python version running: {sys.version_info[0]}.{sys.version_info[1]}')
  657. _LOGGER.info(f'{ENVAR_PATH_DCCSI_PYTHON_LIB} location is: {PATH_DCCSI_PYTHON_LIB}')
  658. _LOGGER.info(f'Follow these steps then re-start the DCC app (or other target):')
  659. _LOGGER.info(f'1. open a cmd prompt')
  660. _LOGGER.info(f'2. change directory to: {PATH_DCCSIG}')
  661. _LOGGER.info(f'3. run this command...')
  662. _LOGGER.info(f'4. >.\python foundation.py -py="{sys.executable}"')
  663. _LOGGER.error(f'{e} , traceback =', exc_info = True)
  664. pass # be forgiving
  665. # settings = LazySettings(
  666. # # Loaded first
  667. # PRELOAD_FOR_DYNACONF=["/path/*", "other/settings.toml"],
  668. # # Loaded second (the main file)
  669. # SETTINGS_FILE_FOR_DYNACONF="/etc/foo/settings.py",
  670. # #Loaded at the end
  671. # INCLUDES_FOR_DYNACONF=["other.module.settings", "other/settings.yaml"]
  672. # )
  673. SETTINGS_FILE_SLUG = 'settings.json'
  674. LOCAL_SETTINGS_FILE_SLUG = 'settings.local.json'
  675. PATH_DCCSIG_SETTINGS = PATH_DCCSIG.joinpath(SETTINGS_FILE_SLUG).resolve()
  676. PATH_DCCSIG_LOCAL_SETTINGS = PATH_DCCSIG.joinpath(LOCAL_SETTINGS_FILE_SLUG).resolve()
  677. # settings = LazySettings(
  678. # SETTINGS_FILE_FOR_DYNACONF=PATH_DCCSIG_SETTINGS.as_posix(),
  679. # INCLUDES_FOR_DYNACONF=[PATH_DCCSIG_LOCAL_SETTINGS.as_posix()]
  680. # )
  681. # settings.setenv()
  682. # -------------------------------------------------------------------------
  683. _MODULE_END = timeit.default_timer() - __MODULE_START
  684. _LOGGER.debug(f'{_PACKAGENAME}.init complete')
  685. _LOGGER.debug(f'{_PACKAGENAME}.init took: {_MODULE_END} sec')
  686. _LOGGER.debug(STR_CROSSBAR)
  687. # -------------------------------------------------------------------------
  688. # -------------------------------------------------------------------------
  689. # will probably deprecate this whole block to avoid cyclical imports
  690. # this doesn't need to be an entrypoint or debugged from cli
  691. # from DccScriptingInterface.globals import *
  692. # from azpy.config_utils import attach_debugger
  693. # from azpy import test_imports
  694. # # suggestion would be to turn this into a method to reduce boilerplate
  695. # # but where to put it that makes sense?
  696. # if DCCSI_DEV_MODE:
  697. # # if dev mode, this will attempt to auto-attach the debugger
  698. # # at the earliest possible point in this module
  699. # attach_debugger(debugger_type=DCCSI_GDEBUGGER)
  700. #
  701. # _LOGGER.debug(f'Testing Imports from {_PACKAGENAME}')
  702. #
  703. # # If in dev mode and test is flagged this will force imports of __all__
  704. # # although slower and verbose, this can help detect cyclical import
  705. # # failure and other issues
  706. #
  707. # # the DCCSI_TESTS flag needs to be properly added in .bat env
  708. # if DCCSI_TESTS:
  709. # test_imports(_all=__all__,
  710. # _pkg=_PACKAGENAME,
  711. # _logger=_LOGGER)
  712. # -------------------------------------------------------------------------