|
| 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 | + |
| 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 | + |
| 64 | + # Validate this function's arguments. |
| 65 | + if("${ARG_OUTVAR}" STREQUAL "") |
| 66 | + message(FATAL_ERROR "OUTVAR must be specified to ${CMAKE_CURRENT_FUNCTION}") |
| 67 | + elseif("${ARG_KEY}" STREQUAL "") |
| 68 | + message(FATAL_ERROR "KEY must be specified to ${CMAKE_CURRENT_FUNCTION}") |
| 69 | + endif() |
| 70 | + |
| 71 | + # Calculate the name of the cmake *cache* variable into which to store the |
| 72 | + # path of the Python interpreter from the virtualenv. |
| 73 | + set(CACHEVAR "FIREBASE_PYTHON_EXECUTABLE_${ARG_KEY}") |
| 74 | + |
| 75 | + set(LOG_PREFIX "${CMAKE_CURRENT_FUNCTION}(${ARG_KEY})") |
| 76 | + |
| 77 | + # Find a "host" Python interpreter using the best available mechanism. |
| 78 | + if(${CMAKE_VERSION} VERSION_LESS "3.12") |
| 79 | + include(FindPythonInterp) |
| 80 | + set(DEFAULT_PYTHON_HOST_EXECUTABLE "${PYTHON_EXECUTABLE}") |
| 81 | + else() |
| 82 | + find_package(Python3 COMPONENTS Interpreter REQUIRED) |
| 83 | + set(DEFAULT_PYTHON_HOST_EXECUTABLE "${Python3_EXECUTABLE}") |
| 84 | + endif() |
| 85 | + |
| 86 | + # Get the host Python interpreter on the host system to use. |
| 87 | + set( |
| 88 | + FIREBASE_PYTHON_HOST_EXECUTABLE |
| 89 | + "${DEFAULT_PYTHON_HOST_EXECUTABLE}" |
| 90 | + CACHE FILEPATH |
| 91 | + "The Python interpreter on the host system to use" |
| 92 | + ) |
| 93 | + |
| 94 | + # Check if the virtualenv is already up-to-date by examining the contents of |
| 95 | + # its stamp files. The stamp files store the path of the host Python |
| 96 | + # interpreter and the dependencies that were installed by pip. If both of |
| 97 | + # these files exist and contain the same Python interpreter and dependencies |
| 98 | + # then just re-use the virtualenv; otherwise, re-create it. |
| 99 | + set(PYVENV_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/pyvenv/${ARG_KEY}") |
| 100 | + set(STAMP_FILE1 "${PYVENV_DIRECTORY}/cmake_firebase_python_stamp1.txt") |
| 101 | + set(STAMP_FILE2 "${PYVENV_DIRECTORY}/cmake_firebase_python_stamp2.txt") |
| 102 | + |
| 103 | + if(EXISTS "${STAMP_FILE1}" AND EXISTS "${STAMP_FILE2}") |
| 104 | + file(READ "${STAMP_FILE1}" STAMP_FILE1_CONTENTS) |
| 105 | + file(READ "${STAMP_FILE2}" STAMP_FILE2_CONTENTS) |
| 106 | + if( |
| 107 | + ("${STAMP_FILE1_CONTENTS}" STREQUAL "${FIREBASE_PYTHON_HOST_EXECUTABLE}") |
| 108 | + AND |
| 109 | + ("${STAMP_FILE2_CONTENTS}" STREQUAL "${ARG_REQUIREMENTS}") |
| 110 | + ) |
| 111 | + set("${ARG_OUTVAR}" "$CACHE{${CACHEVAR}}" PARENT_SCOPE) |
| 112 | + message(STATUS "${LOG_PREFIX}: Using Python interpreter: $CACHE{${CACHEVAR}}") |
| 113 | + return() |
| 114 | + endif() |
| 115 | + endif() |
| 116 | + |
| 117 | + # Create the virtualenv. |
| 118 | + message(STATUS |
| 119 | + "${LOG_PREFIX}: Creating Python virtualenv in ${PYVENV_DIRECTORY} " |
| 120 | + "using ${FIREBASE_PYTHON_HOST_EXECUTABLE}" |
| 121 | + ) |
| 122 | + file(REMOVE_RECURSE "${PYVENV_DIRECTORY}") |
| 123 | + execute_process( |
| 124 | + COMMAND |
| 125 | + "${FIREBASE_PYTHON_HOST_EXECUTABLE}" |
| 126 | + -m |
| 127 | + venv |
| 128 | + "${PYVENV_DIRECTORY}" |
| 129 | + RESULT_VARIABLE |
| 130 | + FIREBASE_PYVENV_CREATE_RESULT |
| 131 | + ) |
| 132 | + if(NOT FIREBASE_PYVENV_CREATE_RESULT EQUAL 0) |
| 133 | + message(FATAL_ERROR |
| 134 | + "Failed to create a Python virtualenv in ${PYVENV_DIRECTORY} " |
| 135 | + "using ${FIREBASE_PYTHON_HOST_EXECUTABLE}") |
| 136 | + endif() |
| 137 | + |
| 138 | + # Find the Python interpreter in the virtualenv. |
| 139 | + find_program( |
| 140 | + "${CACHEVAR}" |
| 141 | + DOC "The Python interpreter to use for ${ARG_KEY}" |
| 142 | + NAMES python3 python |
| 143 | + PATHS "${PYVENV_DIRECTORY}" |
| 144 | + PATH_SUFFIXES bin Scripts |
| 145 | + NO_DEFAULT_PATH |
| 146 | + ) |
| 147 | + if(NOT ${CACHEVAR}) |
| 148 | + message(FATAL_ERROR "Unable to find Python executable in ${PYVENV_DIRECTORY}") |
| 149 | + else() |
| 150 | + set(PYTHON_EXECUTABLE "$CACHE{${CACHEVAR}}") |
| 151 | + message(STATUS "${LOG_PREFIX}: Found Python executable in virtualenv: ${PYTHON_EXECUTABLE}") |
| 152 | + endif() |
| 153 | + |
| 154 | + # Install the dependencies in the virtualenv, if any are requested. |
| 155 | + if(NOT ("${ARG_REQUIREMENTS}" STREQUAL "")) |
| 156 | + message(STATUS |
| 157 | + "${LOG_PREFIX}: Installing Python dependencies into " |
| 158 | + "${PYVENV_DIRECTORY}: ${ARG_REQUIREMENTS}" |
| 159 | + ) |
| 160 | + execute_process( |
| 161 | + COMMAND |
| 162 | + "${PYTHON_EXECUTABLE}" |
| 163 | + -m |
| 164 | + pip |
| 165 | + install |
| 166 | + ${ARG_REQUIREMENTS} |
| 167 | + RESULT_VARIABLE |
| 168 | + PIP_INSTALL_RESULT |
| 169 | + ) |
| 170 | + if(NOT PIP_INSTALL_RESULT EQUAL 0) |
| 171 | + message(FATAL_ERROR |
| 172 | + "Failed to install Python dependencies into " |
| 173 | + "${PYVENV_DIRECTORY}: ${ARG_REQUIREMENTS}" |
| 174 | + ) |
| 175 | + endif() |
| 176 | + endif() |
| 177 | + |
| 178 | + # Write the stamp files. |
| 179 | + file(WRITE "${STAMP_FILE1}" "${FIREBASE_PYTHON_HOST_EXECUTABLE}") |
| 180 | + file(WRITE "${STAMP_FILE2}" "${ARG_REQUIREMENTS}") |
| 181 | + |
| 182 | + set("${ARG_OUTVAR}" "${PYTHON_EXECUTABLE}" PARENT_SCOPE) |
| 183 | +endfunction(FirebaseSetupPythonInterpreter) |
0 commit comments