LYPython.cmake 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. include_guard()
  9. include(cmake/LySet.cmake)
  10. include(cmake/3rdPartyPackages.cmake)
  11. # this script exists to make sure a python interpreter is immediately available
  12. # it will both locate and run pip on python for our requirements.txt
  13. # but you can also call update_pip_requirements(filename) at any time after.
  14. # this is different from the usual package usage, because even if we are targeting
  15. # android, for example, we may still be doing so on a windows HOST pc, and the
  16. # python interpreter we want to use is for the windows HOST pc, not the PAL platform:
  17. set(LY_PAL_PYTHON_PACKAGE_FILE_NAME ${LY_ROOT_FOLDER}/cmake/3rdParty/Platform/${PAL_HOST_PLATFORM_NAME}/Python_${PAL_HOST_PLATFORM_NAME_LOWERCASE}${LY_HOST_ARCHITECTURE_NAME_EXTENSION}.cmake)
  18. cmake_path(NORMAL_PATH LY_PAL_PYTHON_PACKAGE_FILE_NAME)
  19. include(${LY_PAL_PYTHON_PACKAGE_FILE_NAME})
  20. # settings and globals
  21. ly_set(LY_PYTHON_DEFAULT_REQUIREMENTS_TXT "${LY_ROOT_FOLDER}/python/requirements.txt")
  22. # The expected venv for python is based on an ID generated based on the path of the current
  23. # engine using the logic in cmake/CalculateEnginePathId.cmake.
  24. execute_process(COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/CalculateEnginePathId.cmake "${CMAKE_CURRENT_SOURCE_DIR}/"
  25. OUTPUT_VARIABLE ENGINE_SOURCE_PATH_ID
  26. OUTPUT_STRIP_TRAILING_WHITESPACE)
  27. # Normalize the expected location of the Python venv and set its path globally
  28. # The root Python folder is based off of the same folder as the manifest file
  29. if(DEFINED ENV{USERPROFILE} AND EXISTS $ENV{USERPROFILE})
  30. set(PYTHON_ROOT_PATH "$ENV{USERPROFILE}/.o3de/Python") # Windows
  31. else()
  32. set(PYTHON_ROOT_PATH "$ENV{HOME}/.o3de/Python") # Unix
  33. endif()
  34. set(PYTHON_PACKAGES_ROOT_PATH "${PYTHON_ROOT_PATH}/packages")
  35. cmake_path(NORMAL_PATH PYTHON_PACKAGES_ROOT_PATH )
  36. set(PYTHON_PACKAGE_CACHE_ROOT_PATH "${PYTHON_ROOT_PATH}/downloaded_packages")
  37. cmake_path(NORMAL_PATH PYTHON_PACKAGE_CACHE_ROOT_PATH )
  38. set(PYTHON_VENV_PATH "${PYTHON_ROOT_PATH}/venv/${ENGINE_SOURCE_PATH_ID}")
  39. cmake_path(NORMAL_PATH PYTHON_VENV_PATH )
  40. ly_set(LY_PYTHON_VENV_PATH ${PYTHON_VENV_PATH})
  41. function(ly_setup_python_venv)
  42. # Check if we need to setup a new venv.
  43. set(CREATE_NEW_VENV FALSE)
  44. # We track if an existing venv matches the current version of the Python 3rd Party Package
  45. # by tracking the package hash within the venv folder. If there it is missing or does not
  46. # match the current package hash ${LY_PYTHON_PACKAGE_HASH} then reset the venv and request
  47. # that a new venv is created with the current Python package at :
  48. #
  49. if (EXISTS "${PYTHON_VENV_PATH}/.hash")
  50. file(READ "${PYTHON_VENV_PATH}/.hash" LY_CURRENT_VENV_PACKAGE_HASH
  51. LIMIT 80)
  52. if (NOT ${LY_CURRENT_VENV_PACKAGE_HASH} STREQUAL ${LY_PYTHON_PACKAGE_HASH})
  53. # The package hash changed, re-install the venv
  54. set(CREATE_NEW_VENV TRUE)
  55. file(REMOVE_RECURSE "${PYTHON_VENV_PATH}")
  56. else()
  57. # Sanity check to make sure the python launcher in the venv works
  58. execute_process(COMMAND ${PYTHON_VENV_PATH}/${LY_PYTHON_VENV_PYTHON} --version
  59. OUTPUT_QUIET
  60. RESULT_VARIABLE command_result)
  61. if (NOT ${command_result} EQUAL 0)
  62. message(STATUS "Error validating python inside the venv. Reinstalling")
  63. set(CREATE_NEW_VENV TRUE)
  64. file(REMOVE_RECURSE "${PYTHON_VENV_PATH}")
  65. else()
  66. message(STATUS "Using Python venv at ${PYTHON_VENV_PATH}")
  67. endif()
  68. endif()
  69. else()
  70. set(CREATE_NEW_VENV TRUE)
  71. endif()
  72. if (CREATE_NEW_VENV)
  73. # Run the install venv command, but skip the pip setup because we may need to manually create
  74. # a link to the shared library within the created virtual environment before proceeding with
  75. # the pip install command.
  76. message(STATUS "Creating Python venv at ${PYTHON_VENV_PATH}")
  77. execute_process(COMMAND "${PYTHON_PACKAGES_ROOT_PATH}/${LY_PYTHON_PACKAGE_NAME}/${LY_PYTHON_BIN_PATH}/${LY_PYTHON_EXECUTABLE}" -m venv "${PYTHON_VENV_PATH}" --without-pip --clear
  78. WORKING_DIRECTORY "${PYTHON_PACKAGES_ROOT_PATH}/${LY_PYTHON_PACKAGE_NAME}/${LY_PYTHON_BIN_PATH}"
  79. COMMAND_ECHO STDOUT
  80. RESULT_VARIABLE command_result)
  81. if (NOT ${command_result} EQUAL 0)
  82. message(FATAL_ERROR "Error creating a venv")
  83. endif()
  84. ly_post_python_venv_install(${PYTHON_VENV_PATH})
  85. # Manually install pip into the virtual environment
  86. message(STATUS "Installing pip into venv at ${PYTHON_VENV_PATH} (${PYTHON_VENV_PATH}/${LY_PYTHON_BIN_PATH})")
  87. execute_process(COMMAND "${PYTHON_VENV_PATH}/${LY_PYTHON_VENV_PYTHON}" -m ensurepip --upgrade --default-pip -v
  88. WORKING_DIRECTORY "${PYTHON_VENV_PATH}/${LY_PYTHON_VENV_BIN_PATH}"
  89. COMMAND_ECHO STDOUT
  90. RESULT_VARIABLE command_result)
  91. if (NOT ${command_result} EQUAL 0)
  92. message(FATAL_ERROR "Error installing pip into venv: ${LY_PIP_ERROR}")
  93. endif()
  94. file(WRITE "${PYTHON_VENV_PATH}/.hash" ${LY_PYTHON_PACKAGE_HASH})
  95. endif()
  96. endfunction()
  97. # update_pip_requirements
  98. # param: requirements_file_path = path to a requirements.txt file.
  99. # ensures that all the requirements in the requirements.txt are present.
  100. # you can call it repeatedly on sub-requirement.txt files (for exmaple, in gems)
  101. # note that unique_name is a string of your choosing to track and refer to this
  102. # file, and should be unique to your particular gem/package/3rdParty. It will be
  103. # used as a file name, so avoid special characters that would fail as a file name.
  104. function(update_pip_requirements requirements_file_path unique_name)
  105. # we run with --no-deps to prevent it from cascading to child dependencies
  106. # and getting more than we expect.
  107. # to produce a new requirements.txt use pip-compile from pip-tools alongside pip freeze
  108. set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${requirements_file_path})
  109. # We skip running requirements.txt if we can (we use a stamp file to keep track)
  110. # the stamp file is kept in the binary folder with a similar file path to the source file.
  111. set(stamp_file ${LY_PYTHON_VENV_PATH}/requirements_files/${unique_name}.stamp)
  112. get_filename_component(stamp_file_directory ${stamp_file} DIRECTORY)
  113. file(MAKE_DIRECTORY ${stamp_file_directory})
  114. if(EXISTS ${stamp_file} AND ${stamp_file} IS_NEWER_THAN ${requirements_file_path})
  115. # this means we more recently ran PIP than the requirements file was changed.
  116. # however, users may have deleted and reinstalled python. If this happens
  117. # then the package will be newer than our stamp file, and we must not return.
  118. ly_package_is_newer_than(${LY_PYTHON_PACKAGE_NAME} ${stamp_file} package_is_newer)
  119. if (NOT package_is_newer)
  120. # we can early out becuase the python installation is older than our stamp file
  121. # and the stamp file is newer than the requirements.txt
  122. return()
  123. endif()
  124. endif()
  125. message(CHECK_START "Python: Getting/Checking packages listed in ${requirements_file_path}")
  126. # dont allow or use installs in python %USER% location.
  127. if (NOT DEFINED ENV{PYTHONNOUSERSITE})
  128. set(REMOVE_USERSITE TRUE)
  129. endif()
  130. set(ENV{PYTHONNOUSERSITE} 1)
  131. execute_process(COMMAND
  132. ${LY_PYTHON_CMD} -m pip install -r "${requirements_file_path}" --disable-pip-version-check --no-warn-script-location
  133. WORKING_DIRECTORY ${LY_ROOT_FOLDER}/python
  134. RESULT_VARIABLE PIP_RESULT
  135. OUTPUT_VARIABLE PIP_OUT
  136. ERROR_VARIABLE PIP_OUT
  137. )
  138. message(VERBOSE "pip result: ${PIP_RESULT}")
  139. message(VERBOSE "pip output: ${PIP_OUT}")
  140. if (NOT ${PIP_RESULT} EQUAL 0)
  141. message(CHECK_FAIL "Failed to fetch / update python dependencies from ${requirements_file_path}\nPip install log:\n${PIP_OUT}")
  142. message(FATAL_ERROR "The above failure will cause errors later - stopping now. Check the output log (above) for details.")
  143. else()
  144. string(FIND "${PIP_OUT}" "Installing collected packages" NEW_PACKAGES_INSTALLED)
  145. if (NOT ${NEW_PACKAGES_INSTALLED} EQUAL -1)
  146. # this indicates it was found, meaning new stuff was installed.
  147. # in this case, output the pip output to normal message mode
  148. message(VERBOSE ${PIP_OUT})
  149. message(CHECK_PASS "New packages were installed")
  150. else()
  151. message(CHECK_PASS "Already up to date.")
  152. endif()
  153. # since we're in a success state, stamp the stampfile so we don't run this rule again
  154. # unless someone updates the requirements.txt file.
  155. file(TOUCH ${stamp_file})
  156. endif()
  157. # finally, verify that all packages are OK. This runs locally and does not
  158. # hit any repos, so its fairly quick.
  159. execute_process(COMMAND
  160. ${LY_PYTHON_CMD} -m pip check
  161. WORKING_DIRECTORY ${LY_ROOT_FOLDER}/python
  162. RESULT_VARIABLE PIP_RESULT
  163. OUTPUT_VARIABLE PIP_OUT
  164. ERROR_VARIABLE PIP_OUT
  165. )
  166. message(VERBOSE "Results from pip check: ${PIP_OUT}")
  167. if (NOT ${PIP_RESULT} EQUAL 0)
  168. message(WARNING "PIP reports unmet dependencies: ${PIP_OUT}")
  169. endif()
  170. if (REMOVE_USERSITE)
  171. unset(ENV{PYTHONNOUSERSITE})
  172. endif()
  173. endfunction()
  174. # allows you to install a folder into your site packages as an 'editable' package
  175. # meaning, it will show up in python but not actually be copied to your site-packages
  176. # folder, instead, it will be linked from there.
  177. # the pip_package_name should be the name given to the package in setup.py so that
  178. # any old versions may be uninstalled using setuptools before we install the new one.
  179. function(ly_pip_install_local_package_editable package_folder_path pip_package_name)
  180. set(stamp_file ${LY_PYTHON_VENV_PATH}/packages/pip_installs/${pip_package_name}.stamp)
  181. get_filename_component(stamp_file_directory ${stamp_file} DIRECTORY)
  182. file(MAKE_DIRECTORY ${stamp_file_directory})
  183. # for the first release of the o3de snap we will only use packages shipped with o3de
  184. if ($ENV{O3DE_SNAP})
  185. file(TOUCH ${stamp_file})
  186. endif()
  187. # we only ever need to do this once per runtime install, since its a link
  188. # not an actual install:
  189. # If setup.py changes we must reinstall the package in case its dependencies changed
  190. set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${package_folder_path}/setup.py)
  191. if(EXISTS ${stamp_file} AND ${stamp_file} IS_NEWER_THAN ${package_folder_path}/setup.py)
  192. ly_package_is_newer_than(${LY_PYTHON_PACKAGE_NAME} ${stamp_file} package_is_newer)
  193. if (NOT package_is_newer)
  194. # no need to run the command again, as the package is older than the stamp file
  195. # and the stamp file exists.
  196. return()
  197. endif()
  198. endif()
  199. message(CHECK_START "Python: linking ${package_folder_path} into python site-packages...")
  200. # dont allow or use installs in python %USER% location.
  201. if (NOT DEFINED ENV{PYTHONNOUSERSITE})
  202. set(REMOVE_USERSITE TRUE)
  203. endif()
  204. set(ENV{PYTHONNOUSERSITE} 1)
  205. # uninstall any old versions of this package first:
  206. execute_process(COMMAND
  207. ${LY_PYTHON_CMD} -m pip uninstall ${pip_package_name} -y --disable-pip-version-check
  208. WORKING_DIRECTORY ${LY_ROOT_FOLDER}/python
  209. RESULT_VARIABLE PIP_RESULT
  210. OUTPUT_VARIABLE PIP_OUT
  211. ERROR_VARIABLE PIP_OUT
  212. )
  213. # we discard the error output of above, since it might not be installed, which is ok
  214. message(VERBOSE "pip uninstall result: ${PIP_RESULT}")
  215. message(VERBOSE "pip uninstall output: ${PIP_OUT}")
  216. # now install the new one:
  217. execute_process(COMMAND
  218. ${LY_PYTHON_CMD} -m pip install -e ${package_folder_path} --no-deps --disable-pip-version-check --no-warn-script-location
  219. WORKING_DIRECTORY ${LY_ROOT_FOLDER}/python
  220. RESULT_VARIABLE PIP_RESULT
  221. OUTPUT_VARIABLE PIP_OUT
  222. ERROR_VARIABLE PIP_OUT
  223. )
  224. message(VERBOSE "pip install result: ${PIP_RESULT}")
  225. message(VERBOSE "pip install output: ${PIP_OUT}")
  226. if (NOT ${PIP_RESULT} EQUAL 0)
  227. message(CHECK_FAIL "Failed to install ${package_folder_path}: ${PIP_OUT} - use CMAKE_MESSAGE_LOG_LEVEL to VERBOSE for more information")
  228. message(FATAL_ERROR "Failure to install a python package will likely cause errors further down the line, stopping!")
  229. else()
  230. file(TOUCH ${stamp_file})
  231. endif()
  232. if (REMOVE_USERSITE)
  233. unset(ENV{PYTHONNOUSERSITE})
  234. endif()
  235. endfunction()
  236. # python is a special case of third party:
  237. # * We download it into a folder in the build tree
  238. # * The package is modified as time goes on (for example, PYC files appear)
  239. # * we add pip-packages to the site-packages folder
  240. # Because of this, we want a strict verification the first time we download it
  241. # But we don't want to full verify using hashes after we successfully get it the
  242. # first time.
  243. # We need to download the associated Python package early and install the venv
  244. ly_associate_package(PACKAGE_NAME ${LY_PYTHON_PACKAGE_NAME} TARGETS "Python" PACKAGE_HASH ${LY_PYTHON_PACKAGE_HASH})
  245. ly_set_package_download_location(${LY_PYTHON_PACKAGE_NAME} ${PYTHON_PACKAGES_ROOT_PATH})
  246. ly_set_package_download_cache_location(${LY_PYTHON_PACKAGE_NAME} ${PYTHON_PACKAGE_CACHE_ROOT_PATH})
  247. ly_download_associated_package(Python)
  248. ly_setup_python_venv()
  249. if (NOT CMAKE_SCRIPT_MODE_FILE)
  250. # note - if you want to use a normal python via FindPython instead of the LY package above,
  251. # you may have to declare the below variables after find_package, as the project scripts are
  252. # looking for the below variables specifically.
  253. find_package(Python ${LY_PYTHON_VERSION} REQUIRED)
  254. # verify the required variables are present:
  255. if (NOT EXISTS "${PYTHON_VENV_PATH}/" OR NOT Python_EXECUTABLE OR NOT Python_HOME OR NOT Python_PATHS)
  256. message(SEND_ERROR "Python installation not valid expected to find all of the following variables set:")
  257. message(STATUS " Python_EXECUTABLE: ${Python_EXECUTABLE}")
  258. message(STATUS " Python_HOME: ${Python_HOME}")
  259. message(STATUS " Python_PATHS: ${Python_PATHS}")
  260. else()
  261. LIST(APPEND CMAKE_PROGRAM_PATH "${LY_ROOT_FOLDER}/python")
  262. # those using python should call it via LY_PYTHON_CMD - it adds the extra "-s" param
  263. # this param causes python to ignore the users profile folder which can have bogus
  264. # pip installation modules and lead to machine-specific config problems
  265. # it also uses our wrapper python, which can add additional paths to the python path
  266. if (${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Windows")
  267. set(LY_PYTHON_CMD "${LY_ROOT_FOLDER}/python/python.cmd" "-s")
  268. else()
  269. set(LY_PYTHON_CMD "${LY_ROOT_FOLDER}/python/python.sh" "-s")
  270. endif()
  271. update_pip_requirements(${LY_PYTHON_DEFAULT_REQUIREMENTS_TXT} default_requirements)
  272. # we also need to make sure any custom packages are installed.
  273. # this costs a moment of time though, so we'll only do it based on stamp files.
  274. if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND NOT INSTALLED_ENGINE)
  275. ly_pip_install_local_package_editable(${LY_ROOT_FOLDER}/Tools/LyTestTools ly-test-tools)
  276. ly_pip_install_local_package_editable(${LY_ROOT_FOLDER}/Tools/RemoteConsole/ly_remote_console ly-remote-console)
  277. ly_pip_install_local_package_editable(${LY_ROOT_FOLDER}/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools editor-python-test-tools)
  278. endif()
  279. ly_pip_install_local_package_editable(${LY_ROOT_FOLDER}/scripts/o3de o3de)
  280. endif()
  281. endif()