# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Sets up an isolated Python interpreter, installing required dependencies. # # This function does the following: # 1. Finds a Python interpreter using the best-available built-in cmake # mechanism do do so. This is referred to as the "host" interpreter. # 2. Creates a Python virtualenv in the cmake binary directory using the # host Python interpreter found in the previous step. # 3. Locates the Python interpreter in the virtualenv and sets its path in # the specified OUTVAR variable. # 4. Runs `pip install` to install the specified required dependencies, if any, # in the virtualenv. # # This function also writes "stamp files" into the virtualenv. These files # are used to determine if the virtualenv is up-to-date from a previous cmake # run or if it needs to be recreated from scratch. It will simply be re-used if # possible. # # If any errors occur (e.g. cannot install one of the given requirements) then a # fatal error is logged, causing the cmake processing to terminate. # # See https://docs.python.org/3/library/venv.html for details about virtualenv. # # Arguments: # OUTVAR - The name of the variable into which to store the path of the # Python executable from the virtualenv. # KEY - A unique key to ensure isolation from other Python virtualenv # environments created by this function. This value will be incorporated # into the path of the virtualenv and incorporated into the name of the # cmake cache variable that stores its path. # REQUIREMENTS - (Optional) A list of Python packages to install in the # virtualenv. These will be given as arguments to `pip install`. # # Example: # include(python_setup) # FirebaseSetupPythonInterpreter( # OUTVAR MY_PYTHON_EXECUTABLE # KEY ScanStuff # REQUIREMENTS six absl-py # ) # execute_process(COMMAND "${MY_PYTHON_EXECUTABLE}" scan_stuff.py) function(FirebaseSetupPythonInterpreter) cmake_parse_arguments( PARSE_ARGV 0 ARG "" # zero-value arguments "OUTVAR;KEY" # single-value arguments "REQUIREMENTS" # multi-value arguments ) # Validate this function's arguments. if("${ARG_OUTVAR}" STREQUAL "") message(FATAL_ERROR "OUTVAR must be specified to ${CMAKE_CURRENT_FUNCTION}") elseif("${ARG_KEY}" STREQUAL "") message(FATAL_ERROR "KEY must be specified to ${CMAKE_CURRENT_FUNCTION}") endif() # Calculate the name of the cmake *cache* variable into which to store the # path of the Python interpreter from the virtualenv. set(CACHEVAR "FIREBASE_PYTHON_EXECUTABLE_${ARG_KEY}") set(LOG_PREFIX "${CMAKE_CURRENT_FUNCTION}(${ARG_KEY})") # Find a "host" Python interpreter using the best available mechanism. if(${CMAKE_VERSION} VERSION_LESS "3.12") include(FindPythonInterp) set(DEFAULT_PYTHON_HOST_EXECUTABLE "${PYTHON_EXECUTABLE}") else() find_package(Python3 COMPONENTS Interpreter REQUIRED) set(DEFAULT_PYTHON_HOST_EXECUTABLE "${Python3_EXECUTABLE}") endif() # Get the host Python interpreter on the host system to use. set( FIREBASE_PYTHON_HOST_EXECUTABLE "${DEFAULT_PYTHON_HOST_EXECUTABLE}" CACHE FILEPATH "The Python interpreter on the host system to use" ) # Check if the virtualenv is already up-to-date by examining the contents of # its stamp files. The stamp files store the path of the host Python # interpreter and the dependencies that were installed by pip. If both of # these files exist and contain the same Python interpreter and dependencies # then just re-use the virtualenv; otherwise, re-create it. set(PYVENV_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/pyvenv/${ARG_KEY}") set(STAMP_FILE1 "${PYVENV_DIRECTORY}/cmake_firebase_python_stamp1.txt") set(STAMP_FILE2 "${PYVENV_DIRECTORY}/cmake_firebase_python_stamp2.txt") if(EXISTS "${STAMP_FILE1}" AND EXISTS "${STAMP_FILE2}") file(READ "${STAMP_FILE1}" STAMP_FILE1_CONTENTS) file(READ "${STAMP_FILE2}" STAMP_FILE2_CONTENTS) if( ("${STAMP_FILE1_CONTENTS}" STREQUAL "${FIREBASE_PYTHON_HOST_EXECUTABLE}") AND ("${STAMP_FILE2_CONTENTS}" STREQUAL "${ARG_REQUIREMENTS}") ) set("${ARG_OUTVAR}" "$CACHE{${CACHEVAR}}" PARENT_SCOPE) message(STATUS "${LOG_PREFIX}: Using Python interpreter: $CACHE{${CACHEVAR}}") return() endif() endif() # Create the virtualenv. message(STATUS "${LOG_PREFIX}: Creating Python virtualenv in ${PYVENV_DIRECTORY} " "using ${FIREBASE_PYTHON_HOST_EXECUTABLE}" ) file(REMOVE_RECURSE "${PYVENV_DIRECTORY}") execute_process( COMMAND "${FIREBASE_PYTHON_HOST_EXECUTABLE}" -m venv "${PYVENV_DIRECTORY}" RESULT_VARIABLE FIREBASE_PYVENV_CREATE_RESULT ) if(NOT FIREBASE_PYVENV_CREATE_RESULT EQUAL 0) message(FATAL_ERROR "Failed to create a Python virtualenv in ${PYVENV_DIRECTORY} " "using ${FIREBASE_PYTHON_HOST_EXECUTABLE}") endif() # Find the Python interpreter in the virtualenv. find_program( "${CACHEVAR}" DOC "The Python interpreter to use for ${ARG_KEY}" NAMES python3 python PATHS "${PYVENV_DIRECTORY}" PATH_SUFFIXES bin Scripts NO_DEFAULT_PATH ) if(NOT ${CACHEVAR}) message(FATAL_ERROR "Unable to find Python executable in ${PYVENV_DIRECTORY}") else() set(PYTHON_EXECUTABLE "$CACHE{${CACHEVAR}}") message(STATUS "${LOG_PREFIX}: Found Python executable in virtualenv: ${PYTHON_EXECUTABLE}") endif() # Install the dependencies in the virtualenv, if any are requested. if(NOT ("${ARG_REQUIREMENTS}" STREQUAL "")) message(STATUS "${LOG_PREFIX}: Installing Python dependencies into " "${PYVENV_DIRECTORY}: ${ARG_REQUIREMENTS}" ) execute_process( COMMAND "${PYTHON_EXECUTABLE}" -m pip install ${ARG_REQUIREMENTS} RESULT_VARIABLE PIP_INSTALL_RESULT ) if(NOT PIP_INSTALL_RESULT EQUAL 0) message(FATAL_ERROR "Failed to install Python dependencies into " "${PYVENV_DIRECTORY}: ${ARG_REQUIREMENTS}" ) endif() endif() # Write the stamp files. file(WRITE "${STAMP_FILE1}" "${FIREBASE_PYTHON_HOST_EXECUTABLE}") file(WRITE "${STAMP_FILE2}" "${ARG_REQUIREMENTS}") set("${ARG_OUTVAR}" "${PYTHON_EXECUTABLE}" PARENT_SCOPE) endfunction(FirebaseSetupPythonInterpreter)