3
0

LYPython.cmake 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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. # this script exists to make sure a python interpreter is immediately available
  11. # it will both locate and run pip on python for our requirements.txt
  12. # but you can also call update_pip_requirements(filename) at any time after.
  13. # this is different from the usual package usage, because even if we are targetting
  14. # android, for example, we may still be doing so on a windows HOST pc, and the
  15. # python interpreter we want to use is for the windows HOST pc, not the PAL platform:
  16. # CMAKE_HOST_SYSTEM_NAME is "Windows", "Darwin", or "Linux" in our cases..
  17. if (${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Linux" )
  18. # Note: CMAKE_HOST_SYSTEM_PROCESSOR may not available in this script if it is not
  19. # invoked from the base CMakeList.txt since project needs to be declared.
  20. # We will extract the host architecture manually if this script is called externally
  21. if (${CMAKE_SYSTEM_ARCHITECTURE})
  22. set(LINUX_HOST_ARCHITECTURE ${CMAKE_SYSTEM_ARCHITECTURE})
  23. else()
  24. execute_process(COMMAND uname -m OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE LINUX_HOST_ARCHITECTURE)
  25. endif()
  26. if (${LINUX_HOST_ARCHITECTURE} STREQUAL "x86_64")
  27. ly_set(LY_PYTHON_VERSION 3.10.5)
  28. ly_set(LY_PYTHON_VERSION_MAJOR_MINOR 3.10)
  29. ly_set(LY_PYTHON_PACKAGE_NAME python-3.10.5-rev4-linux)
  30. ly_set(LY_PYTHON_PACKAGE_HASH 0144a4a5c9d39a834a319c98ce021741dac579bc39f60a08b6784b71d0983462)
  31. elseif(${LINUX_HOST_ARCHITECTURE} STREQUAL "aarch64")
  32. ly_set(LY_PYTHON_VERSION 3.10.5)
  33. ly_set(LY_PYTHON_VERSION_MAJOR_MINOR 3.10)
  34. ly_set(LY_PYTHON_PACKAGE_NAME python-3.10.5-rev4-linux-aarch64)
  35. ly_set(LY_PYTHON_PACKAGE_HASH c6e5e9cecad36ee1baa3bcbec629fb1cdeb6854f73132e0ca2d97f5b3b8b3a0e)
  36. else()
  37. message(FATAL_ERROR "Linux host architecture ${LINUX_HOST_ARCHITECTURE} not supported.")
  38. endif()
  39. elseif (${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Darwin" )
  40. ly_set(LY_PYTHON_VERSION 3.10.5)
  41. ly_set(LY_PYTHON_VERSION_MAJOR_MINOR 3.10)
  42. ly_set(LY_PYTHON_PACKAGE_NAME python-3.10.5-rev2-darwin)
  43. ly_set(LY_PYTHON_PACKAGE_HASH 46d7c74c64bf639279c53a68ff958d9955e01e08d293524958eb7ea7cac9c4c5)
  44. elseif (${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Windows" )
  45. ly_set(LY_PYTHON_VERSION 3.10.5)
  46. ly_set(LY_PYTHON_VERSION_MAJOR_MINOR 3.10)
  47. ly_set(LY_PYTHON_PACKAGE_NAME python-3.10.5-rev1-windows)
  48. ly_set(LY_PYTHON_PACKAGE_HASH c012e7c8fd20e632446d2cd689a9472e4e4495da7534d484d0f1c63840222cbb)
  49. endif()
  50. # settings and globals
  51. ly_set(LY_PYTHON_DEFAULT_REQUIREMENTS_TXT "${LY_ROOT_FOLDER}/python/requirements.txt")
  52. include(cmake/3rdPartyPackages.cmake)
  53. # update_pip_requirements
  54. # param: requirements_file_path = path to a requirements.txt file.
  55. # ensures that all the requirements in the requirements.txt are present.
  56. # you can call it repeatedly on sub-requirement.txt files (for exmaple, in gems)
  57. # note that unique_name is a string of your choosing to track and refer to this
  58. # file, and should be unique to your particular gem/package/3rdParty. It will be
  59. # used as a file name, so avoid special characters that would fail as a file name.
  60. function(update_pip_requirements requirements_file_path unique_name)
  61. # we run with --no-deps to prevent it from cascading to child dependencies
  62. # and getting more than we expect.
  63. # to produce a new requirements.txt use pip-compile from pip-tools alongside pip freeze
  64. set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${requirements_file_path})
  65. # We skip running requirements.txt if we can (we use a stamp file to keep track)
  66. # the stamp file is kept in the binary folder with a similar file path to the source file.
  67. set(stamp_file ${CMAKE_BINARY_DIR}/packages/requirements_files/${unique_name}.stamp)
  68. get_filename_component(stamp_file_directory ${stamp_file} DIRECTORY)
  69. file(MAKE_DIRECTORY ${stamp_file_directory})
  70. if(EXISTS ${stamp_file} AND ${stamp_file} IS_NEWER_THAN ${requirements_file_path})
  71. # this means we more recently ran PIP than the requirements file was changed.
  72. # however, users may have deleted and reinstalled python. If this happens
  73. # then the package will be newer than our stamp file, and we must not return.
  74. ly_package_is_newer_than(${LY_PYTHON_PACKAGE_NAME} ${stamp_file} package_is_newer)
  75. if (NOT package_is_newer)
  76. # we can early out becuase the python installation is older than our stamp file
  77. # and the stamp file is newer than the requirements.txt
  78. return()
  79. endif()
  80. endif()
  81. message(CHECK_START "Python: Getting/Checking packages listed in ${requirements_file_path}")
  82. # dont allow or use installs in python %USER% location.
  83. if (NOT DEFINED ENV{PYTHONNOUSERSITE})
  84. set(REMOVE_USERSITE TRUE)
  85. endif()
  86. set(ENV{PYTHONNOUSERSITE} 1)
  87. execute_process(COMMAND
  88. ${LY_PYTHON_CMD} -m pip install -r "${requirements_file_path}" --disable-pip-version-check --no-warn-script-location
  89. WORKING_DIRECTORY ${Python_BINFOLDER}
  90. RESULT_VARIABLE PIP_RESULT
  91. OUTPUT_VARIABLE PIP_OUT
  92. ERROR_VARIABLE PIP_OUT
  93. )
  94. message(VERBOSE "pip result: ${PIP_RESULT}")
  95. message(VERBOSE "pip output: ${PIP_OUT}")
  96. if (NOT ${PIP_RESULT} EQUAL 0)
  97. message(CHECK_FAIL "Failed to fetch / update python dependencies from ${requirements_file_path}\nPip install log:\n${PIP_OUT}")
  98. message(FATAL_ERROR "The above failure will cause errors later - stopping now. Check the output log (above) for details.")
  99. else()
  100. string(FIND "${PIP_OUT}" "Installing collected packages" NEW_PACKAGES_INSTALLED)
  101. if (NOT ${NEW_PACKAGES_INSTALLED} EQUAL -1)
  102. # this indicates it was found, meaning new stuff was installed.
  103. # in this case, output the pip output to normal message mode
  104. message(VERBOSE ${PIP_OUT})
  105. message(CHECK_PASS "New packages were installed")
  106. else()
  107. message(CHECK_PASS "Already up to date.")
  108. endif()
  109. # since we're in a success state, stamp the stampfile so we don't run this rule again
  110. # unless someone updates the requirements.txt file.
  111. file(TOUCH ${stamp_file})
  112. endif()
  113. # finally, verify that all packages are OK. This runs locally and does not
  114. # hit any repos, so its fairly quick.
  115. execute_process(COMMAND
  116. ${LY_PYTHON_CMD} -m pip check
  117. WORKING_DIRECTORY ${Python_BINFOLDER}
  118. RESULT_VARIABLE PIP_RESULT
  119. OUTPUT_VARIABLE PIP_OUT
  120. ERROR_VARIABLE PIP_OUT
  121. )
  122. message(VERBOSE "Results from pip check: ${PIP_OUT}")
  123. if (NOT ${PIP_RESULT} EQUAL 0)
  124. message(WARNING "PIP reports unmet dependencies: ${PIP_OUT}")
  125. endif()
  126. if (REMOVE_USERSITE)
  127. unset(ENV{PYTHONNOUSERSITE})
  128. endif()
  129. endfunction()
  130. # allows you to install a folder into your site packages as an 'editable' package
  131. # meaning, it will show up in python but not actually be copied to your site-packages
  132. # folder, instead, it will be linked from there.
  133. # the pip_package_name should be the name given to the package in setup.py so that
  134. # any old versions may be uninstalled using setuptools before we install the new one.
  135. function(ly_pip_install_local_package_editable package_folder_path pip_package_name)
  136. set(stamp_file ${CMAKE_BINARY_DIR}/packages/pip_installs/${pip_package_name}.stamp)
  137. get_filename_component(stamp_file_directory ${stamp_file} DIRECTORY)
  138. file(MAKE_DIRECTORY ${stamp_file_directory})
  139. # for the first release of the o3de snap we will only use packages shipped with o3de
  140. if ($ENV{O3DE_SNAP})
  141. file(TOUCH ${stamp_file})
  142. endif()
  143. # we only ever need to do this once per runtime install, since its a link
  144. # not an actual install:
  145. # If setup.py changes we must reinstall the package in case its dependencies changed
  146. set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${package_folder_path}/setup.py)
  147. if(EXISTS ${stamp_file} AND ${stamp_file} IS_NEWER_THAN ${package_folder_path}/setup.py)
  148. ly_package_is_newer_than(${LY_PYTHON_PACKAGE_NAME} ${stamp_file} package_is_newer)
  149. if (NOT package_is_newer)
  150. # no need to run the command again, as the package is older than the stamp file
  151. # and the stamp file exists.
  152. return()
  153. endif()
  154. endif()
  155. message(CHECK_START "Python: linking ${package_folder_path} into python site-packages...")
  156. # dont allow or use installs in python %USER% location.
  157. if (NOT DEFINED ENV{PYTHONNOUSERSITE})
  158. set(REMOVE_USERSITE TRUE)
  159. endif()
  160. set(ENV{PYTHONNOUSERSITE} 1)
  161. # uninstall any old versions of this package first:
  162. execute_process(COMMAND
  163. ${LY_PYTHON_CMD} -m pip uninstall ${pip_package_name} -y --disable-pip-version-check
  164. WORKING_DIRECTORY ${Python_BINFOLDER}
  165. RESULT_VARIABLE PIP_RESULT
  166. OUTPUT_VARIABLE PIP_OUT
  167. ERROR_VARIABLE PIP_OUT
  168. )
  169. # we discard the error output of above, since it might not be installed, which is ok
  170. message(VERBOSE "pip uninstall result: ${PIP_RESULT}")
  171. message(VERBOSE "pip uninstall output: ${PIP_OUT}")
  172. # now install the new one:
  173. execute_process(COMMAND
  174. ${LY_PYTHON_CMD} -m pip install -e ${package_folder_path} --no-deps --disable-pip-version-check --no-warn-script-location
  175. WORKING_DIRECTORY ${Python_BINFOLDER}
  176. RESULT_VARIABLE PIP_RESULT
  177. OUTPUT_VARIABLE PIP_OUT
  178. ERROR_VARIABLE PIP_OUT
  179. )
  180. message(VERBOSE "pip install result: ${PIP_RESULT}")
  181. message(VERBOSE "pip install output: ${PIP_OUT}")
  182. if (NOT ${PIP_RESULT} EQUAL 0)
  183. message(CHECK_FAIL "Failed to install ${package_folder_path}: ${PIP_OUT} - use CMAKE_MESSAGE_LOG_LEVEL to VERBOSE for more information")
  184. message(FATAL_ERROR "Failure to install a python package will likely cause errors further down the line, stopping!")
  185. else()
  186. file(TOUCH ${stamp_file})
  187. endif()
  188. if (REMOVE_USERSITE)
  189. unset(ENV{PYTHONNOUSERSITE})
  190. endif()
  191. endfunction()
  192. # python is a special case of third party:
  193. # * We download it into a folder in the build tree
  194. # * The package is modified as time goes on (for example, PYC files appear)
  195. # * we add pip-packages to the site-packages folder
  196. # Because of this, we want a strict verification the first time we download it
  197. # But we don't want to full verify using hashes after we successfully get it the
  198. # first time.
  199. set(temp_LY_PACKAGE_VALIDATE_PACKAGE ${LY_PACKAGE_VALIDATE_PACKAGE})
  200. if (EXISTS ${LY_ROOT_FOLDER}/python/runtime/${LY_PYTHON_PACKAGE_NAME})
  201. # we will not validate the hash of every file, just that it is present
  202. # this is not just an optimization, see comment above.
  203. set(LY_PACKAGE_VALIDATE_PACKAGE FALSE)
  204. endif()
  205. ly_associate_package(PACKAGE_NAME ${LY_PYTHON_PACKAGE_NAME} TARGETS "Python" PACKAGE_HASH ${LY_PYTHON_PACKAGE_HASH})
  206. ly_set_package_download_location(${LY_PYTHON_PACKAGE_NAME} ${LY_ROOT_FOLDER}/python/runtime)
  207. ly_download_associated_package(Python)
  208. ly_set(LY_PACKAGE_VALIDATE_CONTENTS ${temp_LY_PACKAGE_VALIDATE_CONTENTS})
  209. ly_set(LY_PACKAGE_VALIDATE_PACKAGE ${temp_LY_PACKAGE_VALIDATE_PACKAGE})
  210. if (NOT CMAKE_SCRIPT_MODE_FILE)
  211. # if we're in script mode, we dont want to actually try to find package or anything else
  212. find_package(Python ${LY_PYTHON_VERSION} REQUIRED)
  213. # note - if you want to use a normal python via FindPython instead of the LY package above,
  214. # you may have to declare the below variables after find_package, as the project scripts are
  215. # looking for the below variables specifically.
  216. # verify the required variables are present:
  217. if (NOT Python_EXECUTABLE OR NOT Python_HOME OR NOT Python_PATHS)
  218. message(SEND_ERROR "Python installation not valid expected to find all of the following variables set:")
  219. message(STATUS " Python_EXECUTABLE: ${Python_EXECUTABLE}")
  220. message(STATUS " Python_HOME: ${Python_HOME}")
  221. message(STATUS " Python_PATHS: ${Python_PATHS}")
  222. else()
  223. message(STATUS "Using Python ${Python_VERSION} at ${Python_EXECUTABLE}")
  224. message(VERBOSE " Python_EXECUTABLE: ${Python_EXECUTABLE}")
  225. message(VERBOSE " Python_HOME: ${Python_HOME}")
  226. message(VERBOSE " Python_PATHS: ${Python_PATHS}")
  227. get_filename_component(Python_BINFOLDER ${Python_EXECUTABLE} DIRECTORY)
  228. # make sure other utils can find python / pip / etc
  229. # using find_program:
  230. LIST(APPEND CMAKE_PROGRAM_PATH "${Python_BINFOLDER}")
  231. # some platforms have this scripts dir, its harmless to add for those that do not.
  232. LIST(APPEND CMAKE_PROGRAM_PATH "${Python_BINFOLDER}/Scripts")
  233. # those using python should call it via LY_PYTHON_CMD - it adds the extra "-s" param
  234. # this param causes python to ignore the users profile folder which can have bogus
  235. # pip installation modules and lead to machine-specific config problems
  236. # it also uses our wrapper python, which can add additional paths to the python path
  237. if (${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Windows")
  238. set(LY_PYTHON_CMD "${LY_ROOT_FOLDER}/python/python.cmd" "-s")
  239. else()
  240. set(LY_PYTHON_CMD "${LY_ROOT_FOLDER}/python/python.sh" "-s")
  241. endif()
  242. update_pip_requirements(${LY_PYTHON_DEFAULT_REQUIREMENTS_TXT} default_requirements)
  243. # we also need to make sure any custom packages are installed.
  244. # this costs a moment of time though, so we'll only do it based on stamp files.
  245. if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND NOT INSTALLED_ENGINE)
  246. ly_pip_install_local_package_editable(${LY_ROOT_FOLDER}/Tools/LyTestTools ly-test-tools)
  247. ly_pip_install_local_package_editable(${LY_ROOT_FOLDER}/Tools/RemoteConsole/ly_remote_console ly-remote-console)
  248. ly_pip_install_local_package_editable(${LY_ROOT_FOLDER}/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools editor-python-test-tools)
  249. endif()
  250. ly_pip_install_local_package_editable(${LY_ROOT_FOLDER}/scripts/o3de o3de)
  251. endif()
  252. endif()