aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/BUILD.bazel33
-rw-r--r--tools/bazel_integration_test/BUILD.bazel1
-rw-r--r--tools/bazel_integration_test/bazel_integration_test.bzl134
-rw-r--r--tools/bazel_integration_test/test_runner.py111
-rwxr-xr-xtools/bazel_integration_test/update_deleted_packages.sh39
-rw-r--r--tools/build_defs/python/BUILD.bazel13
-rw-r--r--tools/build_defs/python/tests/BUILD.bazel27
-rw-r--r--tools/build_defs/python/tests/base_tests.bzl124
-rw-r--r--tools/build_defs/python/tests/py_binary/BUILD.bazel17
-rw-r--r--tools/build_defs/python/tests/py_binary/py_binary_tests.bzl28
-rw-r--r--tools/build_defs/python/tests/py_executable_base_tests.bzl272
-rw-r--r--tools/build_defs/python/tests/py_info_subject.bzl95
-rw-r--r--tools/build_defs/python/tests/py_library/BUILD.bazel18
-rw-r--r--tools/build_defs/python/tests/py_library/py_library_tests.bzl148
-rw-r--r--tools/build_defs/python/tests/py_test/BUILD.bazel18
-rw-r--r--tools/build_defs/python/tests/py_test/py_test_tests.bzl107
-rw-r--r--tools/build_defs/python/tests/py_wheel/BUILD.bazel18
-rw-r--r--tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl39
-rw-r--r--tools/build_defs/python/tests/util.bzl78
-rw-r--r--tools/publish/BUILD.bazel7
-rw-r--r--tools/publish/README.md6
-rw-r--r--tools/publish/requirements.in1
-rw-r--r--tools/publish/requirements.txt297
-rw-r--r--tools/publish/requirements_darwin.txt192
-rw-r--r--tools/publish/requirements_windows.txt196
-rwxr-xr-xtools/update_coverage_deps.py248
-rw-r--r--tools/wheelmaker.py436
27 files changed, 2703 insertions, 0 deletions
diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel
new file mode 100644
index 0000000..fd951d9
--- /dev/null
+++ b/tools/BUILD.bazel
@@ -0,0 +1,33 @@
+# Copyright 2017 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+load("//python:defs.bzl", "py_binary")
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# Implementation detail of py_wheel rule.
+py_binary(
+ name = "wheelmaker",
+ srcs = ["wheelmaker.py"],
+)
+
+filegroup(
+ name = "distribution",
+ srcs = [
+ "BUILD.bazel",
+ "wheelmaker.py",
+ ],
+ visibility = ["//:__pkg__"],
+)
diff --git a/tools/bazel_integration_test/BUILD.bazel b/tools/bazel_integration_test/BUILD.bazel
new file mode 100644
index 0000000..10566c4
--- /dev/null
+++ b/tools/bazel_integration_test/BUILD.bazel
@@ -0,0 +1 @@
+exports_files(["test_runner.py"])
diff --git a/tools/bazel_integration_test/bazel_integration_test.bzl b/tools/bazel_integration_test/bazel_integration_test.bzl
new file mode 100644
index 0000000..c016551
--- /dev/null
+++ b/tools/bazel_integration_test/bazel_integration_test.bzl
@@ -0,0 +1,134 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"Define a rule for running bazel test under Bazel"
+
+load("//:version.bzl", "SUPPORTED_BAZEL_VERSIONS", "bazel_version_to_binary_label")
+load("//python:defs.bzl", "py_test")
+
+BAZEL_BINARY = bazel_version_to_binary_label(SUPPORTED_BAZEL_VERSIONS[0])
+
+_ATTRS = {
+ "bazel_binary": attr.label(
+ default = BAZEL_BINARY,
+ doc = """The bazel binary files to test against.
+
+It is assumed by the test runner that the bazel binary is found at label_workspace/bazel (wksp/bazel.exe on Windows)""",
+ ),
+ "bazel_commands": attr.string_list(
+ default = ["info", "test --test_output=errors ..."],
+ doc = """The list of bazel commands to run.
+
+Note that if a command contains a bare `--` argument, the --test_arg passed to Bazel will appear before it.
+""",
+ ),
+ "bzlmod": attr.bool(
+ default = False,
+ doc = """Whether the test uses bzlmod.""",
+ ),
+ "workspace_files": attr.label(
+ doc = """A filegroup of all files in the workspace-under-test necessary to run the test.""",
+ ),
+}
+
+def _config_impl(ctx):
+ if len(SUPPORTED_BAZEL_VERSIONS) > 1:
+ fail("""
+ bazel_integration_test doesn't support multiple Bazel versions to test against yet.
+ """)
+ if len(ctx.files.workspace_files) == 0:
+ fail("""
+No files were found to run under integration testing. See comment in /.bazelrc.
+You probably need to run
+ tools/bazel_integration_test/update_deleted_packages.sh
+""")
+
+ # Serialize configuration file for test runner
+ config = ctx.actions.declare_file("%s.json" % ctx.attr.name)
+ ctx.actions.write(
+ output = config,
+ content = """
+{{
+ "workspaceRoot": "{TMPL_workspace_root}",
+ "bazelBinaryWorkspace": "{TMPL_bazel_binary_workspace}",
+ "bazelCommands": [ {TMPL_bazel_commands} ],
+ "bzlmod": {TMPL_bzlmod}
+}}
+""".format(
+ TMPL_workspace_root = ctx.files.workspace_files[0].dirname,
+ TMPL_bazel_binary_workspace = ctx.attr.bazel_binary.label.workspace_name,
+ TMPL_bazel_commands = ", ".join(["\"%s\"" % s for s in ctx.attr.bazel_commands]),
+ TMPL_bzlmod = str(ctx.attr.bzlmod).lower(),
+ ),
+ )
+
+ return [DefaultInfo(
+ files = depset([config]),
+ runfiles = ctx.runfiles(files = [config]),
+ )]
+
+_config = rule(
+ implementation = _config_impl,
+ doc = "Configures an integration test that runs a specified version of bazel against an external workspace.",
+ attrs = _ATTRS,
+)
+
+def bazel_integration_test(name, override_bazel_version = None, bzlmod = False, dirname = None, **kwargs):
+ """Wrapper macro to set default srcs and run a py_test with config
+
+ Args:
+ name: name of the resulting py_test
+ override_bazel_version: bazel version to use in test
+ bzlmod: whether the test uses bzlmod
+ dirname: the directory name of the test. Defaults to value of `name` after trimming the `_example` suffix.
+ **kwargs: additional attributes like timeout and visibility
+ """
+
+ # By default, we assume sources for "pip_example" are in examples/pip/**/*
+ dirname = dirname or name[:-len("_example")]
+ native.filegroup(
+ name = "_%s_sources" % name,
+ srcs = native.glob(
+ ["%s/**/*" % dirname],
+ exclude = ["%s/bazel-*/**" % dirname],
+ ),
+ )
+ workspace_files = kwargs.pop("workspace_files", "_%s_sources" % name)
+
+ bazel_binary = BAZEL_BINARY if not override_bazel_version else bazel_version_to_binary_label(override_bazel_version)
+ _config(
+ name = "_%s_config" % name,
+ workspace_files = workspace_files,
+ bazel_binary = bazel_binary,
+ bzlmod = bzlmod,
+ )
+
+ tags = kwargs.pop("tags", [])
+ tags.append("integration-test")
+
+ py_test(
+ name = name,
+ srcs = [Label("//tools/bazel_integration_test:test_runner.py")],
+ main = "test_runner.py",
+ args = [native.package_name() + "/_%s_config.json" % name],
+ deps = [Label("//python/runfiles")],
+ data = [
+ bazel_binary,
+ "//:distribution",
+ "_%s_config" % name,
+ workspace_files,
+ ],
+ tags = tags,
+ **kwargs
+ )
diff --git a/tools/bazel_integration_test/test_runner.py b/tools/bazel_integration_test/test_runner.py
new file mode 100644
index 0000000..3940e87
--- /dev/null
+++ b/tools/bazel_integration_test/test_runner.py
@@ -0,0 +1,111 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import platform
+import re
+import shutil
+import sys
+import tempfile
+import textwrap
+from pathlib import Path
+from subprocess import Popen
+
+from rules_python.python.runfiles import runfiles
+
+r = runfiles.Create()
+
+
+def main(conf_file):
+ with open(conf_file) as j:
+ config = json.load(j)
+
+ isWindows = platform.system() == "Windows"
+ bazelBinary = r.Rlocation(
+ os.path.join(
+ config["bazelBinaryWorkspace"], "bazel.exe" if isWindows else "bazel"
+ )
+ )
+
+ workspacePath = config["workspaceRoot"]
+ # Canonicalize bazel external/some_repo/foo
+ if workspacePath.startswith("external/"):
+ workspacePath = ".." + workspacePath[len("external") :]
+
+ with tempfile.TemporaryDirectory(dir=os.environ["TEST_TMPDIR"]) as tmp_homedir:
+ home_bazel_rc = Path(tmp_homedir) / ".bazelrc"
+ home_bazel_rc.write_text(
+ textwrap.dedent(
+ """\
+ startup --max_idle_secs=1
+ common --announce_rc
+ """
+ )
+ )
+
+ with tempfile.TemporaryDirectory(dir=os.environ["TEST_TMPDIR"]) as tmpdir:
+ workdir = os.path.join(tmpdir, "wksp")
+ print("copying workspace under test %s to %s" % (workspacePath, workdir))
+ shutil.copytree(workspacePath, workdir)
+
+ for command in config["bazelCommands"]:
+ bazel_args = command.split(" ")
+ bazel_args.append(
+ "--override_repository=rules_python=%s/rules_python"
+ % os.environ["TEST_SRCDIR"]
+ )
+ bazel_args.append(
+ "--override_repository=rules_python_gazelle_plugin=%s/rules_python_gazelle_plugin"
+ % os.environ["TEST_SRCDIR"]
+ )
+
+ # TODO: --override_module isn't supported in the current BAZEL_VERSION (5.2.0)
+ # This condition and attribute can be removed when bazel is updated for
+ # the rest of rules_python.
+ if config["bzlmod"]:
+ bazel_args.append(
+ "--override_module=rules_python=%s/rules_python"
+ % os.environ["TEST_SRCDIR"]
+ )
+ bazel_args.append("--enable_bzlmod")
+
+ # Bazel's wrapper script needs this or you get
+ # 2020/07/13 21:58:11 could not get the user's cache directory: $HOME is not defined
+ os.environ["HOME"] = str(tmp_homedir)
+
+ bazel_args.insert(0, bazelBinary)
+ bazel_process = Popen(bazel_args, cwd=workdir)
+ bazel_process.wait()
+ error = bazel_process.returncode != 0
+
+ if platform.system() == "Windows":
+ # Cleanup any bazel files
+ bazel_process = Popen([bazelBinary, "clean"], cwd=workdir)
+ bazel_process.wait()
+ error |= bazel_process.returncode != 0
+
+ # Shutdown the bazel instance to avoid issues cleaning up the workspace
+ bazel_process = Popen([bazelBinary, "shutdown"], cwd=workdir)
+ bazel_process.wait()
+ error |= bazel_process.returncode != 0
+
+ if error != 0:
+ # Test failure in Bazel is exit 3
+ # https://github.com/bazelbuild/bazel/blob/486206012a664ecb20bdb196a681efc9a9825049/src/main/java/com/google/devtools/build/lib/util/ExitCode.java#L44
+ sys.exit(3)
+
+
+if __name__ == "__main__":
+ main(sys.argv[1])
diff --git a/tools/bazel_integration_test/update_deleted_packages.sh b/tools/bazel_integration_test/update_deleted_packages.sh
new file mode 100755
index 0000000..54db026
--- /dev/null
+++ b/tools/bazel_integration_test/update_deleted_packages.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# For integration tests, we want to be able to glob() up the sources inside a nested package
+# See explanation in .bazelrc
+#
+# This script ensures that we only delete subtrees that have something a file
+# signifying a new bazel workspace, whether it be bzlmod or classic. Generic
+# algorithm:
+# 1. Get all directories where a WORKSPACE or MODULE.bazel exists.
+# 2. For each of the directories, get all directories that contains a BUILD.bazel file.
+# 3. Sort and remove duplicates.
+
+set -euxo pipefail
+
+DIR="$(dirname $0)/../.."
+cd $DIR
+
+# The sed -i.bak pattern is compatible between macos and linux
+sed -i.bak "/^[^#].*--deleted_packages/s#=.*#=$(\
+ find examples/*/* tests/*/* \( -name WORKSPACE -or -name MODULE.bazel \) |
+ xargs -n 1 dirname |
+ xargs -n 1 -I{} find {} \( -name BUILD -or -name BUILD.bazel \) |
+ xargs -n 1 dirname |
+ sort -u |
+ paste -sd, -\
+)#" $DIR/.bazelrc && rm .bazelrc.bak
diff --git a/tools/build_defs/python/BUILD.bazel b/tools/build_defs/python/BUILD.bazel
new file mode 100644
index 0000000..aa21042
--- /dev/null
+++ b/tools/build_defs/python/BUILD.bazel
@@ -0,0 +1,13 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tools/build_defs/python/tests/BUILD.bazel b/tools/build_defs/python/tests/BUILD.bazel
new file mode 100644
index 0000000..e271850
--- /dev/null
+++ b/tools/build_defs/python/tests/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+platform(
+ name = "mac",
+ constraint_values = [
+ "@platforms//os:macos",
+ ],
+)
+
+platform(
+ name = "linux",
+ constraint_values = [
+ "@platforms//os:linux",
+ ],
+)
diff --git a/tools/build_defs/python/tests/base_tests.bzl b/tools/build_defs/python/tests/base_tests.bzl
new file mode 100644
index 0000000..467611f
--- /dev/null
+++ b/tools/build_defs/python/tests/base_tests.bzl
@@ -0,0 +1,124 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests common to py_test, py_binary, and py_library rules."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:truth.bzl", "matching")
+load("@rules_testing//lib:util.bzl", "PREVENT_IMPLICIT_BUILDING_TAGS", rt_util = "util")
+load("//python:defs.bzl", "PyInfo")
+load("//tools/build_defs/python/tests:py_info_subject.bzl", "py_info_subject")
+load("//tools/build_defs/python/tests:util.bzl", pt_util = "util")
+
+_tests = []
+
+def _produces_py_info_impl(ctx):
+ return [PyInfo(transitive_sources = depset(ctx.files.srcs))]
+
+_produces_py_info = rule(
+ implementation = _produces_py_info_impl,
+ attrs = {"srcs": attr.label_list(allow_files = True)},
+)
+
+def _test_consumes_provider(name, config):
+ rt_util.helper_target(
+ config.base_test_rule,
+ name = name + "_subject",
+ deps = [name + "_produces_py_info"],
+ )
+ rt_util.helper_target(
+ _produces_py_info,
+ name = name + "_produces_py_info",
+ srcs = [rt_util.empty_file(name + "_produce.py")],
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_consumes_provider_impl,
+ )
+
+def _test_consumes_provider_impl(env, target):
+ env.expect.that_target(target).provider(
+ PyInfo,
+ factory = py_info_subject,
+ ).transitive_sources().contains("{package}/{test_name}_produce.py")
+
+_tests.append(_test_consumes_provider)
+
+def _test_requires_provider(name, config):
+ rt_util.helper_target(
+ config.base_test_rule,
+ name = name + "_subject",
+ deps = [name + "_nopyinfo"],
+ )
+ rt_util.helper_target(
+ native.filegroup,
+ name = name + "_nopyinfo",
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_requires_provider_impl,
+ expect_failure = True,
+ )
+
+def _test_requires_provider_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("mandatory*PyInfo"),
+ )
+
+_tests.append(_test_requires_provider)
+
+def _test_data_sets_uses_shared_library(name, config):
+ rt_util.helper_target(
+ config.base_test_rule,
+ name = name + "_subject",
+ data = [rt_util.empty_file(name + "_dso.so")],
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_data_sets_uses_shared_library_impl,
+ )
+
+def _test_data_sets_uses_shared_library_impl(env, target):
+ env.expect.that_target(target).provider(
+ PyInfo,
+ factory = py_info_subject,
+ ).uses_shared_libraries().equals(True)
+
+_tests.append(_test_data_sets_uses_shared_library)
+
+def _test_tags_can_be_tuple(name, config):
+ # We don't use a helper because we want to ensure that value passed is
+ # a tuple.
+ config.base_test_rule(
+ name = name + "_subject",
+ tags = ("one", "two") + tuple(PREVENT_IMPLICIT_BUILDING_TAGS),
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_tags_can_be_tuple_impl,
+ )
+
+def _test_tags_can_be_tuple_impl(env, target):
+ env.expect.that_target(target).tags().contains_at_least([
+ "one",
+ "two",
+ ])
+
+_tests.append(_test_tags_can_be_tuple)
+
+def create_base_tests(config):
+ return pt_util.create_tests(_tests, config = config)
diff --git a/tools/build_defs/python/tests/py_binary/BUILD.bazel b/tools/build_defs/python/tests/py_binary/BUILD.bazel
new file mode 100644
index 0000000..17a6690
--- /dev/null
+++ b/tools/build_defs/python/tests/py_binary/BUILD.bazel
@@ -0,0 +1,17 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load(":py_binary_tests.bzl", "py_binary_test_suite")
+
+py_binary_test_suite(name = "py_binary_tests")
diff --git a/tools/build_defs/python/tests/py_binary/py_binary_tests.bzl b/tools/build_defs/python/tests/py_binary/py_binary_tests.bzl
new file mode 100644
index 0000000..8d32632
--- /dev/null
+++ b/tools/build_defs/python/tests/py_binary/py_binary_tests.bzl
@@ -0,0 +1,28 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for py_binary."""
+
+load("//python:defs.bzl", "py_binary")
+load(
+ "//tools/build_defs/python/tests:py_executable_base_tests.bzl",
+ "create_executable_tests",
+)
+
+def py_binary_test_suite(name):
+ config = struct(rule = py_binary)
+
+ native.test_suite(
+ name = name,
+ tests = create_executable_tests(config),
+ )
diff --git a/tools/build_defs/python/tests/py_executable_base_tests.bzl b/tools/build_defs/python/tests/py_executable_base_tests.bzl
new file mode 100644
index 0000000..c66ea11
--- /dev/null
+++ b/tools/build_defs/python/tests/py_executable_base_tests.bzl
@@ -0,0 +1,272 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests common to py_binary and py_test (executable rules)."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:truth.bzl", "matching")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load("//tools/build_defs/python/tests:base_tests.bzl", "create_base_tests")
+load("//tools/build_defs/python/tests:util.bzl", "WINDOWS_ATTR", pt_util = "util")
+
+_tests = []
+
+def _test_executable_in_runfiles(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = [name + "_subject.py"],
+ )
+ analysis_test(
+ name = name,
+ impl = _test_executable_in_runfiles_impl,
+ target = name + "_subject",
+ attrs = WINDOWS_ATTR,
+ )
+
+_tests.append(_test_executable_in_runfiles)
+
+def _test_executable_in_runfiles_impl(env, target):
+ if pt_util.is_windows(env):
+ exe = ".exe"
+ else:
+ exe = ""
+
+ env.expect.that_target(target).runfiles().contains_at_least([
+ "{workspace}/{package}/{test_name}_subject" + exe,
+ ])
+
+def _test_default_main_can_be_generated(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = [rt_util.empty_file(name + "_subject.py")],
+ )
+ analysis_test(
+ name = name,
+ impl = _test_default_main_can_be_generated_impl,
+ target = name + "_subject",
+ )
+
+_tests.append(_test_default_main_can_be_generated)
+
+def _test_default_main_can_be_generated_impl(env, target):
+ env.expect.that_target(target).default_outputs().contains(
+ "{package}/{test_name}_subject.py",
+ )
+
+def _test_default_main_can_have_multiple_path_segments(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "/subject",
+ srcs = [name + "/subject.py"],
+ )
+ analysis_test(
+ name = name,
+ impl = _test_default_main_can_have_multiple_path_segments_impl,
+ target = name + "/subject",
+ )
+
+_tests.append(_test_default_main_can_have_multiple_path_segments)
+
+def _test_default_main_can_have_multiple_path_segments_impl(env, target):
+ env.expect.that_target(target).default_outputs().contains(
+ "{package}/{test_name}/subject.py",
+ )
+
+def _test_default_main_must_be_in_srcs(name, config):
+ # Bazel 5 will crash with a Java stacktrace when the native Python
+ # rules have an error.
+ if not pt_util.is_bazel_6_or_higher():
+ rt_util.skip_test(name = name)
+ return
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = ["other.py"],
+ )
+ analysis_test(
+ name = name,
+ impl = _test_default_main_must_be_in_srcs_impl,
+ target = name + "_subject",
+ expect_failure = True,
+ )
+
+_tests.append(_test_default_main_must_be_in_srcs)
+
+def _test_default_main_must_be_in_srcs_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("default*does not appear in srcs"),
+ )
+
+def _test_default_main_cannot_be_ambiguous(name, config):
+ # Bazel 5 will crash with a Java stacktrace when the native Python
+ # rules have an error.
+ if not pt_util.is_bazel_6_or_higher():
+ rt_util.skip_test(name = name)
+ return
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = [name + "_subject.py", "other/{}_subject.py".format(name)],
+ )
+ analysis_test(
+ name = name,
+ impl = _test_default_main_cannot_be_ambiguous_impl,
+ target = name + "_subject",
+ expect_failure = True,
+ )
+
+_tests.append(_test_default_main_cannot_be_ambiguous)
+
+def _test_default_main_cannot_be_ambiguous_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("default main*matches multiple files"),
+ )
+
+def _test_explicit_main(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = ["custom.py"],
+ main = "custom.py",
+ )
+ analysis_test(
+ name = name,
+ impl = _test_explicit_main_impl,
+ target = name + "_subject",
+ )
+
+_tests.append(_test_explicit_main)
+
+def _test_explicit_main_impl(env, target):
+ # There isn't a direct way to ask what main file was selected, so we
+ # rely on it being in the default outputs.
+ env.expect.that_target(target).default_outputs().contains(
+ "{package}/custom.py",
+ )
+
+def _test_explicit_main_cannot_be_ambiguous(name, config):
+ # Bazel 5 will crash with a Java stacktrace when the native Python
+ # rules have an error.
+ if not pt_util.is_bazel_6_or_higher():
+ rt_util.skip_test(name = name)
+ return
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = ["x/foo.py", "y/foo.py"],
+ main = "foo.py",
+ )
+ analysis_test(
+ name = name,
+ impl = _test_explicit_main_cannot_be_ambiguous_impl,
+ target = name + "_subject",
+ expect_failure = True,
+ )
+
+_tests.append(_test_explicit_main_cannot_be_ambiguous)
+
+def _test_explicit_main_cannot_be_ambiguous_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("foo.py*matches multiple"),
+ )
+
+def _test_files_to_build(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = [name + "_subject.py"],
+ )
+ analysis_test(
+ name = name,
+ impl = _test_files_to_build_impl,
+ target = name + "_subject",
+ attrs = WINDOWS_ATTR,
+ )
+
+_tests.append(_test_files_to_build)
+
+def _test_files_to_build_impl(env, target):
+ default_outputs = env.expect.that_target(target).default_outputs()
+ if pt_util.is_windows(env):
+ default_outputs.contains("{package}/{test_name}_subject.exe")
+ else:
+ default_outputs.contains_exactly([
+ "{package}/{test_name}_subject",
+ "{package}/{test_name}_subject.py",
+ ])
+
+def _test_name_cannot_end_in_py(name, config):
+ # Bazel 5 will crash with a Java stacktrace when the native Python
+ # rules have an error.
+ if not pt_util.is_bazel_6_or_higher():
+ rt_util.skip_test(name = name)
+ return
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject.py",
+ srcs = ["main.py"],
+ )
+ analysis_test(
+ name = name,
+ impl = _test_name_cannot_end_in_py_impl,
+ target = name + "_subject.py",
+ expect_failure = True,
+ )
+
+_tests.append(_test_name_cannot_end_in_py)
+
+def _test_name_cannot_end_in_py_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("name must not end in*.py"),
+ )
+
+# Can't test this -- mandatory validation happens before analysis test
+# can intercept it
+# TODO(#1069): Once re-implemented in Starlark, modify rule logic to make this
+# testable.
+# def _test_srcs_is_mandatory(name, config):
+# rt_util.helper_target(
+# config.rule,
+# name = name + "_subject",
+# )
+# analysis_test(
+# name = name,
+# impl = _test_srcs_is_mandatory,
+# target = name + "_subject",
+# expect_failure = True,
+# )
+#
+# _tests.append(_test_srcs_is_mandatory)
+#
+# def _test_srcs_is_mandatory_impl(env, target):
+# env.expect.that_target(target).failures().contains_predicate(
+# matching.str_matches("mandatory*srcs"),
+# )
+
+# =====
+# You were gonna add a test at the end, weren't you?
+# Nope. Please keep them sorted; put it in its alphabetical location.
+# Here's the alphabet so you don't have to sing that song in your head:
+# A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
+# =====
+
+def create_executable_tests(config):
+ def _executable_with_srcs_wrapper(name, **kwargs):
+ if not kwargs.get("srcs"):
+ kwargs["srcs"] = [name + ".py"]
+ config.rule(name = name, **kwargs)
+
+ config = pt_util.struct_with(config, base_test_rule = _executable_with_srcs_wrapper)
+ return pt_util.create_tests(_tests, config = config) + create_base_tests(config = config)
diff --git a/tools/build_defs/python/tests/py_info_subject.bzl b/tools/build_defs/python/tests/py_info_subject.bzl
new file mode 100644
index 0000000..20185e5
--- /dev/null
+++ b/tools/build_defs/python/tests/py_info_subject.bzl
@@ -0,0 +1,95 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""PyInfo testing subject."""
+
+load("@rules_testing//lib:truth.bzl", "subjects")
+
+def py_info_subject(info, *, meta):
+ """Creates a new `PyInfoSubject` for a PyInfo provider instance.
+
+ Method: PyInfoSubject.new
+
+ Args:
+ info: The PyInfo object
+ meta: ExpectMeta object.
+
+ Returns:
+ A `PyInfoSubject` struct
+ """
+
+ # buildifier: disable=uninitialized
+ public = struct(
+ # go/keep-sorted start
+ has_py2_only_sources = lambda *a, **k: _py_info_subject_has_py2_only_sources(self, *a, **k),
+ has_py3_only_sources = lambda *a, **k: _py_info_subject_has_py3_only_sources(self, *a, **k),
+ imports = lambda *a, **k: _py_info_subject_imports(self, *a, **k),
+ transitive_sources = lambda *a, **k: _py_info_subject_transitive_sources(self, *a, **k),
+ uses_shared_libraries = lambda *a, **k: _py_info_subject_uses_shared_libraries(self, *a, **k),
+ # go/keep-sorted end
+ )
+ self = struct(
+ actual = info,
+ meta = meta,
+ )
+ return public
+
+def _py_info_subject_has_py2_only_sources(self):
+ """Returns a `BoolSubject` for the `has_py2_only_sources` attribute.
+
+ Method: PyInfoSubject.has_py2_only_sources
+ """
+ return subjects.bool(
+ self.actual.has_py2_only_sources,
+ meta = self.meta.derive("has_py2_only_sources()"),
+ )
+
+def _py_info_subject_has_py3_only_sources(self):
+ """Returns a `BoolSubject` for the `has_py3_only_sources` attribute.
+
+ Method: PyInfoSubject.has_py3_only_sources
+ """
+ return subjects.bool(
+ self.actual.has_py3_only_sources,
+ meta = self.meta.derive("has_py3_only_sources()"),
+ )
+
+def _py_info_subject_imports(self):
+ """Returns a `CollectionSubject` for the `imports` attribute.
+
+ Method: PyInfoSubject.imports
+ """
+ return subjects.collection(
+ self.actual.imports,
+ meta = self.meta.derive("imports()"),
+ )
+
+def _py_info_subject_transitive_sources(self):
+ """Returns a `DepsetFileSubject` for the `transitive_sources` attribute.
+
+ Method: PyInfoSubject.transitive_sources
+ """
+ return subjects.depset_file(
+ self.actual.transitive_sources,
+ meta = self.meta.derive("transitive_sources()"),
+ )
+
+def _py_info_subject_uses_shared_libraries(self):
+ """Returns a `BoolSubject` for the `uses_shared_libraries` attribute.
+
+ Method: PyInfoSubject.uses_shared_libraries
+ """
+ return subjects.bool(
+ self.actual.uses_shared_libraries,
+ meta = self.meta.derive("uses_shared_libraries()"),
+ )
diff --git a/tools/build_defs/python/tests/py_library/BUILD.bazel b/tools/build_defs/python/tests/py_library/BUILD.bazel
new file mode 100644
index 0000000..9de414b
--- /dev/null
+++ b/tools/build_defs/python/tests/py_library/BUILD.bazel
@@ -0,0 +1,18 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for py_library."""
+
+load(":py_library_tests.bzl", "py_library_test_suite")
+
+py_library_test_suite(name = "py_library_tests")
diff --git a/tools/build_defs/python/tests/py_library/py_library_tests.bzl b/tools/build_defs/python/tests/py_library/py_library_tests.bzl
new file mode 100644
index 0000000..1fcb0c1
--- /dev/null
+++ b/tools/build_defs/python/tests/py_library/py_library_tests.bzl
@@ -0,0 +1,148 @@
+"""Test for py_library."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:truth.bzl", "matching")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load("//python:defs.bzl", "PyRuntimeInfo", "py_library")
+load("//tools/build_defs/python/tests:base_tests.bzl", "create_base_tests")
+load("//tools/build_defs/python/tests:util.bzl", pt_util = "util")
+
+_tests = []
+
+def _test_py_runtime_info_not_present(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = ["lib.py"],
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_py_runtime_info_not_present_impl,
+ )
+
+def _test_py_runtime_info_not_present_impl(env, target):
+ env.expect.that_bool(PyRuntimeInfo in target).equals(False)
+
+_tests.append(_test_py_runtime_info_not_present)
+
+def _test_files_to_build(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = ["lib.py"],
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_files_to_build_impl,
+ )
+
+def _test_files_to_build_impl(env, target):
+ env.expect.that_target(target).default_outputs().contains_exactly([
+ "{package}/lib.py",
+ ])
+
+_tests.append(_test_files_to_build)
+
+def _test_srcs_can_contain_rule_generating_py_and_nonpy_files(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = ["lib.py", name + "_gensrcs"],
+ )
+ rt_util.helper_target(
+ native.genrule,
+ name = name + "_gensrcs",
+ cmd = "touch $(OUTS)",
+ outs = [name + "_gen.py", name + "_gen.cc"],
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_srcs_can_contain_rule_generating_py_and_nonpy_files_impl,
+ )
+
+def _test_srcs_can_contain_rule_generating_py_and_nonpy_files_impl(env, target):
+ env.expect.that_target(target).default_outputs().contains_exactly([
+ "{package}/{test_name}_gen.py",
+ "{package}/lib.py",
+ ])
+
+_tests.append(_test_srcs_can_contain_rule_generating_py_and_nonpy_files)
+
+def _test_srcs_generating_no_py_files_is_error(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = [name + "_gen"],
+ )
+ rt_util.helper_target(
+ native.genrule,
+ name = name + "_gen",
+ cmd = "touch $(OUTS)",
+ outs = [name + "_gen.cc"],
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_srcs_generating_no_py_files_is_error_impl,
+ expect_failure = True,
+ )
+
+def _test_srcs_generating_no_py_files_is_error_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("does not produce*srcs files"),
+ )
+
+_tests.append(_test_srcs_generating_no_py_files_is_error)
+
+def _test_files_to_compile(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = ["lib1.py"],
+ deps = [name + "_lib2"],
+ )
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_lib2",
+ srcs = ["lib2.py"],
+ deps = [name + "_lib3"],
+ )
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_lib3",
+ srcs = ["lib3.py"],
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_files_to_compile_impl,
+ )
+
+def _test_files_to_compile_impl(env, target):
+ target = env.expect.that_target(target)
+ target.output_group(
+ "compilation_prerequisites_INTERNAL_",
+ ).contains_exactly([
+ "{package}/lib1.py",
+ "{package}/lib2.py",
+ "{package}/lib3.py",
+ ])
+ target.output_group(
+ "compilation_outputs",
+ ).contains_exactly([
+ "{package}/lib1.py",
+ "{package}/lib2.py",
+ "{package}/lib3.py",
+ ])
+
+_tests.append(_test_files_to_compile)
+
+def py_library_test_suite(name):
+ config = struct(rule = py_library, base_test_rule = py_library)
+ native.test_suite(
+ name = name,
+ tests = pt_util.create_tests(_tests, config = config) + create_base_tests(config),
+ )
diff --git a/tools/build_defs/python/tests/py_test/BUILD.bazel b/tools/build_defs/python/tests/py_test/BUILD.bazel
new file mode 100644
index 0000000..2dc0e5b
--- /dev/null
+++ b/tools/build_defs/python/tests/py_test/BUILD.bazel
@@ -0,0 +1,18 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for py_test."""
+
+load(":py_test_tests.bzl", "py_test_test_suite")
+
+py_test_test_suite(name = "py_test_tests")
diff --git a/tools/build_defs/python/tests/py_test/py_test_tests.bzl b/tools/build_defs/python/tests/py_test/py_test_tests.bzl
new file mode 100644
index 0000000..1ecb252
--- /dev/null
+++ b/tools/build_defs/python/tests/py_test/py_test_tests.bzl
@@ -0,0 +1,107 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for py_test."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load("//python:defs.bzl", "py_test")
+load(
+ "//tools/build_defs/python/tests:py_executable_base_tests.bzl",
+ "create_executable_tests",
+)
+load("//tools/build_defs/python/tests:util.bzl", pt_util = "util")
+
+# Explicit Label() calls are required so that it resolves in @rules_python context instead of
+# @rules_testing context.
+_FAKE_CC_TOOLCHAIN = Label("//tests/cc:cc_toolchain_suite")
+_FAKE_CC_TOOLCHAINS = [str(Label("//tests/cc:all"))]
+_PLATFORM_MAC = Label("//tools/build_defs/python/tests:mac")
+_PLATFORM_LINUX = Label("//tools/build_defs/python/tests:linux")
+
+_tests = []
+
+def _test_mac_requires_darwin_for_execution(name, config):
+ # Bazel 5.4 has a bug where every access of testing.ExecutionInfo is
+ # a different object that isn't equal to any other, which prevents
+ # rules_testing from detecting it properly and fails with an error.
+ # This is fixed in Bazel 6+.
+ if not pt_util.is_bazel_6_or_higher():
+ rt_util.skip_test(name = name)
+ return
+
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = [name + "_subject.py"],
+ )
+ analysis_test(
+ name = name,
+ impl = _test_mac_requires_darwin_for_execution_impl,
+ target = name + "_subject",
+ config_settings = {
+ "//command_line_option:cpu": "darwin_x86_64",
+ "//command_line_option:crosstool_top": _FAKE_CC_TOOLCHAIN,
+ "//command_line_option:extra_toolchains": _FAKE_CC_TOOLCHAINS,
+ "//command_line_option:platforms": [_PLATFORM_MAC],
+ },
+ )
+
+def _test_mac_requires_darwin_for_execution_impl(env, target):
+ env.expect.that_target(target).provider(
+ testing.ExecutionInfo,
+ ).requirements().keys().contains("requires-darwin")
+
+_tests.append(_test_mac_requires_darwin_for_execution)
+
+def _test_non_mac_doesnt_require_darwin_for_execution(name, config):
+ # Bazel 5.4 has a bug where every access of testing.ExecutionInfo is
+ # a different object that isn't equal to any other, which prevents
+ # rules_testing from detecting it properly and fails with an error.
+ # This is fixed in Bazel 6+.
+ if not pt_util.is_bazel_6_or_higher():
+ rt_util.skip_test(name = name)
+ return
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_subject",
+ srcs = [name + "_subject.py"],
+ )
+ analysis_test(
+ name = name,
+ impl = _test_non_mac_doesnt_require_darwin_for_execution_impl,
+ target = name + "_subject",
+ config_settings = {
+ "//command_line_option:cpu": "k8",
+ "//command_line_option:crosstool_top": _FAKE_CC_TOOLCHAIN,
+ "//command_line_option:extra_toolchains": _FAKE_CC_TOOLCHAINS,
+ "//command_line_option:platforms": [_PLATFORM_LINUX],
+ },
+ )
+
+def _test_non_mac_doesnt_require_darwin_for_execution_impl(env, target):
+ # Non-mac builds don't have the provider at all.
+ if testing.ExecutionInfo not in target:
+ return
+ env.expect.that_target(target).provider(
+ testing.ExecutionInfo,
+ ).requirements().keys().not_contains("requires-darwin")
+
+_tests.append(_test_non_mac_doesnt_require_darwin_for_execution)
+
+def py_test_test_suite(name):
+ config = struct(rule = py_test)
+ native.test_suite(
+ name = name,
+ tests = pt_util.create_tests(_tests, config = config) + create_executable_tests(config),
+ )
diff --git a/tools/build_defs/python/tests/py_wheel/BUILD.bazel b/tools/build_defs/python/tests/py_wheel/BUILD.bazel
new file mode 100644
index 0000000..d925bb9
--- /dev/null
+++ b/tools/build_defs/python/tests/py_wheel/BUILD.bazel
@@ -0,0 +1,18 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for py_wheel."""
+
+load(":py_wheel_tests.bzl", "py_wheel_test_suite")
+
+py_wheel_test_suite(name = "py_wheel_tests")
diff --git a/tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl b/tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl
new file mode 100644
index 0000000..4408592
--- /dev/null
+++ b/tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl
@@ -0,0 +1,39 @@
+"""Test for py_wheel."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:truth.bzl", "matching")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load("//python:packaging.bzl", "py_wheel")
+load("//tools/build_defs/python/tests:util.bzl", pt_util = "util")
+
+_tests = []
+
+def _test_too_long_project_url_label(name, config):
+ rt_util.helper_target(
+ config.rule,
+ name = name + "_wheel",
+ distribution = name + "_wheel",
+ python_tag = "py3",
+ version = "0.0.1",
+ project_urls = {"This is a label whose length is above the limit!": "www.example.com"},
+ )
+ analysis_test(
+ name = name,
+ target = name + "_wheel",
+ impl = _test_too_long_project_url_label_impl,
+ expect_failure = True,
+ )
+
+def _test_too_long_project_url_label_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("in `project_urls` is too long"),
+ )
+
+_tests.append(_test_too_long_project_url_label)
+
+def py_wheel_test_suite(name):
+ config = struct(rule = py_wheel, base_test_rule = py_wheel)
+ native.test_suite(
+ name = name,
+ tests = pt_util.create_tests(_tests, config = config),
+ )
diff --git a/tools/build_defs/python/tests/util.bzl b/tools/build_defs/python/tests/util.bzl
new file mode 100644
index 0000000..9b386ca
--- /dev/null
+++ b/tools/build_defs/python/tests/util.bzl
@@ -0,0 +1,78 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Helpers and utilities multiple tests re-use."""
+
+load("@bazel_skylib//lib:structs.bzl", "structs")
+
+# Use this with is_windows()
+WINDOWS_ATTR = {"windows": attr.label(default = "@platforms//os:windows")}
+
+def _create_tests(tests, **kwargs):
+ test_names = []
+ for func in tests:
+ test_name = _test_name_from_function(func)
+ func(name = test_name, **kwargs)
+ test_names.append(test_name)
+ return test_names
+
+def _test_name_from_function(func):
+ """Derives the name of the given rule implementation function.
+
+ Args:
+ func: the function whose name to extract
+
+ Returns:
+ The name of the given function. Note it will have leading and trailing
+ "_" stripped -- this allows passing a private function and having the
+ name of the test not start with "_".
+ """
+
+ # Starlark currently stringifies a function as "<function NAME>", so we use
+ # that knowledge to parse the "NAME" portion out.
+ # NOTE: This is relying on an implementation detail of Bazel
+ func_name = str(func)
+ func_name = func_name.partition("<function ")[-1]
+ func_name = func_name.rpartition(">")[0]
+ func_name = func_name.partition(" ")[0]
+ return func_name.strip("_")
+
+def _struct_with(s, **kwargs):
+ struct_dict = structs.to_dict(s)
+ struct_dict.update(kwargs)
+ return struct(**struct_dict)
+
+def _is_bazel_6_or_higher():
+ # Bazel 5.4 has a bug where every access of testing.ExecutionInfo is a
+ # different object that isn't equal to any other. This is fixed in bazel 6+.
+ return testing.ExecutionInfo == testing.ExecutionInfo
+
+def _is_windows(env):
+ """Tell if the target platform is windows.
+
+ This assumes the `WINDOWS_ATTR` attribute was added.
+
+ Args:
+ env: The test env struct
+ Returns:
+ True if the target is Windows, False if not.
+ """
+ constraint = env.ctx.attr.windows[platform_common.ConstraintValueInfo]
+ return env.ctx.target_platform_has_constraint(constraint)
+
+util = struct(
+ create_tests = _create_tests,
+ struct_with = _struct_with,
+ is_bazel_6_or_higher = _is_bazel_6_or_higher,
+ is_windows = _is_windows,
+)
diff --git a/tools/publish/BUILD.bazel b/tools/publish/BUILD.bazel
new file mode 100644
index 0000000..065e56b
--- /dev/null
+++ b/tools/publish/BUILD.bazel
@@ -0,0 +1,7 @@
+load("//python:pip.bzl", "compile_pip_requirements")
+
+compile_pip_requirements(
+ name = "requirements",
+ requirements_darwin = "requirements_darwin.txt",
+ requirements_windows = "requirements_windows.txt",
+)
diff --git a/tools/publish/README.md b/tools/publish/README.md
new file mode 100644
index 0000000..6f1e549
--- /dev/null
+++ b/tools/publish/README.md
@@ -0,0 +1,6 @@
+# Publish to pypi with twine
+
+https://packaging.python.org/en/latest/tutorials/packaging-projects/ indicates that the twine
+package is used to publish wheels to pypi.
+
+See more: https://twine.readthedocs.io/en/stable/
diff --git a/tools/publish/requirements.in b/tools/publish/requirements.in
new file mode 100644
index 0000000..af996cf
--- /dev/null
+++ b/tools/publish/requirements.in
@@ -0,0 +1 @@
+twine
diff --git a/tools/publish/requirements.txt b/tools/publish/requirements.txt
new file mode 100644
index 0000000..858fc51
--- /dev/null
+++ b/tools/publish/requirements.txt
@@ -0,0 +1,297 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# bazel run //tools/publish:requirements.update
+#
+bleach==6.0.0 \
+ --hash=sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414 \
+ --hash=sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4
+ # via readme-renderer
+certifi==2022.12.7 \
+ --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \
+ --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18
+ # via requests
+cffi==1.15.1 \
+ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
+ --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
+ --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
+ --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
+ --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
+ --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
+ --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
+ --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
+ --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
+ --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
+ --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
+ --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
+ --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
+ --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
+ --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
+ --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
+ --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
+ --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
+ --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
+ --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
+ --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
+ --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
+ --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
+ --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
+ --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
+ --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
+ --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
+ --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
+ --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
+ --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
+ --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
+ --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
+ --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
+ --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
+ --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
+ --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
+ --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
+ --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
+ --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
+ --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
+ --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
+ --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
+ --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
+ --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
+ --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
+ --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
+ --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
+ --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
+ --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
+ --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
+ --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
+ --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
+ --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
+ --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
+ --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
+ --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
+ --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
+ --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
+ --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
+ --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
+ --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
+ --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
+ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
+ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
+ # via cryptography
+charset-normalizer==3.0.1 \
+ --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \
+ --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \
+ --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \
+ --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \
+ --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \
+ --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \
+ --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \
+ --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \
+ --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \
+ --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \
+ --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \
+ --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \
+ --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \
+ --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \
+ --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \
+ --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \
+ --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \
+ --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \
+ --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \
+ --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \
+ --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \
+ --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \
+ --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \
+ --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \
+ --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \
+ --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \
+ --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \
+ --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \
+ --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \
+ --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \
+ --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \
+ --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \
+ --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \
+ --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \
+ --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \
+ --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \
+ --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \
+ --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \
+ --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \
+ --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \
+ --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \
+ --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \
+ --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \
+ --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \
+ --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \
+ --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \
+ --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \
+ --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \
+ --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \
+ --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \
+ --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \
+ --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \
+ --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \
+ --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \
+ --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \
+ --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \
+ --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \
+ --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \
+ --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \
+ --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \
+ --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \
+ --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \
+ --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \
+ --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \
+ --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \
+ --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \
+ --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \
+ --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \
+ --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \
+ --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \
+ --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \
+ --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \
+ --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \
+ --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \
+ --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \
+ --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \
+ --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \
+ --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \
+ --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \
+ --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \
+ --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \
+ --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \
+ --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \
+ --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \
+ --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \
+ --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \
+ --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \
+ --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8
+ # via requests
+cryptography==39.0.0 \
+ --hash=sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b \
+ --hash=sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f \
+ --hash=sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190 \
+ --hash=sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f \
+ --hash=sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f \
+ --hash=sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb \
+ --hash=sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c \
+ --hash=sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773 \
+ --hash=sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72 \
+ --hash=sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8 \
+ --hash=sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717 \
+ --hash=sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9 \
+ --hash=sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856 \
+ --hash=sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96 \
+ --hash=sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288 \
+ --hash=sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39 \
+ --hash=sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e \
+ --hash=sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce \
+ --hash=sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1 \
+ --hash=sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de \
+ --hash=sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df \
+ --hash=sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf \
+ --hash=sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458
+ # via secretstorage
+docutils==0.19 \
+ --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \
+ --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc
+ # via readme-renderer
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via requests
+importlib-metadata==6.0.0 \
+ --hash=sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad \
+ --hash=sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d
+ # via
+ # keyring
+ # twine
+jaraco-classes==3.2.3 \
+ --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \
+ --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a
+ # via keyring
+jeepney==0.8.0 \
+ --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \
+ --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755
+ # via
+ # keyring
+ # secretstorage
+keyring==23.13.1 \
+ --hash=sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd \
+ --hash=sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678
+ # via twine
+markdown-it-py==2.1.0 \
+ --hash=sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27 \
+ --hash=sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da
+ # via rich
+mdurl==0.1.2 \
+ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
+ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
+ # via markdown-it-py
+more-itertools==9.0.0 \
+ --hash=sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41 \
+ --hash=sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab
+ # via jaraco-classes
+pkginfo==1.9.6 \
+ --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \
+ --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046
+ # via twine
+pycparser==2.21 \
+ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
+ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
+ # via cffi
+pygments==2.14.0 \
+ --hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \
+ --hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717
+ # via
+ # readme-renderer
+ # rich
+readme-renderer==37.3 \
+ --hash=sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273 \
+ --hash=sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343
+ # via twine
+requests==2.28.2 \
+ --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \
+ --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf
+ # via
+ # requests-toolbelt
+ # twine
+requests-toolbelt==0.10.1 \
+ --hash=sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7 \
+ --hash=sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d
+ # via twine
+rfc3986==2.0.0 \
+ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \
+ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c
+ # via twine
+rich==13.2.0 \
+ --hash=sha256:7c963f0d03819221e9ac561e1bc866e3f95a02248c1234daa48954e6d381c003 \
+ --hash=sha256:f1a00cdd3eebf999a15d85ec498bfe0b1a77efe9b34f645768a54132ef444ac5
+ # via twine
+secretstorage==3.3.3 \
+ --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \
+ --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99
+ # via keyring
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via bleach
+twine==4.0.2 \
+ --hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \
+ --hash=sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8
+ # via -r tools/publish/requirements.in
+urllib3==1.26.14 \
+ --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \
+ --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1
+ # via
+ # requests
+ # twine
+webencodings==0.5.1 \
+ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
+ --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
+ # via bleach
+zipp==3.11.0 \
+ --hash=sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa \
+ --hash=sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766
+ # via importlib-metadata
diff --git a/tools/publish/requirements_darwin.txt b/tools/publish/requirements_darwin.txt
new file mode 100644
index 0000000..cb35e69
--- /dev/null
+++ b/tools/publish/requirements_darwin.txt
@@ -0,0 +1,192 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# bazel run //tools/publish:requirements.update
+#
+bleach==6.0.0 \
+ --hash=sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414 \
+ --hash=sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4
+ # via readme-renderer
+certifi==2022.12.7 \
+ --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \
+ --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18
+ # via requests
+charset-normalizer==3.0.1 \
+ --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \
+ --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \
+ --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \
+ --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \
+ --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \
+ --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \
+ --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \
+ --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \
+ --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \
+ --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \
+ --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \
+ --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \
+ --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \
+ --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \
+ --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \
+ --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \
+ --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \
+ --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \
+ --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \
+ --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \
+ --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \
+ --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \
+ --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \
+ --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \
+ --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \
+ --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \
+ --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \
+ --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \
+ --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \
+ --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \
+ --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \
+ --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \
+ --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \
+ --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \
+ --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \
+ --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \
+ --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \
+ --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \
+ --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \
+ --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \
+ --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \
+ --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \
+ --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \
+ --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \
+ --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \
+ --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \
+ --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \
+ --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \
+ --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \
+ --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \
+ --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \
+ --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \
+ --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \
+ --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \
+ --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \
+ --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \
+ --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \
+ --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \
+ --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \
+ --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \
+ --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \
+ --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \
+ --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \
+ --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \
+ --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \
+ --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \
+ --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \
+ --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \
+ --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \
+ --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \
+ --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \
+ --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \
+ --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \
+ --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \
+ --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \
+ --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \
+ --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \
+ --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \
+ --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \
+ --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \
+ --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \
+ --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \
+ --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \
+ --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \
+ --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \
+ --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \
+ --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \
+ --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8
+ # via requests
+docutils==0.19 \
+ --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \
+ --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc
+ # via readme-renderer
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via requests
+importlib-metadata==6.0.0 \
+ --hash=sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad \
+ --hash=sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d
+ # via
+ # keyring
+ # twine
+jaraco-classes==3.2.3 \
+ --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \
+ --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a
+ # via keyring
+keyring==23.13.1 \
+ --hash=sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd \
+ --hash=sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678
+ # via twine
+markdown-it-py==2.1.0 \
+ --hash=sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27 \
+ --hash=sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da
+ # via rich
+mdurl==0.1.2 \
+ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
+ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
+ # via markdown-it-py
+more-itertools==9.0.0 \
+ --hash=sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41 \
+ --hash=sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab
+ # via jaraco-classes
+pkginfo==1.9.6 \
+ --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \
+ --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046
+ # via twine
+pygments==2.14.0 \
+ --hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \
+ --hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717
+ # via
+ # readme-renderer
+ # rich
+readme-renderer==37.3 \
+ --hash=sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273 \
+ --hash=sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343
+ # via twine
+requests==2.28.2 \
+ --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \
+ --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf
+ # via
+ # requests-toolbelt
+ # twine
+requests-toolbelt==0.10.1 \
+ --hash=sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7 \
+ --hash=sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d
+ # via twine
+rfc3986==2.0.0 \
+ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \
+ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c
+ # via twine
+rich==13.2.0 \
+ --hash=sha256:7c963f0d03819221e9ac561e1bc866e3f95a02248c1234daa48954e6d381c003 \
+ --hash=sha256:f1a00cdd3eebf999a15d85ec498bfe0b1a77efe9b34f645768a54132ef444ac5
+ # via twine
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via bleach
+twine==4.0.2 \
+ --hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \
+ --hash=sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8
+ # via -r tools/publish/requirements.in
+urllib3==1.26.14 \
+ --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \
+ --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1
+ # via
+ # requests
+ # twine
+webencodings==0.5.1 \
+ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
+ --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
+ # via bleach
+zipp==3.11.0 \
+ --hash=sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa \
+ --hash=sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766
+ # via importlib-metadata
diff --git a/tools/publish/requirements_windows.txt b/tools/publish/requirements_windows.txt
new file mode 100644
index 0000000..cd175c6
--- /dev/null
+++ b/tools/publish/requirements_windows.txt
@@ -0,0 +1,196 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# bazel run //tools/publish:requirements.update
+#
+bleach==6.0.0 \
+ --hash=sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414 \
+ --hash=sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4
+ # via readme-renderer
+certifi==2022.12.7 \
+ --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \
+ --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18
+ # via requests
+charset-normalizer==3.0.1 \
+ --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \
+ --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \
+ --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \
+ --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \
+ --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \
+ --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \
+ --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \
+ --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \
+ --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \
+ --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \
+ --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \
+ --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \
+ --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \
+ --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \
+ --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \
+ --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \
+ --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \
+ --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \
+ --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \
+ --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \
+ --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \
+ --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \
+ --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \
+ --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \
+ --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \
+ --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \
+ --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \
+ --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \
+ --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \
+ --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \
+ --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \
+ --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \
+ --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \
+ --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \
+ --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \
+ --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \
+ --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \
+ --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \
+ --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \
+ --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \
+ --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \
+ --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \
+ --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \
+ --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \
+ --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \
+ --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \
+ --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \
+ --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \
+ --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \
+ --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \
+ --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \
+ --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \
+ --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \
+ --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \
+ --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \
+ --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \
+ --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \
+ --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \
+ --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \
+ --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \
+ --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \
+ --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \
+ --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \
+ --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \
+ --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \
+ --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \
+ --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \
+ --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \
+ --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \
+ --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \
+ --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \
+ --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \
+ --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \
+ --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \
+ --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \
+ --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \
+ --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \
+ --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \
+ --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \
+ --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \
+ --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \
+ --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \
+ --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \
+ --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \
+ --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \
+ --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \
+ --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \
+ --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8
+ # via requests
+docutils==0.19 \
+ --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \
+ --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc
+ # via readme-renderer
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via requests
+importlib-metadata==6.0.0 \
+ --hash=sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad \
+ --hash=sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d
+ # via
+ # keyring
+ # twine
+jaraco-classes==3.2.3 \
+ --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \
+ --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a
+ # via keyring
+keyring==23.13.1 \
+ --hash=sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd \
+ --hash=sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678
+ # via twine
+markdown-it-py==2.1.0 \
+ --hash=sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27 \
+ --hash=sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da
+ # via rich
+mdurl==0.1.2 \
+ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
+ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
+ # via markdown-it-py
+more-itertools==9.0.0 \
+ --hash=sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41 \
+ --hash=sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab
+ # via jaraco-classes
+pkginfo==1.9.6 \
+ --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \
+ --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046
+ # via twine
+pygments==2.14.0 \
+ --hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \
+ --hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717
+ # via
+ # readme-renderer
+ # rich
+pywin32-ctypes==0.2.0 \
+ --hash=sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942 \
+ --hash=sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98
+ # via keyring
+readme-renderer==37.3 \
+ --hash=sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273 \
+ --hash=sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343
+ # via twine
+requests==2.28.2 \
+ --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \
+ --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf
+ # via
+ # requests-toolbelt
+ # twine
+requests-toolbelt==0.10.1 \
+ --hash=sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7 \
+ --hash=sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d
+ # via twine
+rfc3986==2.0.0 \
+ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \
+ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c
+ # via twine
+rich==13.2.0 \
+ --hash=sha256:7c963f0d03819221e9ac561e1bc866e3f95a02248c1234daa48954e6d381c003 \
+ --hash=sha256:f1a00cdd3eebf999a15d85ec498bfe0b1a77efe9b34f645768a54132ef444ac5
+ # via twine
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via bleach
+twine==4.0.2 \
+ --hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \
+ --hash=sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8
+ # via -r tools/publish/requirements.in
+urllib3==1.26.14 \
+ --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \
+ --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1
+ # via
+ # requests
+ # twine
+webencodings==0.5.1 \
+ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
+ --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
+ # via bleach
+zipp==3.11.0 \
+ --hash=sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa \
+ --hash=sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766
+ # via importlib-metadata
diff --git a/tools/update_coverage_deps.py b/tools/update_coverage_deps.py
new file mode 100755
index 0000000..57b7850
--- /dev/null
+++ b/tools/update_coverage_deps.py
@@ -0,0 +1,248 @@
+#!/usr/bin/python3 -B
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A small script to update bazel files within the repo.
+
+We are not running this with 'bazel run' to keep the dependencies minimal
+"""
+
+# NOTE @aignas 2023-01-09: We should only depend on core Python 3 packages.
+import argparse
+import difflib
+import json
+import pathlib
+import sys
+import textwrap
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import Any
+from urllib import request
+
+# This should be kept in sync with //python:versions.bzl
+_supported_platforms = {
+ # Windows is unsupported right now
+ # "win_amd64": "x86_64-pc-windows-msvc",
+ "manylinux2014_x86_64": "x86_64-unknown-linux-gnu",
+ "manylinux2014_aarch64": "aarch64-unknown-linux-gnu",
+ "macosx_11_0_arm64": "aarch64-apple-darwin",
+ "macosx_10_9_x86_64": "x86_64-apple-darwin",
+}
+
+
+@dataclass
+class Dep:
+ name: str
+ platform: str
+ python: str
+ url: str
+ sha256: str
+
+ @property
+ def repo_name(self):
+ return f"pypi__{self.name}_{self.python}_{self.platform}"
+
+ def __repr__(self):
+ return "\n".join(
+ [
+ "(",
+ f' "{self.url}",',
+ f' "{self.sha256}",',
+ ")",
+ ]
+ )
+
+
+@dataclass
+class Deps:
+ deps: list[Dep]
+
+ def __repr__(self):
+ deps = defaultdict(dict)
+ for d in self.deps:
+ deps[d.python][d.platform] = d
+
+ parts = []
+ for python, contents in deps.items():
+ inner = textwrap.indent(
+ "\n".join([f'"{platform}": {d},' for platform, d in contents.items()]),
+ prefix=" ",
+ )
+ parts.append('"{}": {{\n{}\n}},'.format(python, inner))
+ return "{{\n{}\n}}".format(textwrap.indent("\n".join(parts), prefix=" "))
+
+
+def _get_platforms(filename: str, name: str, version: str, python_version: str):
+ return filename[
+ len(f"{name}-{version}-{python_version}-{python_version}-") : -len(".whl")
+ ].split(".")
+
+
+def _map(
+ name: str,
+ filename: str,
+ python_version: str,
+ url: str,
+ digests: list,
+ platform: str,
+ **kwargs: Any,
+):
+ if platform not in _supported_platforms:
+ return None
+
+ return Dep(
+ name=name,
+ platform=_supported_platforms[platform],
+ python=python_version,
+ url=url,
+ sha256=digests["sha256"],
+ )
+
+
+def _writelines(path: pathlib.Path, lines: list[str]):
+ with open(path, "w") as f:
+ f.writelines(lines)
+
+
+def _difflines(path: pathlib.Path, lines: list[str]):
+ with open(path) as f:
+ input = f.readlines()
+
+ rules_python = pathlib.Path(__file__).parent.parent
+ p = path.relative_to(rules_python)
+
+ print(f"Diff of the changes that would be made to '{p}':")
+ for line in difflib.unified_diff(
+ input,
+ lines,
+ fromfile=f"a/{p}",
+ tofile=f"b/{p}",
+ ):
+ print(line, end="")
+
+ # Add an empty line at the end of the diff
+ print()
+
+
+def _update_file(
+ path: pathlib.Path,
+ snippet: str,
+ start_marker: str,
+ end_marker: str,
+ dry_run: bool = True,
+):
+ with open(path) as f:
+ input = f.readlines()
+
+ out = []
+ skip = False
+ for line in input:
+ if skip:
+ if not line.startswith(end_marker):
+ continue
+
+ skip = False
+
+ out.append(line)
+
+ if not line.startswith(start_marker):
+ continue
+
+ skip = True
+ out.extend([f"{line}\n" for line in snippet.splitlines()])
+
+ if dry_run:
+ _difflines(path, out)
+ else:
+ _writelines(path, out)
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(__doc__)
+ parser.add_argument(
+ "--name",
+ default="coverage",
+ type=str,
+ help="The name of the package",
+ )
+ parser.add_argument(
+ "version",
+ type=str,
+ help="The version of the package to download",
+ )
+ parser.add_argument(
+ "--py",
+ nargs="+",
+ type=str,
+ default=["cp38", "cp39", "cp310", "cp311"],
+ help="Supported python versions",
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Wether to write to files",
+ )
+ return parser.parse_args()
+
+
+def main():
+ args = _parse_args()
+
+ api_url = f"https://pypi.python.org/pypi/{args.name}/{args.version}/json"
+ req = request.Request(api_url)
+ with request.urlopen(req) as response:
+ data = json.loads(response.read().decode("utf-8"))
+
+ urls = []
+ for u in data["urls"]:
+ if u["yanked"]:
+ continue
+
+ if not u["filename"].endswith(".whl"):
+ continue
+
+ if u["python_version"] not in args.py:
+ continue
+
+ if f'_{u["python_version"]}m_' in u["filename"]:
+ continue
+
+ platforms = _get_platforms(
+ u["filename"],
+ args.name,
+ args.version,
+ u["python_version"],
+ )
+
+ result = [_map(name=args.name, platform=p, **u) for p in platforms]
+ urls.extend(filter(None, result))
+
+ urls.sort(key=lambda x: f"{x.python}_{x.platform}")
+
+ rules_python = pathlib.Path(__file__).parent.parent
+
+ # Update the coverage_deps, which are used to register deps
+ _update_file(
+ path=rules_python / "python" / "private" / "coverage_deps.bzl",
+ snippet=f"_coverage_deps = {repr(Deps(urls))}\n",
+ start_marker="#START: managed by update_coverage_deps.py script",
+ end_marker="#END: managed by update_coverage_deps.py script",
+ dry_run=args.dry_run,
+ )
+
+ return
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py
new file mode 100644
index 0000000..63b833f
--- /dev/null
+++ b/tools/wheelmaker.py
@@ -0,0 +1,436 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import base64
+import collections
+import hashlib
+import os
+import re
+import sys
+import zipfile
+from pathlib import Path
+
+
+def commonpath(path1, path2):
+ ret = []
+ for a, b in zip(path1.split(os.path.sep), path2.split(os.path.sep)):
+ if a != b:
+ break
+ ret.append(a)
+ return os.path.sep.join(ret)
+
+
+def escape_filename_segment(segment):
+ """Escapes a filename segment per https://www.python.org/dev/peps/pep-0427/#escaping-and-unicode"""
+ return re.sub(r"[^\w\d.]+", "_", segment, re.UNICODE)
+
+
+class WheelMaker(object):
+ def __init__(
+ self,
+ name,
+ version,
+ build_tag,
+ python_tag,
+ abi,
+ platform,
+ outfile=None,
+ strip_path_prefixes=None,
+ ):
+ self._name = name
+ self._version = version
+ self._build_tag = build_tag
+ self._python_tag = python_tag
+ self._abi = abi
+ self._platform = platform
+ self._outfile = outfile
+ self._strip_path_prefixes = (
+ strip_path_prefixes if strip_path_prefixes is not None else []
+ )
+
+ self._distinfo_dir = (
+ escape_filename_segment(self._name)
+ + "-"
+ + escape_filename_segment(self._version)
+ + ".dist-info/"
+ )
+ self._zipfile = None
+ # Entries for the RECORD file as (filename, hash, size) tuples.
+ self._record = []
+
+ def __enter__(self):
+ self._zipfile = zipfile.ZipFile(
+ self.filename(), mode="w", compression=zipfile.ZIP_DEFLATED
+ )
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self._zipfile.close()
+ self._zipfile = None
+
+ def wheelname(self) -> str:
+ components = [self._name, self._version]
+ if self._build_tag:
+ components.append(self._build_tag)
+ components += [self._python_tag, self._abi, self._platform]
+ return "-".join(components) + ".whl"
+
+ def filename(self) -> str:
+ if self._outfile:
+ return self._outfile
+ return self.wheelname()
+
+ def disttags(self):
+ return ["-".join([self._python_tag, self._abi, self._platform])]
+
+ def distinfo_path(self, basename):
+ return self._distinfo_dir + basename
+
+ def _serialize_digest(self, hash):
+ # https://www.python.org/dev/peps/pep-0376/#record
+ # "base64.urlsafe_b64encode(digest) with trailing = removed"
+ digest = base64.urlsafe_b64encode(hash.digest())
+ digest = b"sha256=" + digest.rstrip(b"=")
+ return digest
+
+ def add_string(self, filename, contents):
+ """Add given 'contents' as filename to the distribution."""
+ if sys.version_info[0] > 2 and isinstance(contents, str):
+ contents = contents.encode("utf-8", "surrogateescape")
+ self._zipfile.writestr(filename, contents)
+ hash = hashlib.sha256()
+ hash.update(contents)
+ self._add_to_record(filename, self._serialize_digest(hash), len(contents))
+
+ def add_file(self, package_filename, real_filename):
+ """Add given file to the distribution."""
+
+ def arcname_from(name):
+ # Always use unix path separators.
+ normalized_arcname = name.replace(os.path.sep, "/")
+ # Don't manipulate names filenames in the .distinfo directory.
+ if normalized_arcname.startswith(self._distinfo_dir):
+ return normalized_arcname
+ for prefix in self._strip_path_prefixes:
+ if normalized_arcname.startswith(prefix):
+ return normalized_arcname[len(prefix) :]
+
+ return normalized_arcname
+
+ if os.path.isdir(real_filename):
+ directory_contents = os.listdir(real_filename)
+ for file_ in directory_contents:
+ self.add_file(
+ "{}/{}".format(package_filename, file_),
+ "{}/{}".format(real_filename, file_),
+ )
+ return
+
+ arcname = arcname_from(package_filename)
+
+ self._zipfile.write(real_filename, arcname=arcname)
+ # Find the hash and length
+ hash = hashlib.sha256()
+ size = 0
+ with open(real_filename, "rb") as f:
+ while True:
+ block = f.read(2**20)
+ if not block:
+ break
+ hash.update(block)
+ size += len(block)
+ self._add_to_record(arcname, self._serialize_digest(hash), size)
+
+ def add_wheelfile(self):
+ """Write WHEEL file to the distribution"""
+ # TODO(pstradomski): Support non-purelib wheels.
+ wheel_contents = """\
+Wheel-Version: 1.0
+Generator: bazel-wheelmaker 1.0
+Root-Is-Purelib: {}
+""".format(
+ "true" if self._platform == "any" else "false"
+ )
+ for tag in self.disttags():
+ wheel_contents += "Tag: %s\n" % tag
+ self.add_string(self.distinfo_path("WHEEL"), wheel_contents)
+
+ def add_metadata(self, metadata, name, description, version):
+ """Write METADATA file to the distribution."""
+ # https://www.python.org/dev/peps/pep-0566/
+ # https://packaging.python.org/specifications/core-metadata/
+ metadata = re.sub("^Name: .*$", "Name: %s" % name, metadata, flags=re.MULTILINE)
+ metadata += "Version: %s\n\n" % version
+ # setuptools seems to insert UNKNOWN as description when none is
+ # provided.
+ metadata += description if description else "UNKNOWN"
+ metadata += "\n"
+ self.add_string(self.distinfo_path("METADATA"), metadata)
+
+ def add_recordfile(self):
+ """Write RECORD file to the distribution."""
+ record_path = self.distinfo_path("RECORD")
+ entries = self._record + [(record_path, b"", b"")]
+ entries.sort()
+ contents = b""
+ for filename, digest, size in entries:
+ if sys.version_info[0] > 2 and isinstance(filename, str):
+ filename = filename.lstrip("/").encode("utf-8", "surrogateescape")
+ contents += b"%s,%s,%s\n" % (filename, digest, size)
+ self.add_string(record_path, contents)
+
+ def _add_to_record(self, filename, hash, size):
+ size = str(size).encode("ascii")
+ self._record.append((filename, hash, size))
+
+
+def get_files_to_package(input_files):
+ """Find files to be added to the distribution.
+
+ input_files: list of pairs (package_path, real_path)
+ """
+ files = {}
+ for package_path, real_path in input_files:
+ files[package_path] = real_path
+ return files
+
+
+def resolve_argument_stamp(
+ argument: str, volatile_status_stamp: Path, stable_status_stamp: Path
+) -> str:
+ """Resolve workspace status stamps format strings found in the argument string
+
+ Args:
+ argument (str): The raw argument represenation for the wheel (may include stamp variables)
+ volatile_status_stamp (Path): The path to a volatile workspace status file
+ stable_status_stamp (Path): The path to a stable workspace status file
+
+ Returns:
+ str: A resolved argument string
+ """
+ lines = (
+ volatile_status_stamp.read_text().splitlines()
+ + stable_status_stamp.read_text().splitlines()
+ )
+ for line in lines:
+ if not line:
+ continue
+ key, value = line.split(" ", maxsplit=1)
+ stamp = "{" + key + "}"
+ argument = argument.replace(stamp, value)
+
+ return argument
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description="Builds a python wheel")
+ metadata_group = parser.add_argument_group("Wheel name, version and platform")
+ metadata_group.add_argument(
+ "--name", required=True, type=str, help="Name of the distribution"
+ )
+ metadata_group.add_argument(
+ "--version", required=True, type=str, help="Version of the distribution"
+ )
+ metadata_group.add_argument(
+ "--build_tag",
+ type=str,
+ default="",
+ help="Optional build tag for the distribution",
+ )
+ metadata_group.add_argument(
+ "--python_tag",
+ type=str,
+ default="py3",
+ help="Python version, e.g. 'py2' or 'py3'",
+ )
+ metadata_group.add_argument("--abi", type=str, default="none")
+ metadata_group.add_argument(
+ "--platform", type=str, default="any", help="Target platform. "
+ )
+
+ output_group = parser.add_argument_group("Output file location")
+ output_group.add_argument(
+ "--out", type=str, default=None, help="Override name of ouptut file"
+ )
+ output_group.add_argument(
+ "--name_file",
+ type=Path,
+ help="A file where the canonical name of the " "wheel will be written",
+ )
+
+ output_group.add_argument(
+ "--strip_path_prefix",
+ type=str,
+ action="append",
+ default=[],
+ help="Path prefix to be stripped from input package files' path. "
+ "Can be supplied multiple times. Evaluated in order.",
+ )
+
+ wheel_group = parser.add_argument_group("Wheel metadata")
+ wheel_group.add_argument(
+ "--metadata_file",
+ type=Path,
+ help="Contents of the METADATA file (before appending contents of "
+ "--description_file)",
+ )
+ wheel_group.add_argument(
+ "--description_file", help="Path to the file with package description"
+ )
+ wheel_group.add_argument(
+ "--description_content_type", help="Content type of the package description"
+ )
+ wheel_group.add_argument(
+ "--entry_points_file",
+ help="Path to a correctly-formatted entry_points.txt file",
+ )
+
+ contents_group = parser.add_argument_group("Wheel contents")
+ contents_group.add_argument(
+ "--input_file",
+ action="append",
+ help="'package_path;real_path' pairs listing "
+ "files to be included in the wheel. "
+ "Can be supplied multiple times.",
+ )
+ contents_group.add_argument(
+ "--input_file_list",
+ action="append",
+ help="A file that has all the input files defined as a list to avoid "
+ "the long command",
+ )
+ contents_group.add_argument(
+ "--extra_distinfo_file",
+ action="append",
+ help="'filename;real_path' pairs listing extra files to include in"
+ "dist-info directory. Can be supplied multiple times.",
+ )
+
+ build_group = parser.add_argument_group("Building requirements")
+ build_group.add_argument(
+ "--volatile_status_file",
+ type=Path,
+ help="Pass in the stamp info file for stamping",
+ )
+ build_group.add_argument(
+ "--stable_status_file",
+ type=Path,
+ help="Pass in the stamp info file for stamping",
+ )
+
+ return parser.parse_args(sys.argv[1:])
+
+
+def main() -> None:
+ arguments = parse_args()
+
+ if arguments.input_file:
+ input_files = [i.split(";") for i in arguments.input_file]
+ else:
+ input_files = []
+
+ if arguments.extra_distinfo_file:
+ extra_distinfo_file = [i.split(";") for i in arguments.extra_distinfo_file]
+ else:
+ extra_distinfo_file = []
+
+ if arguments.input_file_list:
+ for input_file in arguments.input_file_list:
+ with open(input_file) as _file:
+ input_file_list = _file.read().splitlines()
+ for _input_file in input_file_list:
+ input_files.append(_input_file.split(";"))
+
+ all_files = get_files_to_package(input_files)
+ # Sort the files for reproducible order in the archive.
+ all_files = sorted(all_files.items())
+
+ strip_prefixes = [p for p in arguments.strip_path_prefix]
+
+ if arguments.volatile_status_file and arguments.stable_status_file:
+ name = resolve_argument_stamp(
+ arguments.name,
+ arguments.volatile_status_file,
+ arguments.stable_status_file,
+ )
+ else:
+ name = arguments.name
+
+ if arguments.volatile_status_file and arguments.stable_status_file:
+ version = resolve_argument_stamp(
+ arguments.version,
+ arguments.volatile_status_file,
+ arguments.stable_status_file,
+ )
+ else:
+ version = arguments.version
+
+ with WheelMaker(
+ name=name,
+ version=version,
+ build_tag=arguments.build_tag,
+ python_tag=arguments.python_tag,
+ abi=arguments.abi,
+ platform=arguments.platform,
+ outfile=arguments.out,
+ strip_path_prefixes=strip_prefixes,
+ ) as maker:
+ for package_filename, real_filename in all_files:
+ maker.add_file(package_filename, real_filename)
+ maker.add_wheelfile()
+
+ description = None
+ if arguments.description_file:
+ if sys.version_info[0] == 2:
+ with open(arguments.description_file, "rt") as description_file:
+ description = description_file.read()
+ else:
+ with open(
+ arguments.description_file, "rt", encoding="utf-8"
+ ) as description_file:
+ description = description_file.read()
+
+ metadata = None
+ if sys.version_info[0] == 2:
+ with open(arguments.metadata_file, "rt") as metadata_file:
+ metadata = metadata_file.read()
+ else:
+ with open(arguments.metadata_file, "rt", encoding="utf-8") as metadata_file:
+ metadata = metadata_file.read()
+
+ maker.add_metadata(
+ metadata=metadata, name=name, description=description, version=version
+ )
+
+ if arguments.entry_points_file:
+ maker.add_file(
+ maker.distinfo_path("entry_points.txt"), arguments.entry_points_file
+ )
+
+ # Sort the files for reproducible order in the archive.
+ for filename, real_path in sorted(extra_distinfo_file):
+ maker.add_file(maker.distinfo_path(filename), real_path)
+
+ maker.add_recordfile()
+
+ # Since stamping may otherwise change the target name of the
+ # wheel, the canonical name (with stamps resolved) is written
+ # to a file so consumers of the wheel can easily determine
+ # the correct name.
+ arguments.name_file.write_text(maker.wheelname())
+
+
+if __name__ == "__main__":
+ main()