blob: 6a5ae770c68ddc77ea76412c18f2943fffb9d7f1 [file] [log] [blame]
# Copyright 2022 The Pigweed Authors
#
# 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
#
# https://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.
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/python_action.gni")
# Defines and creates a Python virtualenv. This template is used by Pigweed in
# https://cs.pigweed.dev/pigweed/+/main:pw_env_setup/BUILD.gn to
# create a virtualenv for use within the GN build that all Python actions will
# run in.
#
# Example:
#
# pw_python_venv("test_venv") {
# path = "test-venv"
# constraints = [ "//tools/constraints.list" ]
# requirements = [ "//tools/requirements.txt" ]
# source_packages = [
# "$dir_pw_cli/py",
# "$dir_pw_console/py",
# "//tools:another_pw_python_package",
# ]
# }
#
# Args:
# path: The directory where the virtualenv will be created. This is relative
# to the GN root and must begin with "$root_build_dir/" if it lives in the
# output directory or "//" if it lives in elsewhere.
#
# constraints: A list of constraint files used when performing pip install
# into this virtualenv. By default this is set to pw_build_PIP_CONSTRAINTS
#
# requirements: A list of requirements files to install into this virtualenv
# on creation. By default this is set to pw_build_PIP_REQUIREMENTS
#
# pip_generate_hashes: (Default: false) Use --generate-hashes When
# running pip-compile to compute the final requirements.txt
#
# source_packages: A list of in-tree pw_python_package targets that will be
# checked for external third_party pip dependencies to install into this
# virtualenv. Note this list of targets isn't actually installed into the
# virtualenv. Only packages defined inside the [options] install_requires
# section of each pw_python_package's setup.cfg will be pip installed. See
# this page for a setup.cfg example:
# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
#
# output_logs: (Default: true) Commands will output logs.
#
template("pw_python_venv") {
assert(defined(invoker.path), "pw_python_venv requires a 'path'")
_path = invoker.path
_generated_requirements_file =
"$target_gen_dir/$target_name/generated_requirements.txt"
_compiled_requirements_file =
"$target_gen_dir/$target_name/compiled_requirements.txt"
_source_packages = []
if (defined(invoker.source_packages)) {
_source_packages += invoker.source_packages
} else {
not_needed([
"_source_packages",
"_generated_requirements_file",
])
}
_output_logs = true
if (defined(invoker.output_logs)) {
_output_logs = invoker.output_logs
}
if (!defined(invoker.output_logs) ||
current_toolchain != pw_build_PYTHON_TOOLCHAIN) {
not_needed([ "_output_logs" ])
}
_source_package_labels = []
foreach(pkg, _source_packages) {
_source_package_labels += [ get_label_info(pkg, "label_no_toolchain") ]
}
if (defined(invoker.requirements)) {
_requirements = invoker.requirements
} else {
_requirements = pw_build_PIP_REQUIREMENTS
}
if (defined(invoker.constraints)) {
_constraints = invoker.constraints
} else {
_constraints = pw_build_PIP_CONSTRAINTS
}
_python_interpreter = _path + "/bin/python"
if (host_os == "win") {
_python_interpreter = _path + "/Scripts/python.exe"
}
_venv_metadata_json_file = "$target_gen_dir/$target_name/venv_metadata.json"
_venv_metadata = {
gn_target_name =
get_label_info(":${invoker.target_name}", "label_no_toolchain")
path = rebase_path(_path, root_build_dir)
generated_requirements =
rebase_path(_generated_requirements_file, root_build_dir)
compiled_requirements =
rebase_path(_compiled_requirements_file, root_build_dir)
requirements = rebase_path(_requirements, root_build_dir)
constraints = rebase_path(_constraints, root_build_dir)
interpreter = rebase_path(_python_interpreter, root_build_dir)
source_packages = _source_package_labels
}
write_file(_venv_metadata_json_file, _venv_metadata, "json")
pw_python_action("${target_name}._create_virtualenv") {
_pw_internal_run_in_venv = false
# Note: The if the venv isn't in the out dir then we can't declare
# outputs and must stamp instead.
stamp = true
# The virtualenv should depend on the version of Python currently in use.
stampfile = "$target_gen_dir/$target_name.pw_pystamp"
depfile = "$target_gen_dir/$target_name.d"
script = "$dir_pw_build/py/pw_build/create_gn_venv.py"
args = [
"--depfile",
rebase_path(depfile, root_build_dir),
"--destination-dir",
rebase_path(_path, root_build_dir),
"--stampfile",
rebase_path(stampfile, root_build_dir),
]
}
if (defined(invoker.source_packages) &&
current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
pw_python_action("${target_name}._generate_3p_requirements") {
inputs = _requirements + _constraints
_pw_internal_run_in_venv = false
_forward_python_metadata_deps = true
script = "$dir_pw_build/py/pw_build/generate_python_requirements.py"
_pkg_gn_labels = []
foreach(pkg, _source_packages) {
_pkg_gn_labels += [ get_label_info(pkg, "label_no_toolchain") +
"($pw_build_PYTHON_TOOLCHAIN)" ]
}
# pw_build/py is always needed for venv creation and Python lint checks.
python_metadata_deps =
[ get_label_info("$dir_pw_build/py", "label_no_toolchain") +
"($pw_build_PYTHON_TOOLCHAIN)" ]
python_metadata_deps += _pkg_gn_labels
args = [
"--gn-root-build-dir",
rebase_path(root_build_dir, root_build_dir),
"--output-requirement-file",
rebase_path(_generated_requirements_file, root_build_dir),
]
if (_constraints != []) {
args += [ "--constraint-files" ]
}
foreach(_constraints_file, _constraints) {
args += [ rebase_path(_constraints_file, root_build_dir) ]
}
args += [
"--gn-packages",
string_join(",", _pkg_gn_labels),
]
outputs = [ _generated_requirements_file ]
deps = [ ":${invoker.target_name}._create_virtualenv($pw_build_PYTHON_TOOLCHAIN)" ]
}
} else {
group("${target_name}._generate_3p_requirements") {
}
}
_pip_generate_hashes = false
if (defined(invoker.pip_generate_hashes)) {
_pip_generate_hashes = invoker.pip_generate_hashes
} else {
not_needed([ "_pip_generate_hashes" ])
}
if (defined(invoker.source_packages) || defined(invoker.requirements)) {
if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
# Compile requirements with hashes
pw_python_action("${target_name}._compile_requirements") {
module = "piptools"
# Set the venv to run this pip install in.
_pw_internal_run_in_venv = true
_skip_installing_external_python_deps = true
venv = get_label_info(":${invoker.target_name}", "label_no_toolchain")
_pip_args = []
if (pw_build_PYTHON_PIP_INSTALL_OFFLINE) {
_pip_args += [ "--no-index" ]
}
if (pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE) {
_pip_args += [ "--no-cache-dir" ]
}
if (pw_build_PYTHON_PIP_INSTALL_FIND_LINKS != []) {
foreach(link_dir, pw_build_PYTHON_PIP_INSTALL_FIND_LINKS) {
_pip_args +=
[ "--find-links=" + rebase_path(link_dir, root_build_dir) ]
}
}
args = [ "compile" ]
if (_pip_generate_hashes) {
args += [
"--generate-hashes",
"--reuse-hashes",
]
}
args += [
"--resolver=backtracking",
# --allow-unsafe will force pinning pip and setuptools.
"--allow-unsafe",
"--output-file",
rebase_path(_compiled_requirements_file, root_build_dir),
# Input requirements file:
rebase_path(_generated_requirements_file, root_build_dir),
]
# Pass offline related pip args through the pip-compile command.
if (_pip_args != []) {
args += [
"--pip-args",
string_join(" ", _pip_args),
]
}
# Extra requirements files
foreach(_requirements_file, _requirements) {
args += [ rebase_path(_requirements_file, root_build_dir) ]
}
inputs = []
# NOTE: constraint files are included in the content of the
# _generated_requirements_file. This occurs in the
# ._generate_3p_requirements target.
inputs += _constraints
inputs += _requirements
inputs += [ _generated_requirements_file ]
deps = [
":${invoker.target_name}._generate_3p_requirements($pw_build_PYTHON_TOOLCHAIN)",
":${invoker.target_name}._install_base_3p_deps($pw_build_PYTHON_TOOLCHAIN)",
]
outputs = [ _compiled_requirements_file ]
}
# This target will run 'pip install' in the venv to provide base
# dependencies needed to run piptools commands. That is required for the
# _compile_requirements sub target.
pw_python_action("${target_name}._install_base_3p_deps") {
module = "pip"
# Set the venv to run this pip install in.
_pw_internal_run_in_venv = true
_skip_installing_external_python_deps = true
venv = get_label_info(":${invoker.target_name}", "label_no_toolchain")
_base_requirement_file = "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/python_base_requirements.txt"
args = [
"install",
"--requirement",
rebase_path(_base_requirement_file, root_build_dir),
]
if (_output_logs) {
_pip_install_log_file =
"$target_gen_dir/$target_name/pip_install_log.txt"
args += [
"--log",
rebase_path(_pip_install_log_file, root_build_dir),
]
outputs = [ _pip_install_log_file ]
}
# NOTE: Constraints should be ignored for this step.
if (pw_build_PYTHON_PIP_INSTALL_OFFLINE) {
args += [ "--no-index" ]
}
if (pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE) {
args += [ "--no-cache-dir" ]
}
if (pw_build_PYTHON_PIP_INSTALL_FIND_LINKS != []) {
foreach(link_dir, pw_build_PYTHON_PIP_INSTALL_FIND_LINKS) {
args += [
"--find-links",
rebase_path(link_dir, root_build_dir),
]
}
}
deps = [ ":${invoker.target_name}._create_virtualenv($pw_build_PYTHON_TOOLCHAIN)" ]
stamp = true
pool = "$dir_pw_build/pool:pip($default_toolchain)"
}
# Install all 3rd party Python dependencies.
pw_python_action("${target_name}._install_3p_deps") {
module = "pip"
# Set the venv to run this pip install in.
_pw_internal_run_in_venv = true
_skip_installing_external_python_deps = true
venv = get_label_info(":${invoker.target_name}", "label_no_toolchain")
args = pw_build_PYTHON_PIP_DEFAULT_OPTIONS
args += [
"install",
"--upgrade",
]
if (_output_logs) {
_pip_install_log_file =
"$target_gen_dir/$target_name/pip_install_log.txt"
args += [
"--log",
rebase_path(_pip_install_log_file, root_build_dir),
]
}
if (_pip_generate_hashes) {
args += [ "--require-hashes" ]
}
if (pw_build_PYTHON_PIP_INSTALL_OFFLINE) {
args += [ "--no-index" ]
}
if (pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE) {
args += [ "--no-cache-dir" ]
}
if (pw_build_PYTHON_PIP_INSTALL_FIND_LINKS != []) {
foreach(link_dir, pw_build_PYTHON_PIP_INSTALL_FIND_LINKS) {
args += [
"--find-links",
rebase_path(link_dir, root_build_dir),
]
}
}
# Note: --no-build-isolation should be avoided for installing 3rd party
# Python packages that use C/C++ extension modules.
# https://setuptools.pypa.io/en/latest/userguide/ext_modules.html
inputs = _constraints + _requirements + [ _compiled_requirements_file ]
# Use the pip-tools compiled requiremets file. This contains the fully
# expanded list of deps with constraints applied.
if (defined(invoker.source_packages)) {
inputs += [ _compiled_requirements_file ]
args += [
"--requirement",
rebase_path(_compiled_requirements_file, root_build_dir),
]
}
deps = [
":${invoker.target_name}._compile_requirements($pw_build_PYTHON_TOOLCHAIN)",
":${invoker.target_name}._generate_3p_requirements($pw_build_PYTHON_TOOLCHAIN)",
":${invoker.target_name}._install_base_3p_deps($pw_build_PYTHON_TOOLCHAIN)",
]
stamp = true
pool = "$dir_pw_build/pool:pip($default_toolchain)"
}
# Target to create a Python package cache for this pw_python_venv.
pw_python_action("${target_name}.vendor_wheels") {
_wheel_output_dir = "$target_gen_dir/$target_name/wheels"
_pip_download_logfile =
"$target_gen_dir/$target_name/pip_download_log.txt"
_pip_wheel_logfile = "$target_gen_dir/$target_name/pip_wheel_log.txt"
metadata = {
pw_python_package_wheels = [ _wheel_output_dir ]
}
script = "$dir_pw_build/py/pw_build/generate_python_wheel_cache.py"
# Set the venv to run this pip install in.
_pw_internal_run_in_venv = true
_skip_installing_external_python_deps = true
venv = get_label_info(":${invoker.target_name}", "label_no_toolchain")
args = [
"--pip-download-log",
rebase_path(_pip_download_logfile, root_build_dir),
"--wheel-dir",
rebase_path(_wheel_output_dir, root_build_dir),
"-r",
rebase_path(_compiled_requirements_file, root_build_dir),
]
if (pw_build_PYTHON_PIP_DOWNLOAD_ALL_PLATFORMS) {
args += [ "--download-all-platforms" ]
}
deps = [
":${invoker.target_name}._compile_requirements($pw_build_PYTHON_TOOLCHAIN)",
":${invoker.target_name}._generate_3p_requirements($pw_build_PYTHON_TOOLCHAIN)",
]
outputs = [
_wheel_output_dir,
_pip_wheel_logfile,
_pip_download_logfile,
]
pool = "$dir_pw_build/pool:pip($default_toolchain)"
}
# End pw_build_PYTHON_TOOLCHAIN check
} else {
group("${target_name}._compile_requirements") {
public_deps = [ ":${target_name}($pw_build_PYTHON_TOOLCHAIN)" ]
}
group("${target_name}._install_3p_deps") {
public_deps = [ ":${target_name}($pw_build_PYTHON_TOOLCHAIN)" ]
}
group("${target_name}.vendor_wheels") {
public_deps = [ ":${target_name}($pw_build_PYTHON_TOOLCHAIN)" ]
}
}
} else {
group("${target_name}._compile_requirements") {
}
group("${target_name}._install_3p_deps") {
}
group("${target_name}.vendor_wheels") {
}
}
# Have this target directly depend on _install_3p_deps
group("$target_name") {
public_deps =
[ ":${target_name}._install_3p_deps($pw_build_PYTHON_TOOLCHAIN)" ]
}
}