feat: multi-toolchain support (#846)
* feat: multi-toolchain support
This adds support for multiple Python versions on the same Bazel
workspace.
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* feat: cross-version testing
A py_test using 3.10 runs a py_binary using 3.9.
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: error message
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* doc: add link to bazelbuild/bazel PR fixing expand_location
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: set environment variables for py_binary too
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* test: extra case for default version taking another version
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: fail if args attribute is set
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: remove confusing output with same target name
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: buildifier
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* revert: use testing.TestEnvironment
See comment in code for the reasons.
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: linting issues
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: black linter
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: move tests to a sub-dir
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* feat: add multi_pip_parse
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: add missing aliases
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: use requirement function in example
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: deleted packages
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: update generated docs
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: version checking of the rule is already done by other tests
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: add python_interpreter_target to multi_pip_parse
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: windows
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: unify py_test and py_binary transition impls
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: test compatible with all platforms
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* rebase: adjust multi_python_versions on ci
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: use usr flags instead of platforms in transition
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: rename rule -> rule_impl
This avoids confusion with the global `rule`
https://bazel.build/rules/lib/globals#rule.
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: reduce repetition of args
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: missing test and binary-specific attributes
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: add srcs and deps attrs for path expansion
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: missing bazel_skylib on integration tests
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: use ctx.target_platform_has_constraint over select
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* doc: why symlink <name>.zip under Windows
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: apply suggestions from code review
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: use incoming edge transitions
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: use RunEnvironmentInfo when available
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: cfg should be target not exec
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index d7ca8ef..56516e9 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -85,6 +85,22 @@
working_directory: examples/bzlmod
platform: windows
+ integration_test_multi_python_versions_linux:
+ <<: *reusable_build_test_all
+ name: multi_python_versions integration tests on Linux
+ working_directory: examples/multi_python_versions
+ platform: ubuntu2004
+ integration_test_multi_python_versions_macos:
+ <<: *reusable_build_test_all
+ name: multi_python_versions integration tests on macOS
+ working_directory: examples/multi_python_versions
+ platform: macos
+ integration_test_multi_python_versions_windows:
+ <<: *reusable_build_test_all
+ name: multi_python_versions integration tests on Windows
+ working_directory: examples/multi_python_versions
+ platform: windows
+
integration_test_pip_install_linux:
<<: *reusable_build_test_all
name: pip_install integration tests on Linux
diff --git a/.bazelrc b/.bazelrc
index a4bcccf..510191b 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -3,8 +3,8 @@
# This lets us glob() up all the files inside the examples to make them inputs to tests
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
# To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh
-build --deleted_packages=examples/build_file_generation,examples/bzlmod,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements,tests/pip_repository_entry_points,tests/pip_deps
-query --deleted_packages=examples/build_file_generation,examples/bzlmod,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements,tests/pip_repository_entry_points,tests/pip_deps
+build --deleted_packages=examples/build_file_generation,examples/bzlmod,examples/multi_python_versions,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements,tests/pip_repository_entry_points,tests/pip_deps
+query --deleted_packages=examples/build_file_generation,examples/bzlmod,examples/multi_python_versions,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements,tests/pip_repository_entry_points,tests/pip_deps
test --test_output=errors
diff --git a/WORKSPACE b/WORKSPACE
index ff1b956..1d9d5e4 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -25,13 +25,13 @@
rules_python_internal_setup()
-load("//python:repositories.bzl", "python_register_toolchains")
+load("//python:repositories.bzl", "python_register_multi_toolchains")
load("//python:versions.bzl", "MINOR_MAPPING")
-python_register_toolchains(
+python_register_multi_toolchains(
name = "python",
- # We always use the latest Python internally.
- python_version = MINOR_MAPPING.values()[-1],
+ default_version = MINOR_MAPPING.values()[-1],
+ python_versions = MINOR_MAPPING.values(),
)
load("//gazelle:deps.bzl", "gazelle_deps")
diff --git a/docs/pip.md b/docs/pip.md
index fc38f0f..5f6c1d8 100644
--- a/docs/pip.md
+++ b/docs/pip.md
@@ -2,13 +2,36 @@
Import pip requirements into Bazel.
+<a id="whl_library_alias"></a>
+
+## whl_library_alias
+
+<pre>
+whl_library_alias(<a href="#whl_library_alias-name">name</a>, <a href="#whl_library_alias-default_version">default_version</a>, <a href="#whl_library_alias-repo_mapping">repo_mapping</a>, <a href="#whl_library_alias-version_map">version_map</a>, <a href="#whl_library_alias-wheel_name">wheel_name</a>)
+</pre>
+
+
+
+**ATTRIBUTES**
+
+
+| Name | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="whl_library_alias-name"></a>name | A unique name for this repository. | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required | |
+| <a id="whl_library_alias-default_version"></a>default_version | - | String | required | |
+| <a id="whl_library_alias-repo_mapping"></a>repo_mapping | A dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.<p>For example, an entry <code>"@foo": "@bar"</code> declares that, for any time this repository depends on <code>@foo</code> (such as a dependency on <code>@foo//some:target</code>, it should actually resolve that dependency within globally-declared <code>@bar</code> (<code>@bar//some:target</code>). | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required | |
+| <a id="whl_library_alias-version_map"></a>version_map | - | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required | |
+| <a id="whl_library_alias-wheel_name"></a>wheel_name | - | String | required | |
+
+
<a id="compile_pip_requirements"></a>
## compile_pip_requirements
<pre>
-compile_pip_requirements(<a href="#compile_pip_requirements-name">name</a>, <a href="#compile_pip_requirements-extra_args">extra_args</a>, <a href="#compile_pip_requirements-visibility">visibility</a>, <a href="#compile_pip_requirements-requirements_in">requirements_in</a>, <a href="#compile_pip_requirements-requirements_txt">requirements_txt</a>,
- <a href="#compile_pip_requirements-requirements_linux">requirements_linux</a>, <a href="#compile_pip_requirements-requirements_darwin">requirements_darwin</a>, <a href="#compile_pip_requirements-requirements_windows">requirements_windows</a>, <a href="#compile_pip_requirements-tags">tags</a>, <a href="#compile_pip_requirements-kwargs">kwargs</a>)
+compile_pip_requirements(<a href="#compile_pip_requirements-name">name</a>, <a href="#compile_pip_requirements-extra_args">extra_args</a>, <a href="#compile_pip_requirements-py_binary">py_binary</a>, <a href="#compile_pip_requirements-py_test">py_test</a>, <a href="#compile_pip_requirements-requirements_in">requirements_in</a>, <a href="#compile_pip_requirements-requirements_txt">requirements_txt</a>,
+ <a href="#compile_pip_requirements-requirements_darwin">requirements_darwin</a>, <a href="#compile_pip_requirements-requirements_linux">requirements_linux</a>, <a href="#compile_pip_requirements-requirements_windows">requirements_windows</a>, <a href="#compile_pip_requirements-visibility">visibility</a>,
+ <a href="#compile_pip_requirements-tags">tags</a>, <a href="#compile_pip_requirements-kwargs">kwargs</a>)
</pre>
Generates targets for managing pip dependencies with pip-compile.
@@ -28,16 +51,50 @@
| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
-| <a id="compile_pip_requirements-name"></a>name | base name for generated targets, typically "requirements" | none |
-| <a id="compile_pip_requirements-extra_args"></a>extra_args | passed to pip-compile | <code>[]</code> |
-| <a id="compile_pip_requirements-visibility"></a>visibility | passed to both the _test and .update rules | <code>["//visibility:private"]</code> |
-| <a id="compile_pip_requirements-requirements_in"></a>requirements_in | file expressing desired dependencies | <code>None</code> |
-| <a id="compile_pip_requirements-requirements_txt"></a>requirements_txt | result of "compiling" the requirements.in file | <code>None</code> |
-| <a id="compile_pip_requirements-requirements_linux"></a>requirements_linux | File of linux specific resolve output to check validate if requirement.in has changes. | <code>None</code> |
+| <a id="compile_pip_requirements-name"></a>name | base name for generated targets, typically "requirements". | none |
+| <a id="compile_pip_requirements-extra_args"></a>extra_args | passed to pip-compile. | <code>[]</code> |
+| <a id="compile_pip_requirements-py_binary"></a>py_binary | the py_binary rule to be used. | <code><function py_binary></code> |
+| <a id="compile_pip_requirements-py_test"></a>py_test | the py_test rule to be used. | <code><function py_test></code> |
+| <a id="compile_pip_requirements-requirements_in"></a>requirements_in | file expressing desired dependencies. | <code>None</code> |
+| <a id="compile_pip_requirements-requirements_txt"></a>requirements_txt | result of "compiling" the requirements.in file. | <code>None</code> |
| <a id="compile_pip_requirements-requirements_darwin"></a>requirements_darwin | File of darwin specific resolve output to check validate if requirement.in has changes. | <code>None</code> |
+| <a id="compile_pip_requirements-requirements_linux"></a>requirements_linux | File of linux specific resolve output to check validate if requirement.in has changes. | <code>None</code> |
| <a id="compile_pip_requirements-requirements_windows"></a>requirements_windows | File of windows specific resolve output to check validate if requirement.in has changes. | <code>None</code> |
-| <a id="compile_pip_requirements-tags"></a>tags | tagging attribute common to all build rules, passed to both the _test and .update rules | <code>None</code> |
-| <a id="compile_pip_requirements-kwargs"></a>kwargs | other bazel attributes passed to the "_test" rule | none |
+| <a id="compile_pip_requirements-visibility"></a>visibility | passed to both the _test and .update rules. | <code>["//visibility:private"]</code> |
+| <a id="compile_pip_requirements-tags"></a>tags | tagging attribute common to all build rules, passed to both the _test and .update rules. | <code>None</code> |
+| <a id="compile_pip_requirements-kwargs"></a>kwargs | other bazel attributes passed to the "_test" rule. | none |
+
+
+<a id="multi_pip_parse"></a>
+
+## multi_pip_parse
+
+<pre>
+multi_pip_parse(<a href="#multi_pip_parse-name">name</a>, <a href="#multi_pip_parse-default_version">default_version</a>, <a href="#multi_pip_parse-python_versions">python_versions</a>, <a href="#multi_pip_parse-python_interpreter_target">python_interpreter_target</a>,
+ <a href="#multi_pip_parse-requirements_lock">requirements_lock</a>, <a href="#multi_pip_parse-kwargs">kwargs</a>)
+</pre>
+
+NOT INTENDED FOR DIRECT USE!
+
+This is intended to be used by the multi_pip_parse implementation in the template of the
+multi_toolchain_aliases repository rule.
+
+
+**PARAMETERS**
+
+
+| Name | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="multi_pip_parse-name"></a>name | the name of the multi_pip_parse repository. | none |
+| <a id="multi_pip_parse-default_version"></a>default_version | the default Python version. | none |
+| <a id="multi_pip_parse-python_versions"></a>python_versions | all Python toolchain versions currently registered. | none |
+| <a id="multi_pip_parse-python_interpreter_target"></a>python_interpreter_target | a dictionary which keys are Python versions and values are resolved host interpreters. | none |
+| <a id="multi_pip_parse-requirements_lock"></a>requirements_lock | a dictionary which keys are Python versions and values are locked requirements files. | none |
+| <a id="multi_pip_parse-kwargs"></a>kwargs | extra arguments passed to all wrapped pip_parse. | none |
+
+**RETURNS**
+
+The internal implementation of multi_pip_parse repository rule.
<a id="package_annotation"></a>
diff --git a/examples/BUILD b/examples/BUILD
index fcdbdb1..39e4fce 100644
--- a/examples/BUILD
+++ b/examples/BUILD
@@ -39,6 +39,11 @@
)
bazel_integration_test(
+ name = "multi_python_versions_example",
+ timeout = "long",
+)
+
+bazel_integration_test(
name = "bzlmod_example",
bzlmod = True,
override_bazel_version = "6.0.0rc1",
diff --git a/examples/multi_python_versions/.bazelrc b/examples/multi_python_versions/.bazelrc
new file mode 100644
index 0000000..f23315a
--- /dev/null
+++ b/examples/multi_python_versions/.bazelrc
@@ -0,0 +1,5 @@
+test --test_output=errors
+
+# Windows requires these for multi-python support:
+build --enable_runfiles
+startup --windows_enable_symlinks
diff --git a/examples/multi_python_versions/.gitignore b/examples/multi_python_versions/.gitignore
new file mode 100644
index 0000000..ac51a05
--- /dev/null
+++ b/examples/multi_python_versions/.gitignore
@@ -0,0 +1 @@
+bazel-*
diff --git a/examples/multi_python_versions/WORKSPACE b/examples/multi_python_versions/WORKSPACE
new file mode 100644
index 0000000..9a6676e
--- /dev/null
+++ b/examples/multi_python_versions/WORKSPACE
@@ -0,0 +1,59 @@
+workspace(name = "rules_python_multi_python_versions")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+ name = "bazel_skylib",
+ sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d",
+ urls = [
+ "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
+ "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
+ ],
+)
+
+local_repository(
+ name = "rules_python",
+ path = "../..",
+)
+
+load("@rules_python//python/pip_install:repositories.bzl", "pip_install_dependencies")
+
+pip_install_dependencies()
+
+load("@rules_python//python:repositories.bzl", "python_register_multi_toolchains")
+
+default_python_version = "3.9"
+
+python_register_multi_toolchains(
+ name = "python",
+ default_version = default_python_version,
+ python_versions = [
+ "3.8",
+ "3.9",
+ "3.10",
+ ],
+)
+
+load("@python//:pip.bzl", "multi_pip_parse")
+load("@python//3.10:defs.bzl", interpreter_3_10 = "interpreter")
+load("@python//3.8:defs.bzl", interpreter_3_8 = "interpreter")
+load("@python//3.9:defs.bzl", interpreter_3_9 = "interpreter")
+
+multi_pip_parse(
+ name = "pypi",
+ default_version = default_python_version,
+ python_interpreter_target = {
+ "3.10": interpreter_3_10,
+ "3.8": interpreter_3_8,
+ "3.9": interpreter_3_9,
+ },
+ requirements_lock = {
+ "3.10": "//requirements:requirements_lock_3_10.txt",
+ "3.8": "//requirements:requirements_lock_3_8.txt",
+ "3.9": "//requirements:requirements_lock_3_9.txt",
+ },
+)
+
+load("@pypi//:requirements.bzl", "install_deps")
+
+install_deps()
diff --git a/examples/multi_python_versions/libs/my_lib/BUILD.bazel b/examples/multi_python_versions/libs/my_lib/BUILD.bazel
new file mode 100644
index 0000000..8c29f60
--- /dev/null
+++ b/examples/multi_python_versions/libs/my_lib/BUILD.bazel
@@ -0,0 +1,9 @@
+load("@pypi//:requirements.bzl", "requirement")
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "my_lib",
+ srcs = ["__init__.py"],
+ visibility = ["@//tests:__pkg__"],
+ deps = [requirement("websockets")],
+)
diff --git a/examples/multi_python_versions/libs/my_lib/__init__.py b/examples/multi_python_versions/libs/my_lib/__init__.py
new file mode 100644
index 0000000..3f02b96
--- /dev/null
+++ b/examples/multi_python_versions/libs/my_lib/__init__.py
@@ -0,0 +1,5 @@
+import websockets
+
+
+def websockets_is_for_python_version(sanitized_version_check):
+ return f"pypi_{sanitized_version_check}_websockets" in websockets.__file__
diff --git a/examples/multi_python_versions/requirements/BUILD.bazel b/examples/multi_python_versions/requirements/BUILD.bazel
new file mode 100644
index 0000000..4848fab
--- /dev/null
+++ b/examples/multi_python_versions/requirements/BUILD.bazel
@@ -0,0 +1,24 @@
+load("@python//3.10:defs.bzl", compile_pip_requirements_3_10 = "compile_pip_requirements")
+load("@python//3.8:defs.bzl", compile_pip_requirements_3_8 = "compile_pip_requirements")
+load("@python//3.9:defs.bzl", compile_pip_requirements_3_9 = "compile_pip_requirements")
+
+compile_pip_requirements_3_8(
+ name = "requirements_3_8",
+ extra_args = ["--allow-unsafe"],
+ requirements_in = "requirements.in",
+ requirements_txt = "requirements_lock_3_8.txt",
+)
+
+compile_pip_requirements_3_9(
+ name = "requirements_3_9",
+ extra_args = ["--allow-unsafe"],
+ requirements_in = "requirements.in",
+ requirements_txt = "requirements_lock_3_9.txt",
+)
+
+compile_pip_requirements_3_10(
+ name = "requirements_3_10",
+ extra_args = ["--allow-unsafe"],
+ requirements_in = "requirements.in",
+ requirements_txt = "requirements_lock_3_10.txt",
+)
diff --git a/examples/multi_python_versions/requirements/requirements.in b/examples/multi_python_versions/requirements/requirements.in
new file mode 100644
index 0000000..14774b4
--- /dev/null
+++ b/examples/multi_python_versions/requirements/requirements.in
@@ -0,0 +1 @@
+websockets
diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_10.txt b/examples/multi_python_versions/requirements/requirements_lock_3_10.txt
new file mode 100644
index 0000000..0e332bf
--- /dev/null
+++ b/examples/multi_python_versions/requirements/requirements_lock_3_10.txt
@@ -0,0 +1,56 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+# bazel run //requirements:requirements_3_10.update
+#
+websockets==10.3 \
+ --hash=sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af \
+ --hash=sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c \
+ --hash=sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76 \
+ --hash=sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47 \
+ --hash=sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69 \
+ --hash=sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079 \
+ --hash=sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c \
+ --hash=sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55 \
+ --hash=sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02 \
+ --hash=sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559 \
+ --hash=sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3 \
+ --hash=sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e \
+ --hash=sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978 \
+ --hash=sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98 \
+ --hash=sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae \
+ --hash=sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755 \
+ --hash=sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d \
+ --hash=sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991 \
+ --hash=sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1 \
+ --hash=sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680 \
+ --hash=sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247 \
+ --hash=sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f \
+ --hash=sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2 \
+ --hash=sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7 \
+ --hash=sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4 \
+ --hash=sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667 \
+ --hash=sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb \
+ --hash=sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094 \
+ --hash=sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36 \
+ --hash=sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79 \
+ --hash=sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500 \
+ --hash=sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e \
+ --hash=sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582 \
+ --hash=sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442 \
+ --hash=sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd \
+ --hash=sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6 \
+ --hash=sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731 \
+ --hash=sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4 \
+ --hash=sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d \
+ --hash=sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8 \
+ --hash=sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f \
+ --hash=sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677 \
+ --hash=sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8 \
+ --hash=sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9 \
+ --hash=sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e \
+ --hash=sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b \
+ --hash=sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916 \
+ --hash=sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4
+ # via -r requirements/requirements.in
diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_8.txt b/examples/multi_python_versions/requirements/requirements_lock_3_8.txt
new file mode 100644
index 0000000..30419da
--- /dev/null
+++ b/examples/multi_python_versions/requirements/requirements_lock_3_8.txt
@@ -0,0 +1,56 @@
+#
+# This file is autogenerated by pip-compile with python 3.8
+# To update, run:
+#
+# bazel run //requirements:requirements_3_8.update
+#
+websockets==10.3 \
+ --hash=sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af \
+ --hash=sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c \
+ --hash=sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76 \
+ --hash=sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47 \
+ --hash=sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69 \
+ --hash=sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079 \
+ --hash=sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c \
+ --hash=sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55 \
+ --hash=sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02 \
+ --hash=sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559 \
+ --hash=sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3 \
+ --hash=sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e \
+ --hash=sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978 \
+ --hash=sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98 \
+ --hash=sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae \
+ --hash=sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755 \
+ --hash=sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d \
+ --hash=sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991 \
+ --hash=sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1 \
+ --hash=sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680 \
+ --hash=sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247 \
+ --hash=sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f \
+ --hash=sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2 \
+ --hash=sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7 \
+ --hash=sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4 \
+ --hash=sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667 \
+ --hash=sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb \
+ --hash=sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094 \
+ --hash=sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36 \
+ --hash=sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79 \
+ --hash=sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500 \
+ --hash=sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e \
+ --hash=sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582 \
+ --hash=sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442 \
+ --hash=sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd \
+ --hash=sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6 \
+ --hash=sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731 \
+ --hash=sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4 \
+ --hash=sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d \
+ --hash=sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8 \
+ --hash=sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f \
+ --hash=sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677 \
+ --hash=sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8 \
+ --hash=sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9 \
+ --hash=sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e \
+ --hash=sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b \
+ --hash=sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916 \
+ --hash=sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4
+ # via -r requirements/requirements.in
diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_9.txt b/examples/multi_python_versions/requirements/requirements_lock_3_9.txt
new file mode 100644
index 0000000..124355e
--- /dev/null
+++ b/examples/multi_python_versions/requirements/requirements_lock_3_9.txt
@@ -0,0 +1,56 @@
+#
+# This file is autogenerated by pip-compile with python 3.9
+# To update, run:
+#
+# bazel run //requirements:requirements_3_9.update
+#
+websockets==10.3 \
+ --hash=sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af \
+ --hash=sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c \
+ --hash=sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76 \
+ --hash=sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47 \
+ --hash=sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69 \
+ --hash=sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079 \
+ --hash=sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c \
+ --hash=sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55 \
+ --hash=sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02 \
+ --hash=sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559 \
+ --hash=sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3 \
+ --hash=sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e \
+ --hash=sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978 \
+ --hash=sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98 \
+ --hash=sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae \
+ --hash=sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755 \
+ --hash=sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d \
+ --hash=sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991 \
+ --hash=sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1 \
+ --hash=sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680 \
+ --hash=sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247 \
+ --hash=sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f \
+ --hash=sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2 \
+ --hash=sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7 \
+ --hash=sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4 \
+ --hash=sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667 \
+ --hash=sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb \
+ --hash=sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094 \
+ --hash=sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36 \
+ --hash=sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79 \
+ --hash=sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500 \
+ --hash=sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e \
+ --hash=sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582 \
+ --hash=sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442 \
+ --hash=sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd \
+ --hash=sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6 \
+ --hash=sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731 \
+ --hash=sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4 \
+ --hash=sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d \
+ --hash=sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8 \
+ --hash=sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f \
+ --hash=sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677 \
+ --hash=sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8 \
+ --hash=sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9 \
+ --hash=sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e \
+ --hash=sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b \
+ --hash=sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916 \
+ --hash=sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4
+ # via -r requirements/requirements.in
diff --git a/examples/multi_python_versions/tests/BUILD.bazel b/examples/multi_python_versions/tests/BUILD.bazel
new file mode 100644
index 0000000..7219ca5
--- /dev/null
+++ b/examples/multi_python_versions/tests/BUILD.bazel
@@ -0,0 +1,148 @@
+load("@python//3.10:defs.bzl", py_binary_3_10 = "py_binary", py_test_3_10 = "py_test")
+load("@python//3.8:defs.bzl", py_binary_3_8 = "py_binary", py_test_3_8 = "py_test")
+load("@python//3.9:defs.bzl", py_binary_3_9 = "py_binary", py_test_3_9 = "py_test")
+load("@rules_python//python:defs.bzl", "py_binary", "py_test")
+
+py_binary(
+ name = "version_default",
+ srcs = ["version.py"],
+ main = "version.py",
+)
+
+py_binary_3_8(
+ name = "version_3_8",
+ srcs = ["version.py"],
+ main = "version.py",
+)
+
+py_binary_3_9(
+ name = "version_3_9",
+ srcs = ["version.py"],
+ main = "version.py",
+)
+
+py_binary_3_10(
+ name = "version_3_10",
+ srcs = ["version.py"],
+ main = "version.py",
+)
+
+py_test(
+ name = "my_lib_default_test",
+ srcs = ["my_lib_test.py"],
+ main = "my_lib_test.py",
+ deps = ["//libs/my_lib"],
+)
+
+py_test_3_8(
+ name = "my_lib_3_8_test",
+ srcs = ["my_lib_test.py"],
+ main = "my_lib_test.py",
+ deps = ["//libs/my_lib"],
+)
+
+py_test_3_9(
+ name = "my_lib_3_9_test",
+ srcs = ["my_lib_test.py"],
+ main = "my_lib_test.py",
+ deps = ["//libs/my_lib"],
+)
+
+py_test_3_10(
+ name = "my_lib_3_10_test",
+ srcs = ["my_lib_test.py"],
+ main = "my_lib_test.py",
+ deps = ["//libs/my_lib"],
+)
+
+py_test(
+ name = "version_default_test",
+ srcs = ["version_test.py"],
+ env = {"VERSION_CHECK": "3.9"}, # The default defined in the WORKSPACE.
+ main = "version_test.py",
+)
+
+py_test_3_8(
+ name = "version_3_8_test",
+ srcs = ["version_test.py"],
+ env = {"VERSION_CHECK": "3.8"},
+ main = "version_test.py",
+)
+
+py_test_3_9(
+ name = "version_3_9_test",
+ srcs = ["version_test.py"],
+ env = {"VERSION_CHECK": "3.9"},
+ main = "version_test.py",
+)
+
+py_test_3_10(
+ name = "version_3_10_test",
+ srcs = ["version_test.py"],
+ env = {"VERSION_CHECK": "3.10"},
+ main = "version_test.py",
+)
+
+py_test(
+ name = "version_default_takes_3_10_subprocess_test",
+ srcs = ["cross_version_test.py"],
+ data = [":version_3_10"],
+ env = {
+ "SUBPROCESS_VERSION_CHECK": "3.10",
+ "SUBPROCESS_VERSION_PY_BINARY": "$(rootpath :version_3_10)",
+ "VERSION_CHECK": "3.9",
+ },
+ main = "cross_version_test.py",
+)
+
+py_test_3_10(
+ name = "version_3_10_takes_3_9_subprocess_test",
+ srcs = ["cross_version_test.py"],
+ data = [":version_3_9"],
+ env = {
+ "SUBPROCESS_VERSION_CHECK": "3.9",
+ "SUBPROCESS_VERSION_PY_BINARY": "$(rootpath :version_3_9)",
+ "VERSION_CHECK": "3.10",
+ },
+ main = "cross_version_test.py",
+)
+
+sh_test(
+ name = "version_test_binary_default",
+ srcs = ["version_test.sh"],
+ data = [":version_default"],
+ env = {
+ "VERSION_CHECK": "3.9", # The default defined in the WORKSPACE.
+ "VERSION_PY_BINARY": "$(rootpath :version_default)",
+ },
+)
+
+sh_test(
+ name = "version_test_binary_3_8",
+ srcs = ["version_test.sh"],
+ data = [":version_3_8"],
+ env = {
+ "VERSION_CHECK": "3.8",
+ "VERSION_PY_BINARY": "$(rootpath :version_3_8)",
+ },
+)
+
+sh_test(
+ name = "version_test_binary_3_9",
+ srcs = ["version_test.sh"],
+ data = [":version_3_9"],
+ env = {
+ "VERSION_CHECK": "3.9",
+ "VERSION_PY_BINARY": "$(rootpath :version_3_9)",
+ },
+)
+
+sh_test(
+ name = "version_test_binary_3_10",
+ srcs = ["version_test.sh"],
+ data = [":version_3_10"],
+ env = {
+ "VERSION_CHECK": "3.10",
+ "VERSION_PY_BINARY": "$(rootpath :version_3_10)",
+ },
+)
diff --git a/examples/multi_python_versions/tests/cross_version_test.py b/examples/multi_python_versions/tests/cross_version_test.py
new file mode 100644
index 0000000..f933ed6
--- /dev/null
+++ b/examples/multi_python_versions/tests/cross_version_test.py
@@ -0,0 +1,25 @@
+import os
+import subprocess
+import sys
+
+process = subprocess.run(
+ [os.getenv("SUBPROCESS_VERSION_PY_BINARY")],
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+)
+
+subprocess_current = process.stdout.strip()
+subprocess_expected = os.getenv("SUBPROCESS_VERSION_CHECK")
+
+if subprocess_current != subprocess_expected:
+ print(
+ f"expected subprocess version '{subprocess_expected}' is different than returned '{subprocess_current}'"
+ )
+ sys.exit(1)
+
+expected = os.getenv("VERSION_CHECK")
+current = f"{sys.version_info.major}.{sys.version_info.minor}"
+
+if current != expected:
+ print(f"expected version '{expected}' is different than returned '{current}'")
+ sys.exit(1)
diff --git a/examples/multi_python_versions/tests/my_lib_test.py b/examples/multi_python_versions/tests/my_lib_test.py
new file mode 100644
index 0000000..4fc7095
--- /dev/null
+++ b/examples/multi_python_versions/tests/my_lib_test.py
@@ -0,0 +1,10 @@
+import os
+import sys
+
+import libs.my_lib as my_lib
+
+sanitized_version_check = f"{sys.version_info.major}_{sys.version_info.minor}"
+
+if not my_lib.websockets_is_for_python_version(sanitized_version_check):
+ print("expected package for Python version is different than returned")
+ sys.exit(1)
diff --git a/examples/multi_python_versions/tests/version.py b/examples/multi_python_versions/tests/version.py
new file mode 100644
index 0000000..1007a14
--- /dev/null
+++ b/examples/multi_python_versions/tests/version.py
@@ -0,0 +1,3 @@
+import sys
+
+print(f"{sys.version_info.major}.{sys.version_info.minor}")
diff --git a/examples/multi_python_versions/tests/version_test.py b/examples/multi_python_versions/tests/version_test.py
new file mode 100644
index 0000000..305773c
--- /dev/null
+++ b/examples/multi_python_versions/tests/version_test.py
@@ -0,0 +1,9 @@
+import os
+import sys
+
+expected = os.getenv("VERSION_CHECK")
+current = f"{sys.version_info.major}.{sys.version_info.minor}"
+
+if current != expected:
+ print(f"expected version '{expected}' is different than returned '{current}'")
+ sys.exit(1)
diff --git a/examples/multi_python_versions/tests/version_test.sh b/examples/multi_python_versions/tests/version_test.sh
new file mode 100755
index 0000000..b8f510d
--- /dev/null
+++ b/examples/multi_python_versions/tests/version_test.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -o errexit -o nounset -o pipefail
+
+version_py_binary=$("${VERSION_PY_BINARY}")
+
+if [[ "${version_py_binary}" != "${VERSION_CHECK}" ]]; then
+ echo >&2 "expected version '${VERSION_CHECK}' is different than returned '${version_py_binary}'"
+ exit 1
+fi
diff --git a/examples/pip_parse/WORKSPACE b/examples/pip_parse/WORKSPACE
index e96db9f..cd557a3 100644
--- a/examples/pip_parse/WORKSPACE
+++ b/examples/pip_parse/WORKSPACE
@@ -1,5 +1,16 @@
workspace(name = "rules_python_pip_parse_example")
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+ name = "bazel_skylib",
+ sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d",
+ urls = [
+ "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
+ "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
+ ],
+)
+
local_repository(
name = "rules_python",
path = "../..",
diff --git a/python/BUILD b/python/BUILD
index ce19653..dcdbee1 100644
--- a/python/BUILD
+++ b/python/BUILD
@@ -34,8 +34,9 @@
name = "distribution",
srcs = glob(["**"]) + [
"//python/constraints:distribution",
- "//python/runfiles:distribution",
+ "//python/config_settings:distribution",
"//python/private:distribution",
+ "//python/runfiles:distribution",
],
visibility = ["//:__pkg__"],
)
diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel
new file mode 100644
index 0000000..272ba78
--- /dev/null
+++ b/python/config_settings/BUILD.bazel
@@ -0,0 +1,12 @@
+load("//python:versions.bzl", "TOOL_VERSIONS")
+load(":config_settings.bzl", "construct_config_settings")
+
+filegroup(
+ name = "distribution",
+ srcs = glob(["*.bzl"]) + [
+ "BUILD.bazel",
+ ],
+ visibility = ["//python:__pkg__"],
+)
+
+construct_config_settings(python_versions = TOOL_VERSIONS.keys())
diff --git a/python/config_settings/config_settings.bzl b/python/config_settings/config_settings.bzl
new file mode 100644
index 0000000..977d023
--- /dev/null
+++ b/python/config_settings/config_settings.bzl
@@ -0,0 +1,26 @@
+"""This module is used to construct the config settings in the BUILD file in this same package.
+"""
+
+load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
+
+# buildifier: disable=unnamed-macro
+def construct_config_settings(python_versions):
+ """Constructs a set of configs for all Python versions.
+
+ Args:
+ python_versions: The Python versions supported by rules_python.
+ """
+ string_flag(
+ name = "python_version",
+ build_setting_default = python_versions[0],
+ values = python_versions,
+ visibility = ["//visibility:public"],
+ )
+
+ for python_version in python_versions:
+ python_version_constraint_setting = "is_python_" + python_version
+ native.config_setting(
+ name = python_version_constraint_setting,
+ flag_values = {":python_version": python_version},
+ visibility = ["//visibility:public"],
+ )
diff --git a/python/config_settings/transition.bzl b/python/config_settings/transition.bzl
new file mode 100644
index 0000000..2fd3384
--- /dev/null
+++ b/python/config_settings/transition.bzl
@@ -0,0 +1,208 @@
+"""The transition module contains the rule definitions to wrap py_binary and py_test and transition
+them to the desired target platform.
+"""
+
+load("@bazel_skylib//lib:dicts.bzl", "dicts")
+load("//python:defs.bzl", _py_binary = "py_binary", _py_test = "py_test")
+
+def _transition_python_version_impl(_, attr):
+ return {"//python/config_settings:python_version": str(attr.python_version)}
+
+_transition_python_version = transition(
+ implementation = _transition_python_version_impl,
+ inputs = [],
+ outputs = ["//python/config_settings:python_version"],
+)
+
+def _transition_py_impl(ctx):
+ target = ctx.attr.target
+ windows_constraint = ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]
+ target_is_windows = ctx.target_platform_has_constraint(windows_constraint)
+ executable = ctx.actions.declare_file(ctx.attr.name + (".exe" if target_is_windows else ""))
+ ctx.actions.symlink(
+ is_executable = True,
+ output = executable,
+ target_file = target[DefaultInfo].files_to_run.executable,
+ )
+ zipfile_symlink = None
+ if target_is_windows:
+ # Under Windows, the expected "<name>.zip" does not exist, so we have to
+ # create the symlink ourselves to achieve the same behaviour as in macOS
+ # and Linux.
+ zipfile = None
+ expected_target_path = target[DefaultInfo].files_to_run.executable.short_path[:-4] + ".zip"
+ for file in target[DefaultInfo].default_runfiles.files.to_list():
+ if file.short_path == expected_target_path:
+ zipfile = file
+ zipfile_symlink = ctx.actions.declare_file(ctx.attr.name + ".zip")
+ ctx.actions.symlink(
+ is_executable = True,
+ output = zipfile_symlink,
+ target_file = zipfile,
+ )
+ env = {}
+ for k, v in ctx.attr.env.items():
+ env[k] = ctx.expand_location(v)
+
+ providers = [
+ DefaultInfo(
+ executable = executable,
+ files = depset([zipfile_symlink] if zipfile_symlink else [], transitive = [target[DefaultInfo].files]),
+ runfiles = ctx.runfiles([zipfile_symlink] if zipfile_symlink else []).merge(target[DefaultInfo].default_runfiles),
+ ),
+ target[PyInfo],
+ target[PyRuntimeInfo],
+ # Ensure that the binary we're wrapping is included in code coverage.
+ coverage_common.instrumented_files_info(
+ ctx,
+ dependency_attributes = ["target"],
+ ),
+ target[OutputGroupInfo],
+ # testing.TestEnvironment is deprecated in favour of RunEnvironmentInfo but
+ # RunEnvironmentInfo is not exposed in Bazel < 5.3.
+ # https://github.com/bazelbuild/bazel/commit/dbdfa07e92f99497be9c14265611ad2920161483
+ (RunEnvironmentInfo if hasattr(native, "RunEnvironmentInfo") else testing.TestEnvironment)(environment = env),
+ ]
+ return providers
+
+_COMMON_ATTRS = {
+ "deps": attr.label_list(
+ mandatory = False,
+ ),
+ "env": attr.string_dict(
+ mandatory = False,
+ ),
+ "python_version": attr.string(
+ mandatory = True,
+ ),
+ "srcs": attr.label_list(
+ allow_files = True,
+ mandatory = False,
+ ),
+ "target": attr.label(
+ executable = True,
+ cfg = "target",
+ mandatory = True,
+ providers = [PyInfo],
+ ),
+ # "tools" is a hack here. It should be "data" but "data" is not included by default in the
+ # location expansion in the same way it is in the native Python rules. The difference on how
+ # the Bazel deals with those special attributes differ on the LocationExpander, e.g.:
+ # https://github.com/bazelbuild/bazel/blob/ce611646/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java#L415-L429
+ #
+ # Since the default LocationExpander used by ctx.expand_location is not the same as the native
+ # rules (it doesn't set "allowDataAttributeEntriesInLabel"), we use "tools" temporarily while a
+ # proper fix in Bazel happens.
+ #
+ # A fix for this was proposed in https://github.com/bazelbuild/bazel/pull/16381.
+ "tools": attr.label_list(
+ allow_files = True,
+ mandatory = False,
+ ),
+ # Required to Opt-in to the transitions feature.
+ "_allowlist_function_transition": attr.label(
+ default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+ ),
+ "_windows_constraint": attr.label(
+ default = "@platforms//os:windows",
+ ),
+}
+
+_transition_py_binary = rule(
+ _transition_py_impl,
+ attrs = _COMMON_ATTRS,
+ cfg = _transition_python_version,
+ executable = True,
+)
+
+_transition_py_test = rule(
+ _transition_py_impl,
+ attrs = _COMMON_ATTRS,
+ cfg = _transition_python_version,
+ test = True,
+)
+
+def _py_rule(rule_impl, transition_rule, name, python_version, **kwargs):
+ args = kwargs.pop("args", None)
+ data = kwargs.pop("data", None)
+ env = kwargs.pop("env", None)
+ srcs = kwargs.pop("srcs", None)
+ deps = kwargs.pop("deps", None)
+
+ # Attributes common to all build rules.
+ # https://bazel.build/reference/be/common-definitions#common-attributes
+ compatible_with = kwargs.pop("compatible_with", None)
+ deprecation = kwargs.pop("deprecation", None)
+ distribs = kwargs.pop("distribs", None)
+ exec_compatible_with = kwargs.pop("exec_compatible_with", None)
+ exec_properties = kwargs.pop("exec_properties", None)
+ features = kwargs.pop("features", None)
+ restricted_to = kwargs.pop("restricted_to", None)
+ tags = kwargs.pop("tags", None)
+ target_compatible_with = kwargs.pop("target_compatible_with", None)
+ testonly = kwargs.pop("testonly", None)
+ toolchains = kwargs.pop("toolchains", None)
+ visibility = kwargs.pop("visibility", None)
+
+ common_attrs = {
+ "compatible_with": compatible_with,
+ "deprecation": deprecation,
+ "distribs": distribs,
+ "exec_compatible_with": exec_compatible_with,
+ "exec_properties": exec_properties,
+ "features": features,
+ "restricted_to": restricted_to,
+ "target_compatible_with": target_compatible_with,
+ "testonly": testonly,
+ "toolchains": toolchains,
+ }
+
+ # Test-specific extra attributes.
+ if "env_inherit" in kwargs:
+ common_attrs["env_inherit"] = kwargs.pop("env_inherit")
+ if "size" in kwargs:
+ common_attrs["size"] = kwargs.pop("size")
+ if "timeout" in kwargs:
+ common_attrs["timeout"] = kwargs.pop("timeout")
+ if "flaky" in kwargs:
+ common_attrs["flaky"] = kwargs.pop("flaky")
+ if "shard_count" in kwargs:
+ common_attrs["shard_count"] = kwargs.pop("shard_count")
+ if "local" in kwargs:
+ common_attrs["local"] = kwargs.pop("local")
+
+ # Binary-specific extra attributes.
+ if "output_licenses" in kwargs:
+ common_attrs["output_licenses"] = kwargs.pop("output_licenses")
+
+ rule_impl(
+ name = "_" + name,
+ args = args,
+ data = data,
+ deps = deps,
+ env = env,
+ srcs = srcs,
+ tags = ["manual"] + (tags if tags else []),
+ visibility = ["//visibility:private"],
+ **dicts.add(common_attrs, kwargs)
+ )
+
+ return transition_rule(
+ name = name,
+ args = args,
+ deps = deps,
+ env = env,
+ python_version = python_version,
+ srcs = srcs,
+ tags = tags,
+ target = ":_" + name,
+ tools = data,
+ visibility = visibility,
+ **common_attrs
+ )
+
+def py_binary(name, python_version, **kwargs):
+ return _py_rule(_py_binary, _transition_py_binary, name, python_version, **kwargs)
+
+def py_test(name, python_version, **kwargs):
+ return _py_rule(_py_test, _transition_py_test, name, python_version, **kwargs)
diff --git a/python/pip.bzl b/python/pip.bzl
index 02ea538..a3c9b69 100644
--- a/python/pip.bzl
+++ b/python/pip.bzl
@@ -16,6 +16,7 @@
load("//python/pip_install:pip_repository.bzl", "pip_repository", _package_annotation = "package_annotation")
load("//python/pip_install:repositories.bzl", "pip_install_dependencies")
load("//python/pip_install:requirements.bzl", _compile_pip_requirements = "compile_pip_requirements")
+load(":versions.bzl", "MINOR_MAPPING")
compile_pip_requirements = _compile_pip_requirements
package_annotation = _package_annotation
@@ -163,3 +164,200 @@
bzlmod = bzlmod,
**kwargs
)
+
+def _multi_pip_parse_impl(rctx):
+ rules_python = rctx.attr._rules_python_workspace.workspace_name
+ load_statements = []
+ install_deps_calls = []
+ process_requirements_calls = []
+ for python_version, pypi_repository in rctx.attr.pip_parses.items():
+ sanitized_python_version = python_version.replace(".", "_")
+ load_statement = """\
+load(
+ "@{pypi_repository}//:requirements.bzl",
+ _{sanitized_python_version}_install_deps = "install_deps",
+ _{sanitized_python_version}_all_requirements = "all_requirements",
+)""".format(
+ pypi_repository = pypi_repository,
+ sanitized_python_version = sanitized_python_version,
+ )
+ load_statements.append(load_statement)
+ process_requirements_call = """\
+_process_requirements(
+ pkg_labels = _{sanitized_python_version}_all_requirements,
+ python_version = "{python_version}",
+ repo_prefix = "{pypi_repository}_",
+)""".format(
+ pypi_repository = pypi_repository,
+ python_version = python_version,
+ sanitized_python_version = sanitized_python_version,
+ )
+ process_requirements_calls.append(process_requirements_call)
+ install_deps_call = """ _{sanitized_python_version}_install_deps(**whl_library_kwargs)""".format(
+ sanitized_python_version = sanitized_python_version,
+ )
+ install_deps_calls.append(install_deps_call)
+
+ requirements_bzl = """\
+# Generated by python/pip.bzl
+
+load("@{rules_python}//python:pip.bzl", "whl_library_alias")
+{load_statements}
+
+_wheel_names = []
+_version_map = dict()
+def _process_requirements(pkg_labels, python_version, repo_prefix):
+ for pkg_label in pkg_labels:
+ workspace_name = Label(pkg_label).workspace_name
+ wheel_name = workspace_name[len(repo_prefix):]
+ _wheel_names.append(wheel_name)
+ if not wheel_name in _version_map:
+ _version_map[wheel_name] = dict()
+ _version_map[wheel_name][python_version] = repo_prefix
+
+{process_requirements_calls}
+
+def _clean_name(name):
+ return name.replace("-", "_").replace(".", "_").lower()
+
+def requirement(name):
+ return "@{name}_" + _clean_name(name) + "//:pkg"
+
+def whl_requirement(name):
+ return "@{name}_" + _clean_name(name) + "//:whl"
+
+def data_requirement(name):
+ return "@{name}_" + _clean_name(name) + "//:data"
+
+def dist_info_requirement(name):
+ return "@{name}_" + _clean_name(name) + "//:dist_info"
+
+def entry_point(pkg, script = None):
+ fail("Not implemented yet")
+
+def install_deps(**whl_library_kwargs):
+{install_deps_calls}
+ for wheel_name in _wheel_names:
+ whl_library_alias(
+ name = "{name}_" + wheel_name,
+ wheel_name = wheel_name,
+ default_version = "{default_version}",
+ version_map = _version_map[wheel_name],
+ )
+""".format(
+ name = rctx.attr.name,
+ install_deps_calls = "\n".join(install_deps_calls),
+ load_statements = "\n".join(load_statements),
+ process_requirements_calls = "\n".join(process_requirements_calls),
+ rules_python = rules_python,
+ default_version = rctx.attr.default_version,
+ )
+ rctx.file("requirements.bzl", requirements_bzl)
+ rctx.file("BUILD.bazel", "exports_files(['requirements.bzl'])")
+
+_multi_pip_parse = repository_rule(
+ _multi_pip_parse_impl,
+ attrs = {
+ "default_version": attr.string(),
+ "pip_parses": attr.string_dict(),
+ "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
+ },
+)
+
+def _whl_library_alias_impl(rctx):
+ rules_python = rctx.attr._rules_python_workspace.workspace_name
+ default_repo_prefix = rctx.attr.version_map[rctx.attr.default_version]
+ version_map = rctx.attr.version_map.items()
+ build_content = ["# Generated by python/pip.bzl"]
+ for alias_name in ["pkg", "whl", "data", "dist_info"]:
+ build_content.append(_whl_library_render_alias_target(
+ alias_name = alias_name,
+ default_repo_prefix = default_repo_prefix,
+ rules_python = rules_python,
+ version_map = version_map,
+ wheel_name = rctx.attr.wheel_name,
+ ))
+ rctx.file("BUILD.bazel", "\n".join(build_content))
+
+def _whl_library_render_alias_target(
+ alias_name,
+ default_repo_prefix,
+ rules_python,
+ version_map,
+ wheel_name):
+ alias = ["""\
+alias(
+ name = "{alias_name}",
+ actual = select({{""".format(alias_name = alias_name)]
+ for [python_version, repo_prefix] in version_map:
+ alias.append("""\
+ "@{rules_python}//python/config_settings:is_python_{full_python_version}": "{actual}",""".format(
+ full_python_version = MINOR_MAPPING[python_version] if python_version in MINOR_MAPPING else python_version,
+ actual = "@{repo_prefix}{wheel_name}//:{alias_name}".format(
+ repo_prefix = repo_prefix,
+ wheel_name = wheel_name,
+ alias_name = alias_name,
+ ),
+ rules_python = rules_python,
+ ))
+ alias.append("""\
+ "//conditions:default": "{default_actual}",
+ }}),
+ visibility = ["//visibility:public"],
+)""".format(
+ default_actual = "@{repo_prefix}{wheel_name}//:{alias_name}".format(
+ repo_prefix = default_repo_prefix,
+ wheel_name = wheel_name,
+ alias_name = alias_name,
+ ),
+ ))
+ return "\n".join(alias)
+
+whl_library_alias = repository_rule(
+ _whl_library_alias_impl,
+ attrs = {
+ "default_version": attr.string(mandatory = True),
+ "version_map": attr.string_dict(mandatory = True),
+ "wheel_name": attr.string(mandatory = True),
+ "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
+ },
+)
+
+def multi_pip_parse(name, default_version, python_versions, python_interpreter_target, requirements_lock, **kwargs):
+ """NOT INTENDED FOR DIRECT USE!
+
+ This is intended to be used by the multi_pip_parse implementation in the template of the
+ multi_toolchain_aliases repository rule.
+
+ Args:
+ name: the name of the multi_pip_parse repository.
+ default_version: the default Python version.
+ python_versions: all Python toolchain versions currently registered.
+ python_interpreter_target: a dictionary which keys are Python versions and values are resolved host interpreters.
+ requirements_lock: a dictionary which keys are Python versions and values are locked requirements files.
+ **kwargs: extra arguments passed to all wrapped pip_parse.
+
+ Returns:
+ The internal implementation of multi_pip_parse repository rule.
+ """
+ pip_parses = {}
+ for python_version in python_versions:
+ if not python_version in python_interpreter_target:
+ fail("Missing python_interpreter_target for Python version %s in '%s'" % (python_version, name))
+ if not python_version in requirements_lock:
+ fail("Missing requirements_lock for Python version %s in '%s'" % (python_version, name))
+
+ pip_parse_name = name + "_" + python_version.replace(".", "_")
+ pip_parse(
+ name = pip_parse_name,
+ python_interpreter_target = python_interpreter_target[python_version],
+ requirements_lock = requirements_lock[python_version],
+ **kwargs
+ )
+ pip_parses[python_version] = pip_parse_name
+
+ return _multi_pip_parse(
+ name = name,
+ default_version = default_version,
+ pip_parses = pip_parses,
+ )
diff --git a/python/pip_install/requirements.bzl b/python/pip_install/requirements.bzl
index cca9213..7e248f6 100644
--- a/python/pip_install/requirements.bzl
+++ b/python/pip_install/requirements.bzl
@@ -1,17 +1,19 @@
"""Rules to verify and update pip-compile locked requirements.txt"""
-load("//python:defs.bzl", "py_binary", "py_test")
+load("//python:defs.bzl", _py_binary = "py_binary", _py_test = "py_test")
load("//python/pip_install:repositories.bzl", "requirement")
def compile_pip_requirements(
name,
extra_args = [],
- visibility = ["//visibility:private"],
+ py_binary = _py_binary,
+ py_test = _py_test,
requirements_in = None,
requirements_txt = None,
- requirements_linux = None,
requirements_darwin = None,
+ requirements_linux = None,
requirements_windows = None,
+ visibility = ["//visibility:private"],
tags = None,
**kwargs):
"""Generates targets for managing pip dependencies with pip-compile.
@@ -26,16 +28,18 @@
- update with `bazel run <name>.update`
Args:
- name: base name for generated targets, typically "requirements"
- extra_args: passed to pip-compile
- visibility: passed to both the _test and .update rules
- requirements_in: file expressing desired dependencies
- requirements_txt: result of "compiling" the requirements.in file
+ name: base name for generated targets, typically "requirements".
+ extra_args: passed to pip-compile.
+ py_binary: the py_binary rule to be used.
+ py_test: the py_test rule to be used.
+ requirements_in: file expressing desired dependencies.
+ requirements_txt: result of "compiling" the requirements.in file.
requirements_linux: File of linux specific resolve output to check validate if requirement.in has changes.
requirements_darwin: File of darwin specific resolve output to check validate if requirement.in has changes.
requirements_windows: File of windows specific resolve output to check validate if requirement.in has changes.
- tags: tagging attribute common to all build rules, passed to both the _test and .update rules
- **kwargs: other bazel attributes passed to the "_test" rule
+ tags: tagging attribute common to all build rules, passed to both the _test and .update rules.
+ visibility: passed to both the _test and .update rules.
+ **kwargs: other bazel attributes passed to the "_test" rule.
"""
requirements_in = name + ".in" if requirements_in == None else requirements_in
requirements_txt = name + ".txt" if requirements_txt == None else requirements_txt
diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl
index 282859a..93cf31a 100644
--- a/python/private/toolchains_repo.bzl
+++ b/python/private/toolchains_repo.bzl
@@ -32,8 +32,14 @@
)
def _toolchains_repo_impl(rctx):
+ rules_python_repository_name = rctx.attr._rules_python_workspace.workspace_name
+ python_version_constraint = "@{rules_python}//python/config_settings:is_python_{python_version}".format(
+ rules_python = rules_python_repository_name,
+ python_version = rctx.attr.python_version,
+ )
+
build_content = """\
-# Generated by toolchains_repo.bzl
+# Generated by python/private/toolchains_repo.bzl
#
# These can be registered in the workspace file or passed to --extra_toolchains
# flag. By default all these toolchains are registered by the
@@ -49,14 +55,17 @@
toolchain(
name = "{platform}_toolchain",
target_compatible_with = {compatible_with},
+ target_settings = ["{python_version_constraint}"] if {set_python_version_constraint} else [],
toolchain = "@{user_repository_name}_{platform}//:python_runtimes",
toolchain_type = "@bazel_tools//tools/python:toolchain_type",
)
""".format(
- platform = platform,
- name = rctx.attr.name,
- user_repository_name = rctx.attr.user_repository_name,
compatible_with = meta.compatible_with,
+ name = rctx.attr.name,
+ platform = platform,
+ python_version_constraint = python_version_constraint,
+ set_python_version_constraint = rctx.attr.set_python_version_constraint,
+ user_repository_name = rctx.attr.user_repository_name,
)
rctx.file("BUILD.bazel", build_content)
@@ -66,26 +75,26 @@
doc = "Creates a repository with toolchain definitions for all known platforms " +
"which can be registered or selected.",
attrs = {
+ "python_version": attr.string(doc = "The Python version."),
+ "set_python_version_constraint": attr.bool(doc = "if target_compatible_with for the toolchain should set the version constraint"),
"user_repository_name": attr.string(doc = "what the user chose for the base name"),
+ "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
},
)
-def _resolved_interpreter_os_alias_impl(rctx):
- (os_name, arch) = _host_os_arch(rctx)
+def _toolchain_aliases_impl(rctx):
+ (os_name, arch) = get_host_os_arch(rctx)
- host_platform = None
- for platform, meta in PLATFORMS.items():
- if meta.os_name == os_name and meta.arch == arch:
- host_platform = platform
- if not host_platform:
- fail("No platform declared for host OS {} on arch {}".format(os_name, arch))
+ host_platform = get_host_platform(os_name, arch)
is_windows = (os_name == WINDOWS_NAME)
python3_binary_path = "python.exe" if is_windows else "bin/python3"
+ rules_python_repository_name = rctx.attr._rules_python_workspace.workspace_name
+
# Base BUILD file for this repository.
build_contents = """\
-# Generated by python/repositories.bzl
+# Generated by python/private/toolchains_repo.bzl
package(default_visibility = ["//visibility:public"])
exports_files(["defs.bzl"])
alias(name = "files", actual = "@{py_repository}_{host_platform}//:files")
@@ -112,29 +121,135 @@
# Expose a Starlark file so rules can know what host platform we used and where to find an interpreter
# when using repository_ctx.path, which doesn't understand aliases.
rctx.file("defs.bzl", content = """\
-# Generated by python/repositories.bzl
+# Generated by python/private/toolchains_repo.bzl
+
+load("@{rules_python}//python/config_settings:transition.bzl", _py_binary = "py_binary", _py_test = "py_test")
+load("@{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements")
+
host_platform = "{host_platform}"
interpreter = "@{py_repository}_{host_platform}//:{python3_binary_path}"
+
+def py_binary(name, **kwargs):
+ return _py_binary(
+ name = name,
+ python_version = "{python_version}",
+ **kwargs
+ )
+
+def py_test(name, **kwargs):
+ return _py_test(
+ name = name,
+ python_version = "{python_version}",
+ **kwargs
+ )
+
+def compile_pip_requirements(name, **kwargs):
+ return _compile_pip_requirements(
+ name = name,
+ py_binary = py_binary,
+ py_test = py_test,
+ **kwargs
+ )
+
""".format(
- py_repository = rctx.attr.user_repository_name,
host_platform = host_platform,
+ py_repository = rctx.attr.user_repository_name,
+ python_version = rctx.attr.python_version,
python3_binary_path = python3_binary_path,
+ rules_python = rules_python_repository_name,
))
-resolved_interpreter_os_alias = repository_rule(
- _resolved_interpreter_os_alias_impl,
+toolchain_aliases = repository_rule(
+ _toolchain_aliases_impl,
doc = """Creates a repository with a shorter name meant for the host platform, which contains
a BUILD.bazel file declaring aliases to the host platform's targets.
""",
attrs = {
+ "python_version": attr.string(doc = "The Python version."),
"user_repository_name": attr.string(
mandatory = True,
doc = "The base name for all created repositories, like 'python38'.",
),
+ "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
},
)
-def _host_os_arch(rctx):
+def _multi_toolchain_aliases_impl(rctx):
+ rules_python = rctx.attr._rules_python_workspace.workspace_name
+
+ for python_version, repository_name in rctx.attr.python_versions.items():
+ file = "{}/defs.bzl".format(python_version)
+ rctx.file(file, content = """\
+# Generated by python/private/toolchains_repo.bzl
+
+load(
+ "@{repository_name}//:defs.bzl",
+ _compile_pip_requirements = "compile_pip_requirements",
+ _host_platform = "host_platform",
+ _interpreter = "interpreter",
+ _py_binary = "py_binary",
+ _py_test = "py_test",
+)
+
+compile_pip_requirements = _compile_pip_requirements
+host_platform = _host_platform
+interpreter = _interpreter
+py_binary = _py_binary
+py_test = _py_test
+""".format(
+ repository_name = repository_name,
+ ))
+ rctx.file("{}/BUILD.bazel".format(python_version), "")
+
+ pip_bzl = """\
+# Generated by python/private/toolchains_repo.bzl
+
+load("@{rules_python}//python:pip.bzl", "pip_parse", _multi_pip_parse = "multi_pip_parse")
+
+def multi_pip_parse(name, requirements_lock, **kwargs):
+ return _multi_pip_parse(
+ name = name,
+ python_versions = {python_versions},
+ requirements_lock = requirements_lock,
+ **kwargs
+ )
+
+""".format(
+ python_versions = rctx.attr.python_versions.keys(),
+ rules_python = rules_python,
+ )
+ rctx.file("pip.bzl", content = pip_bzl)
+ rctx.file("BUILD.bazel", "")
+
+multi_toolchain_aliases = repository_rule(
+ _multi_toolchain_aliases_impl,
+ attrs = {
+ "python_versions": attr.string_dict(doc = "The Python versions."),
+ "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
+ },
+)
+
+def sanitize_platform_name(platform):
+ return platform.replace("-", "_")
+
+def get_host_platform(os_name, arch):
+ """Gets the host platform.
+
+ Args:
+ os_name: the host OS name.
+ arch: the host arch.
+ Returns:
+ The host platform.
+ """
+ host_platform = None
+ for platform, meta in PLATFORMS.items():
+ if meta.os_name == os_name and meta.arch == arch:
+ host_platform = platform
+ if not host_platform:
+ fail("No platform declared for host OS {} on arch {}".format(os_name, arch))
+ return host_platform
+
+def get_host_os_arch(rctx):
"""Infer the host OS name and arch from a repository context.
Args:
diff --git a/python/repositories.bzl b/python/repositories.bzl
index 6965bcd..e0c9b06 100644
--- a/python/repositories.bzl
+++ b/python/repositories.bzl
@@ -17,7 +17,12 @@
For historic reasons, pip_repositories() is defined in //python:pip.bzl.
"""
-load("//python/private:toolchains_repo.bzl", "resolved_interpreter_os_alias", "toolchains_repo")
+load(
+ "//python/private:toolchains_repo.bzl",
+ "multi_toolchain_aliases",
+ "toolchain_aliases",
+ "toolchains_repo",
+)
load(
":versions.bzl",
"DEFAULT_RELEASE_BASE_URL",
@@ -333,6 +338,7 @@
distutils = None,
distutils_content = None,
register_toolchains = True,
+ set_python_version_constraint = False,
tool_versions = TOOL_VERSIONS,
**kwargs):
"""Convenience macro for users which does typical setup.
@@ -350,6 +356,7 @@
distutils: see the distutils attribute in the python_repository repository rule.
distutils_content: see the distutils_content attribute in the python_repository repository rule.
register_toolchains: Whether or not to register the downloaded toolchains.
+ set_python_version_constraint: When set to true, target_compatible_with for the toolchains will include a version constraint.
tool_versions: a dict containing a mapping of version with SHASUM and platform info. If not supplied, the defaults
in python/versions.bzl will be used
**kwargs: passed to each python_repositories call.
@@ -359,6 +366,8 @@
if python_version in MINOR_MAPPING:
python_version = MINOR_MAPPING[python_version]
+ toolchain_repo_name = "{name}_toolchains".format(name = name)
+
for platform in PLATFORMS.keys():
sha256 = tool_versions[python_version]["sha256"].get(platform, None)
if not sha256:
@@ -382,17 +391,67 @@
**kwargs
)
if register_toolchains:
- native.register_toolchains("@{name}_toolchains//:{platform}_toolchain".format(
- name = name,
+ native.register_toolchains("@{toolchain_repo_name}//:{platform}_toolchain".format(
+ toolchain_repo_name = toolchain_repo_name,
platform = platform,
))
- resolved_interpreter_os_alias(
- name = name,
+ toolchains_repo(
+ name = toolchain_repo_name,
+ python_version = python_version,
+ set_python_version_constraint = set_python_version_constraint,
user_repository_name = name,
)
- toolchains_repo(
- name = "{name}_toolchains".format(name = name),
+ toolchain_aliases(
+ name = name,
+ python_version = python_version,
user_repository_name = name,
)
+
+def python_register_multi_toolchains(
+ name,
+ python_versions,
+ default_version = None,
+ **kwargs):
+ """Convenience macro for registering multiple Python toolchains.
+
+ Args:
+ name: base name for each name in python_register_toolchains call.
+ python_versions: the Python version.
+ default_version: the default Python version. If not set, the first version in
+ python_versions is used.
+ **kwargs: passed to each python_register_toolchains call.
+ """
+ if len(python_versions) == 0:
+ fail("python_versions must not be empty")
+
+ if not default_version:
+ default_version = python_versions.pop(0)
+ for python_version in python_versions:
+ if python_version == default_version:
+ # We register the default version lastly so that it's not picked first when --platforms
+ # is set with a constraint during toolchain resolution. This is due to the fact that
+ # Bazel will match the unconstrained toolchain if we register it before the constrained
+ # ones.
+ continue
+ python_register_toolchains(
+ name = name + "_" + python_version.replace(".", "_"),
+ python_version = python_version,
+ set_python_version_constraint = True,
+ **kwargs
+ )
+ python_register_toolchains(
+ name = name + "_" + default_version.replace(".", "_"),
+ python_version = default_version,
+ set_python_version_constraint = False,
+ **kwargs
+ )
+
+ multi_toolchain_aliases(
+ name = name,
+ python_versions = {
+ python_version: name + "_" + python_version.replace(".", "_")
+ for python_version in (python_versions + [default_version])
+ },
+ )
diff --git a/tests/pip_repository_entry_points/WORKSPACE b/tests/pip_repository_entry_points/WORKSPACE
index dd80db4..f9f3543 100644
--- a/tests/pip_repository_entry_points/WORKSPACE
+++ b/tests/pip_repository_entry_points/WORKSPACE
@@ -1,5 +1,16 @@
workspace(name = "pip_repository_annotations_example")
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+ name = "bazel_skylib",
+ sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d",
+ urls = [
+ "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
+ "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
+ ],
+)
+
local_repository(
name = "rules_python",
path = "../..",