Skip to content

Commit 2ab9dcb

Browse files
authored
Create individual Python virtualenv's in cmake builds (#9662)
1 parent b58ae53 commit 2ab9dcb

File tree

4 files changed

+203
-6
lines changed

4 files changed

+203
-6
lines changed

Firestore/Protos/CMakeLists.txt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
include(FindPythonInterp)
15+
include(python_setup)
16+
FirebaseSetupPythonInterpreter(
17+
OUTVAR MY_PYTHON_EXECUTABLE
18+
KEY FirestoreProtos
19+
REQUIREMENTS six
20+
)
1621

1722
# Generate output in-place. So long as the build is idempotent this helps
1823
# verify that the protoc-generated output isn't changing.
@@ -200,7 +205,7 @@ if(FIREBASE_IOS_PROTOC_GENERATE_SOURCES)
200205
COMMENT "Generating nanopb sources"
201206
OUTPUT ${NANOPB_GENERATED_SOURCES}
202207
COMMAND
203-
${PYTHON_EXECUTABLE}
208+
${MY_PYTHON_EXECUTABLE}
204209
${CMAKE_CURRENT_SOURCE_DIR}/build_protos.py
205210
--nanopb
206211
--protoc=$<TARGET_FILE:protoc>
@@ -232,7 +237,7 @@ if(FIREBASE_IOS_PROTOC_GENERATE_SOURCES)
232237
COMMENT "Generating C++ protobuf sources"
233238
OUTPUT ${PROTOBUF_CPP_GENERATED_SOURCES}
234239
COMMAND
235-
${PYTHON_EXECUTABLE}
240+
${MY_PYTHON_EXECUTABLE}
236241
${CMAKE_CURRENT_SOURCE_DIR}/build_protos.py
237242
--cpp
238243
--protoc=$<TARGET_FILE:protoc>

Firestore/core/CMakeLists.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414

1515
include(CheckSymbolExists)
1616
include(CheckIncludeFiles)
17-
include(FindPythonInterp)
1817

18+
include(python_setup)
19+
FirebaseSetupPythonInterpreter(
20+
OUTVAR MY_PYTHON_EXECUTABLE
21+
KEY FirestoreCore
22+
)
1923

2024
## firestore_util
2125

@@ -286,7 +290,7 @@ add_custom_command(
286290
OUTPUT
287291
${GRPC_ROOT_CERTIFICATE_SOURCES}
288292
COMMAND
289-
${PYTHON_EXECUTABLE} ${FIREBASE_SOURCE_DIR}/scripts/binary_to_array.py
293+
${MY_PYTHON_EXECUTABLE} ${FIREBASE_SOURCE_DIR}/scripts/binary_to_array.py
290294
--output_header=${OUTPUT_DIR}/grpc_root_certificates_generated.h
291295
--output_source=${OUTPUT_DIR}/grpc_root_certificates_generated.cc
292296
--cpp_namespace=firebase::firestore::remote

cmake/external/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
cmake_minimum_required(VERSION 3.5.1)
1616
project(Firebase-download C CXX)
1717

18-
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR})
18+
list(
19+
APPEND
20+
CMAKE_MODULE_PATH
21+
${CMAKE_CURRENT_LIST_DIR}
22+
${CMAKE_CURRENT_LIST_DIR}/..
23+
)
1924

2025
set(
2126
FIREBASE_DOWNLOAD_DIR

cmake/python_setup.cmake

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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

Comments
 (0)