python_setup.cmake 6.3 KB

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