python_setup.cmake 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. # Copyright 2022 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. # Sets up an isolated Python interpreter, installing required dependencies.
  15. #
  16. # This function does the following:
  17. # 1. Finds a Python interpreter using the best-available built-in cmake
  18. # mechanism do do so. This is referred to as the "host" interpreter.
  19. # 2. Creates a Python virtualenv in the cmake binary directory using the
  20. # host Python interpreter found in the previous step.
  21. # 3. Locates the Python interpreter in the virtualenv and sets its path in
  22. # the specified OUTVAR variable.
  23. # 4. Runs `pip install` to install the specified required dependencies, if any,
  24. # in the virtualenv.
  25. #
  26. # This function also writes "stamp files" into the virtualenv. These files
  27. # are used to determine if the virtualenv is up-to-date from a previous cmake
  28. # run or if it needs to be recreated from scratch. It will simply be re-used if
  29. # possible.
  30. #
  31. # If any errors occur (e.g. cannot install one of the given requirements) then a
  32. # fatal error is logged, causing the cmake processing to terminate.
  33. #
  34. # See https://docs.python.org/3/library/venv.html for details about virtualenv.
  35. #
  36. # Arguments:
  37. # OUTVAR - The name of the variable into which to store the path of the
  38. # Python executable from the virtualenv.
  39. # KEY - A unique key to ensure isolation from other Python virtualenv
  40. # environments created by this function. This value will be incorporated
  41. # into the path of the virtualenv and incorporated into the name of the
  42. # cmake cache variable that stores its path.
  43. # REQUIREMENTS - (Optional) A list of Python packages to install in the
  44. # virtualenv. These will be given as arguments to `pip install`.
  45. #
  46. # Example:
  47. # include(python_setup)
  48. # FirebaseSetupPythonInterpreter(
  49. # OUTVAR MY_PYTHON_EXECUTABLE
  50. # KEY ScanStuff
  51. # REQUIREMENTS six absl-py
  52. # )
  53. # execute_process(COMMAND "${MY_PYTHON_EXECUTABLE}" scan_stuff.py)
  54. function(FirebaseSetupPythonInterpreter)
  55. cmake_parse_arguments(
  56. PARSE_ARGV 0
  57. ARG
  58. "" # zero-value arguments
  59. "OUTVAR;KEY" # single-value arguments
  60. "REQUIREMENTS" # multi-value arguments
  61. )
  62. # Validate this function's arguments.
  63. if("${ARG_OUTVAR}" STREQUAL "")
  64. message(FATAL_ERROR "OUTVAR must be specified to ${CMAKE_CURRENT_FUNCTION}")
  65. elseif("${ARG_KEY}" STREQUAL "")
  66. message(FATAL_ERROR "KEY must be specified to ${CMAKE_CURRENT_FUNCTION}")
  67. endif()
  68. # Calculate the name of the cmake *cache* variable into which to store the
  69. # path of the Python interpreter from the virtualenv.
  70. set(CACHEVAR "FIREBASE_PYTHON_EXECUTABLE_${ARG_KEY}")
  71. set(LOG_PREFIX "${CMAKE_CURRENT_FUNCTION}(${ARG_KEY})")
  72. # Find a "host" Python interpreter using the best available mechanism.
  73. if(${CMAKE_VERSION} VERSION_LESS "3.12")
  74. include(FindPythonInterp)
  75. set(DEFAULT_PYTHON_HOST_EXECUTABLE "${PYTHON_EXECUTABLE}")
  76. else()
  77. find_package(Python3 COMPONENTS Interpreter REQUIRED)
  78. set(DEFAULT_PYTHON_HOST_EXECUTABLE "${Python3_EXECUTABLE}")
  79. endif()
  80. # Get the host Python interpreter on the host system to use.
  81. set(
  82. FIREBASE_PYTHON_HOST_EXECUTABLE
  83. "${DEFAULT_PYTHON_HOST_EXECUTABLE}"
  84. CACHE FILEPATH
  85. "The Python interpreter on the host system to use"
  86. )
  87. # Check if the virtualenv is already up-to-date by examining the contents of
  88. # its stamp files. The stamp files store the path of the host Python
  89. # interpreter and the dependencies that were installed by pip. If both of
  90. # these files exist and contain the same Python interpreter and dependencies
  91. # then just re-use the virtualenv; otherwise, re-create it.
  92. set(PYVENV_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/pyvenv/${ARG_KEY}")
  93. set(STAMP_FILE1 "${PYVENV_DIRECTORY}/cmake_firebase_python_stamp1.txt")
  94. set(STAMP_FILE2 "${PYVENV_DIRECTORY}/cmake_firebase_python_stamp2.txt")
  95. if(EXISTS "${STAMP_FILE1}" AND EXISTS "${STAMP_FILE2}")
  96. file(READ "${STAMP_FILE1}" STAMP_FILE1_CONTENTS)
  97. file(READ "${STAMP_FILE2}" STAMP_FILE2_CONTENTS)
  98. if(
  99. ("${STAMP_FILE1_CONTENTS}" STREQUAL "${FIREBASE_PYTHON_HOST_EXECUTABLE}")
  100. AND
  101. ("${STAMP_FILE2_CONTENTS}" STREQUAL "${ARG_REQUIREMENTS}")
  102. )
  103. set("${ARG_OUTVAR}" "$CACHE{${CACHEVAR}}" PARENT_SCOPE)
  104. message(STATUS "${LOG_PREFIX}: Using Python interpreter: $CACHE{${CACHEVAR}}")
  105. return()
  106. endif()
  107. endif()
  108. # Create the virtualenv.
  109. message(STATUS
  110. "${LOG_PREFIX}: Creating Python virtualenv in ${PYVENV_DIRECTORY} "
  111. "using ${FIREBASE_PYTHON_HOST_EXECUTABLE}"
  112. )
  113. file(REMOVE_RECURSE "${PYVENV_DIRECTORY}")
  114. execute_process(
  115. COMMAND
  116. "${FIREBASE_PYTHON_HOST_EXECUTABLE}"
  117. -m
  118. venv
  119. "${PYVENV_DIRECTORY}"
  120. RESULT_VARIABLE
  121. FIREBASE_PYVENV_CREATE_RESULT
  122. )
  123. if(NOT FIREBASE_PYVENV_CREATE_RESULT EQUAL 0)
  124. message(FATAL_ERROR
  125. "Failed to create a Python virtualenv in ${PYVENV_DIRECTORY} "
  126. "using ${FIREBASE_PYTHON_HOST_EXECUTABLE}")
  127. endif()
  128. # Find the Python interpreter in the virtualenv.
  129. find_program(
  130. "${CACHEVAR}"
  131. DOC "The Python interpreter to use for ${ARG_KEY}"
  132. NAMES python3 python
  133. PATHS "${PYVENV_DIRECTORY}"
  134. PATH_SUFFIXES bin Scripts
  135. NO_DEFAULT_PATH
  136. )
  137. if(NOT ${CACHEVAR})
  138. message(FATAL_ERROR "Unable to find Python executable in ${PYVENV_DIRECTORY}")
  139. else()
  140. set(PYTHON_EXECUTABLE "$CACHE{${CACHEVAR}}")
  141. message(STATUS "${LOG_PREFIX}: Found Python executable in virtualenv: ${PYTHON_EXECUTABLE}")
  142. endif()
  143. # Install the dependencies in the virtualenv, if any are requested.
  144. if(NOT ("${ARG_REQUIREMENTS}" STREQUAL ""))
  145. message(STATUS
  146. "${LOG_PREFIX}: Installing Python dependencies into "
  147. "${PYVENV_DIRECTORY}: ${ARG_REQUIREMENTS}"
  148. )
  149. execute_process(
  150. COMMAND
  151. "${PYTHON_EXECUTABLE}"
  152. -m
  153. pip
  154. install
  155. ${ARG_REQUIREMENTS}
  156. RESULT_VARIABLE
  157. PIP_INSTALL_RESULT
  158. )
  159. if(NOT PIP_INSTALL_RESULT EQUAL 0)
  160. message(FATAL_ERROR
  161. "Failed to install Python dependencies into "
  162. "${PYVENV_DIRECTORY}: ${ARG_REQUIREMENTS}"
  163. )
  164. endif()
  165. endif()
  166. # Write the stamp files.
  167. file(WRITE "${STAMP_FILE1}" "${FIREBASE_PYTHON_HOST_EXECUTABLE}")
  168. file(WRITE "${STAMP_FILE2}" "${ARG_REQUIREMENTS}")
  169. set("${ARG_OUTVAR}" "${PYTHON_EXECUTABLE}" PARENT_SCOPE)
  170. endfunction(FirebaseSetupPythonInterpreter)