Skip to content

Commit 9534509

Browse files
committed
Dynamically create Python virtualenvs and install requirements into them
1 parent eb5272c commit 9534509

File tree

4 files changed

+200
-19
lines changed

4 files changed

+200
-19
lines changed

CMakeLists.txt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,6 @@ option(
7272
ON
7373
)
7474

75-
# Find a Python interpreter using the best available mechanism.
76-
if(${CMAKE_VERSION} VERSION_LESS "3.12")
77-
include(FindPythonInterp)
78-
set(DEFAULT_FIREBASE_PYTHON_EXECUTABLE "${PYTHON_EXECUTABLE}")
79-
else()
80-
find_package(Python3 COMPONENTS Interpreter)
81-
set(DEFAULT_FIREBASE_PYTHON_EXECUTABLE "${Python3_EXECUTABLE}")
82-
endif()
83-
84-
set(
85-
FIREBASE_PYTHON_EXECUTABLE
86-
"${DEFAULT_FIREBASE_PYTHON_EXECUTABLE}"
87-
CACHE FILEPATH
88-
"The Python interpreter to use"
89-
)
90-
9175
list(INSERT CMAKE_MODULE_PATH 0 ${PROJECT_SOURCE_DIR}/cmake)
9276
include(compiler_setup)
9377
include(sanitizer_options)

Firestore/Protos/CMakeLists.txt

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

15+
include(python_setup)
16+
FirebaseSetupPythonInterpreter(
17+
OUTVAR MY_PYTHON_EXECUTABLE
18+
KEY FirestoreProtos
19+
REQUIREMENTS six
20+
)
21+
1522
# Generate output in-place. So long as the build is idempotent this helps
1623
# verify that the protoc-generated output isn't changing.
1724
set(OUTPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR})
@@ -198,7 +205,7 @@ if(FIREBASE_IOS_PROTOC_GENERATE_SOURCES)
198205
COMMENT "Generating nanopb sources"
199206
OUTPUT ${NANOPB_GENERATED_SOURCES}
200207
COMMAND
201-
${FIREBASE_PYTHON_EXECUTABLE}
208+
${MY_PYTHON_EXECUTABLE}
202209
${CMAKE_CURRENT_SOURCE_DIR}/build_protos.py
203210
--nanopb
204211
--protoc=$<TARGET_FILE:protoc>
@@ -230,7 +237,7 @@ if(FIREBASE_IOS_PROTOC_GENERATE_SOURCES)
230237
COMMENT "Generating C++ protobuf sources"
231238
OUTPUT ${PROTOBUF_CPP_GENERATED_SOURCES}
232239
COMMAND
233-
${FIREBASE_PYTHON_EXECUTABLE}
240+
${MY_PYTHON_EXECUTABLE}
234241
${CMAKE_CURRENT_SOURCE_DIR}/build_protos.py
235242
--cpp
236243
--protoc=$<TARGET_FILE:protoc>

Firestore/core/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
include(CheckSymbolExists)
1616
include(CheckIncludeFiles)
1717

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

1924
## firestore_util
2025

@@ -285,7 +290,7 @@ add_custom_command(
285290
OUTPUT
286291
${GRPC_ROOT_CERTIFICATE_SOURCES}
287292
COMMAND
288-
${FIREBASE_PYTHON_EXECUTABLE} ${FIREBASE_SOURCE_DIR}/scripts/binary_to_array.py
293+
${MY_PYTHON_EXECUTABLE} ${FIREBASE_SOURCE_DIR}/scripts/binary_to_array.py
289294
--output_header=${OUTPUT_DIR}/grpc_root_certificates_generated.h
290295
--output_source=${OUTPUT_DIR}/grpc_root_certificates_generated.cc
291296
--cpp_namespace=firebase::firestore::remote

cmake/python_setup.cmake

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Copyright 2020 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 a Python interpreter, installing required dependencies.
16+
#
17+
# This function does the following:
18+
# 1. Uses the best-available built-in cmake mechanism to find a Python
19+
# interpreter (a.k.a. the "host" interpreter).
20+
# 2. Creates a Python virtualenv in the cmake binary directory using the
21+
# Python interpreter found in the previous step.
22+
# 3. Locates the Python interpreter in the virtualenv and sets its path in
23+
# the specified variable.
24+
# 4. Runs pip install to install the required dependencies 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.
29+
#
30+
# If any errors are detected (e.g. cannot install one of the given requirements)
31+
# then a fatal error is logged, causing the cmake processing to terminate.
32+
#
33+
# Arguments:
34+
# OUTVAR - The name of the variable into which to store the path of the
35+
# Python executable from the virtualenv.
36+
# KEY - A unique key to ensure isolation from other Python virtualenv
37+
# environments created by this function. This value will be used in the
38+
# path of the virtualenv on the file system and in the name of the cmake
39+
# cache variable that stores its path.
40+
# REQUIREMENTS - (Optional) A list of Python packages to install in the
41+
# virtualenv using pip.
42+
#
43+
# Example:
44+
# include(python_setup)
45+
# FirebaseSetupPythonInterpreter(
46+
# OUTVAR MY_PYTHON_EXECUTABLE
47+
# KEY ScanStuff
48+
# REQUIREMENTS six absl-py
49+
# )
50+
# execute_process(COMMAND "${MY_PYTHON_EXECUTABLE}" scan_stuff.py)
51+
function(FirebaseSetupPythonInterpreter)
52+
cmake_parse_arguments(
53+
PARSE_ARGV 0
54+
ARG
55+
"" # zero-value arguments
56+
"OUTVAR;KEY" # single-value arguments
57+
"REQUIREMENTS" # multi-value arguments
58+
)
59+
60+
# Validate this function's arguments.
61+
if("${ARG_OUTVAR}" STREQUAL "")
62+
message(FATAL_ERROR "OUTVAR must be specified to ${CMAKE_CURRENT_FUNCTION}")
63+
elseif("${ARG_KEY}" STREQUAL "")
64+
message(FATAL_ERROR "KEY must be specified to ${CMAKE_CURRENT_FUNCTION}")
65+
endif()
66+
67+
# Calculate the name of the cmake cache variable where to store the
68+
# Python interpreter path.
69+
set(CACHEVAR "FIREBASE_PYTHON_EXECUTABLE_${ARG_KEY}")
70+
71+
# Find a Python interpreter using the best available mechanism.
72+
if(${CMAKE_VERSION} VERSION_LESS "3.12")
73+
include(FindPythonInterp)
74+
set(DEFAULT_PYTHON_HOST_EXECUTABLE "${PYTHON_EXECUTABLE}")
75+
else()
76+
find_package(Python3 COMPONENTS Interpreter REQUIRED)
77+
set(DEFAULT_PYTHON_HOST_EXECUTABLE "${Python3_EXECUTABLE}")
78+
endif()
79+
80+
# Get the 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+
88+
# Create a virtualenv into which to install Python's dependencies.
89+
# https://docs.python.org/3/library/venv.html
90+
set(PYVENV_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/pyvenv/${ARG_KEY}")
91+
set(STAMP_FILE1 "${PYVENV_DIRECTORY}/cmake_firebase_python_stamp1.txt")
92+
set(STAMP_FILE2 "${PYVENV_DIRECTORY}/cmake_firebase_python_stamp2.txt")
93+
94+
# Check if the virtualenv is already up-to-date by examining the contents of
95+
# the 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+
if(EXISTS "${STAMP_FILE1}" AND EXISTS "${STAMP_FILE2}")
100+
file(READ "${STAMP_FILE1}" STAMP_FILE1_CONTENTS)
101+
file(READ "${STAMP_FILE2}" STAMP_FILE2_CONTENTS)
102+
if(
103+
("${STAMP_FILE1_CONTENTS}" STREQUAL "${FIREBASE_PYTHON_HOST_EXECUTABLE}")
104+
AND
105+
("${STAMP_FILE2_CONTENTS}" STREQUAL "${ARG_REQUIREMENTS}")
106+
)
107+
set("${ARG_OUTVAR}" "$CACHE{${CACHEVAR}}" PARENT_SCOPE)
108+
message(STATUS
109+
"${CMAKE_CURRENT_FUNCTION}: Using Python interpreter: $CACHE{${CACHEVAR}}"
110+
)
111+
return()
112+
endif()
113+
endif()
114+
115+
# Create the virtualenv.
116+
message(STATUS
117+
"${CMAKE_CURRENT_FUNCTION}: Creating Python virtualenv in "
118+
"${PYVENV_DIRECTORY} using ${FIREBASE_PYTHON_HOST_EXECUTABLE}"
119+
)
120+
file(REMOVE_RECURSE "${PYVENV_DIRECTORY}")
121+
execute_process(
122+
COMMAND
123+
"${FIREBASE_PYTHON_HOST_EXECUTABLE}"
124+
-m
125+
venv
126+
"${PYVENV_DIRECTORY}"
127+
--upgrade-deps
128+
RESULT_VARIABLE
129+
FIREBASE_PYVENV_CREATE_RESULT
130+
)
131+
if(NOT FIREBASE_PYVENV_CREATE_RESULT EQUAL 0)
132+
message(FATAL_ERROR
133+
"Failed to create a Python virtualenv in ${PYVENV_DIRECTORY} "
134+
"using ${FIREBASE_PYTHON_HOST_EXECUTABLE}")
135+
endif()
136+
137+
# Find the Python interpreter in the virtualenv.
138+
find_program(
139+
"${CACHEVAR}"
140+
DOC "The Python interpreter to use for ${ARG_KEY}"
141+
NAMES python3 python
142+
PATHS "${PYVENV_DIRECTORY}"
143+
PATH_SUFFIXES bin Scripts
144+
NO_DEFAULT_PATH
145+
)
146+
if(NOT ${CACHEVAR})
147+
message(FATAL_ERROR "Unable to find Python executable in ${PYVENV_DIRECTORY}")
148+
else()
149+
set(PYTHON_EXECUTABLE "$CACHE{${CACHEVAR}}")
150+
message(STATUS
151+
"${CMAKE_CURRENT_FUNCTION}: Found Python executable in virtualenv: "
152+
"${PYTHON_EXECUTABLE}"
153+
)
154+
endif()
155+
156+
# Install the dependencies in the virtualenv, if any are requested.
157+
if(NOT ("${ARG_REQUIREMENTS}" STREQUAL ""))
158+
message(STATUS
159+
"${CMAKE_CURRENT_FUNCTION}: Installing Python dependencies into "
160+
"${PYVENV_DIRECTORY}: ${ARG_REQUIREMENTS}"
161+
)
162+
execute_process(
163+
COMMAND
164+
"${PYTHON_EXECUTABLE}"
165+
-m
166+
pip
167+
install
168+
${ARG_REQUIREMENTS}
169+
RESULT_VARIABLE
170+
PIP_INSTALL_RESULT
171+
)
172+
if(NOT PIP_INSTALL_RESULT EQUAL 0)
173+
message(FATAL_ERROR
174+
"Failed to install Python dependencies into "
175+
"${PYVENV_DIRECTORY}: ${ARG_REQUIREMENTS}"
176+
)
177+
endif()
178+
endif()
179+
180+
# Write the stamp files.
181+
file(WRITE "${STAMP_FILE1}" "${FIREBASE_PYTHON_HOST_EXECUTABLE}")
182+
file(WRITE "${STAMP_FILE2}" "${ARG_REQUIREMENTS}")
183+
184+
set("${ARG_OUTVAR}" "${PYTHON_EXECUTABLE}" PARENT_SCOPE)
185+
endfunction(FirebaseSetupPythonInterpreter)

0 commit comments

Comments
 (0)