Skip to content

Commit eb55280

Browse files
committed
feat: wheel publishing
Tested manually with: ``` $ bazel run --stamp --embed_label=0.17.0 //python/runfiles:wheel.publish -- --repository testpypi ``` That result is here: https://test.pypi.org/project/bazel-runfiles/0.17.0/ Note, I'd also like to add this to the examples/wheel, see #1017 for a pre-requisite.
1 parent 9960253 commit eb55280

File tree

8 files changed

+462
-26
lines changed

8 files changed

+462
-26
lines changed

.github/workflows/release.yml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,14 @@ jobs:
1414
uses: actions/checkout@v2
1515
- name: Prepare workspace snippet
1616
run: .github/workflows/workspace_snippet.sh > release_notes.txt
17-
- name: Build wheel dist
18-
run: bazel build --stamp --embed_label=${{ env.GITHUB_REF_NAME }} //python/runfiles:wheel
19-
- name: Publish runfiles package to PyPI
20-
uses: pypa/gh-action-pypi-publish@release/v1
21-
with:
17+
- name: Publish wheel dist
18+
env:
19+
# This special value tells pypi that the user identity is supplied within the token
20+
TWINE_USERNAME: __token__
2221
# Note, the PYPI_API_TOKEN was added on
2322
# https://github.com/bazelbuild/rules_python/settings/secrets/actions
24-
# and currently uses a token which authenticates as https://pypi.org/user/alexeagle/
25-
password: ${{ secrets.PYPI_API_TOKEN }}
26-
packages_dir: bazel-bin/python/runfiles
23+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
24+
run: bazel run --stamp --embed_label=${{ env.GITHUB_REF_NAME }} //python/runfiles:wheel.publish
2725
- name: Release
2826
uses: softprops/action-gh-release@v1
2927
with:

WORKSPACE

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,21 @@ load("@rules_python_gazelle_plugin//:deps.bzl", _py_gazelle_deps = "gazelle_deps
6767
# This rule loads and compiles various go dependencies that running gazelle
6868
# for python requirements.
6969
_py_gazelle_deps()
70+
71+
load("@python//3.11.1:defs.bzl", "interpreter")
72+
73+
#####################
74+
# Install twine for our own runfiles wheel publishing
75+
# Eventually we might want to install twine automatically for users too, see:
76+
# See https://github.com/bazelbuild/rules_python/issues/1016
77+
load("@rules_python//python:pip.bzl", "pip_parse")
78+
79+
pip_parse(
80+
name = "publish_deps",
81+
python_interpreter_target = interpreter,
82+
requirements_lock = "//python/runfiles:requirements.txt",
83+
)
84+
85+
load("@publish_deps//:requirements.bzl", "install_deps")
86+
87+
install_deps()

python/packaging.bzl

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Public API for for building wheels."""
1616

1717
load("//python/private:py_package.bzl", "py_package_lib")
18+
load("//python/private:py_twine.bzl", "py_twine_lib")
1819
load("//python/private:py_wheel.bzl", _PyWheelInfo = "PyWheelInfo", _py_wheel = "py_wheel")
1920

2021
# Re-export as public API
@@ -80,15 +81,22 @@ def py_wheel(name, **kwargs):
8081
name: A unique name for this target.
8182
**kwargs: other named parameters passed to the underlying [py_wheel rule](#py_wheel_rule)
8283
"""
83-
_py_wheel(name = name, **kwargs)
84+
py_twine(
85+
name = "{}.publish".format(name),
86+
wheel = name,
87+
twine_bin = kwargs.pop("twine_bin", None),
88+
)
8489

85-
# TODO(alexeagle): produce an executable target like this:
86-
# py_publish_wheel(
87-
# name = "{}.publish".format(name),
88-
# wheel = name,
89-
# # Optional: override the label for a py_binary that runs twine
90-
# # https://twine.readthedocs.io/en/stable/
91-
# twine_bin = "//path/to:twine",
92-
# )
90+
_py_wheel(name = name, **kwargs)
9391

9492
py_wheel_rule = _py_wheel
93+
94+
py_twine = rule(
95+
doc = """\
96+
The py_twine rule executes the twine CLI to upload packages.
97+
https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-the-distribution-archives
98+
""",
99+
implementation = py_twine_lib.implementation,
100+
attrs = py_twine_lib.attrs,
101+
executable = True,
102+
)

python/private/py_twine.bzl

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Implementation of py_twine rule.
2+
3+
Simply wraps the tool with a bash script and a runfiles manifest.
4+
See https://twine.readthedocs.io/
5+
"""
6+
7+
load(":py_wheel.bzl", "PyWheelInfo")
8+
9+
_attrs = {
10+
"twine_bin": attr.label(
11+
doc = """\
12+
A py_binary that runs the twine tool.
13+
The default value assumes you have `twine` listed in your own requirements.txt, and have run
14+
`pip_parse` with the default name of `pypi`.
15+
16+
If these don't apply, you might use the `entry_point` helper to supply your own twine binary:
17+
```starlark
18+
load("@my_pip_parse_name//:requirements.bzl", "entry_point")
19+
py_twine(
20+
...
21+
twine_bin = entry_point("twine"),
22+
)
23+
```
24+
25+
Or of course you can supply a py_binary by some other means which is CLI-compatible with twine.
26+
27+
Currently rules_python doesn't supply twine itself.
28+
Follow https://github.com/bazelbuild/rules_python/issues/1016
29+
""",
30+
default = "@pypi_twine//:rules_python_wheel_entry_point_twine",
31+
executable = True,
32+
cfg = "exec",
33+
),
34+
"wheel": attr.label(providers = [PyWheelInfo]),
35+
"_runfiles_lib": attr.label(default = "@bazel_tools//tools/bash/runfiles"),
36+
}
37+
38+
# Bash helper function for looking up runfiles.
39+
# Vendored from
40+
# https://github.com/bazelbuild/bazel/blob/master/tools/bash/runfiles/runfiles.bash
41+
BASH_RLOCATION_FUNCTION = r"""
42+
# --- begin runfiles.bash initialization v2 ---
43+
set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash
44+
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
45+
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
46+
source "$0.runfiles/$f" 2>/dev/null || \
47+
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
48+
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
49+
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
50+
# --- end runfiles.bash initialization v2 ---
51+
"""
52+
53+
# Copied from https://github.com/aspect-build/bazel-lib/blob/main/lib/private/paths.bzl
54+
# to avoid a dependency from bazelbuild -> aspect-build
55+
def _to_manifest_path(ctx, file):
56+
"""The runfiles manifest entry path for a file.
57+
58+
This is the full runfiles path of a file including its workspace name as
59+
the first segment. We refert to it as the manifest path as it is the path
60+
flavor that is used for in the runfiles MANIFEST file.
61+
We must avoid using non-normalized paths (workspace/../other_workspace/path)
62+
in order to locate entries by their key.
63+
Args:
64+
ctx: starlark rule execution context
65+
file: a File object
66+
Returns:
67+
The runfiles manifest entry path for a file
68+
"""
69+
70+
if file.short_path.startswith("../"):
71+
return file.short_path[3:]
72+
else:
73+
return ctx.workspace_name + "/" + file.short_path
74+
75+
_exec_tmpl = """\
76+
#!/usr/bin/env bash
77+
{rlocation}
78+
tmp=$(mktemp -d)
79+
# The namefile is just a file with one line, containing the real filename for the wheel.
80+
wheel_filename=$tmp/$(cat "$(rlocation {wheel_namefile})")
81+
cp $(rlocation {wheel}) $wheel_filename
82+
$(rlocation {twine_bin}) upload $wheel_filename "$@"
83+
"""
84+
85+
def _implementation(ctx):
86+
exec = ctx.actions.declare_file(ctx.label.name + ".sh")
87+
88+
ctx.actions.write(exec, content = _exec_tmpl.format(
89+
rlocation = BASH_RLOCATION_FUNCTION,
90+
twine_bin = _to_manifest_path(ctx, ctx.executable.twine_bin),
91+
wheel = _to_manifest_path(ctx, ctx.files.wheel[0]),
92+
wheel_namefile = _to_manifest_path(ctx, ctx.attr.wheel[PyWheelInfo].name_file),
93+
), is_executable = True)
94+
95+
runfiles = ctx.runfiles(ctx.files.twine_bin + ctx.files.wheel + ctx.files._runfiles_lib + [
96+
ctx.attr.wheel[PyWheelInfo].name_file,
97+
])
98+
runfiles = runfiles.merge(ctx.attr.twine_bin[DefaultInfo].default_runfiles)
99+
return [
100+
DefaultInfo(executable = exec, runfiles = runfiles),
101+
]
102+
103+
py_twine_lib = struct(
104+
implementation = _implementation,
105+
attrs = _attrs,
106+
)

python/runfiles/BUILD.bazel

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414

1515
load("//python:defs.bzl", "py_library")
1616
load("//python:packaging.bzl", "py_wheel")
17+
load("//python:pip.bzl", "compile_pip_requirements")
18+
19+
compile_pip_requirements(
20+
name = "requirements",
21+
)
1722

1823
filegroup(
1924
name = "distribution",
@@ -40,10 +45,12 @@ py_wheel(
4045
"Development Status :: 5 - Production/Stable",
4146
"License :: OSI Approved :: Apache Software License",
4247
],
43-
description_file = "README.md",
48+
description_file = "README.rst",
4449
distribution = "bazel_runfiles",
4550
homepage = "https://github.com/bazelbuild/rules_python",
4651
strip_path_prefixes = ["python"],
52+
# This attribute is for the "wheel.publish" target.
53+
twine_bin = "@publish_deps_twine//:rules_python_wheel_entry_point_twine",
4754
version = "{BUILD_EMBED_LABEL}",
4855
visibility = ["//visibility:public"],
4956
deps = [":runfiles"],

python/runfiles/README.md renamed to python/runfiles/README.rst

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
1-
# bazel-runfiles library
1+
bazel-runfiles library
2+
======================
23

34
This is a Bazel Runfiles lookup library for Bazel-built Python binaries and tests.
45

56
Typical Usage
67
-------------
78

89
1. Add the 'runfiles' dependency along with other third-party dependencies, for example in your
9-
`requirements.txt` file.
10+
``requirements.txt`` file.
1011

11-
2. Depend on this runfiles library from your build rule, like you would other third-party libraries.
12+
2. Depend on this runfiles library from your build rule, like you would other third-party libraries::
1213

1314
py_binary(
1415
name = "my_binary",
1516
...
1617
deps = [requirement("runfiles")],
1718
)
1819

19-
3. Import the runfiles library.
20+
3. Import the runfiles library::
2021

2122
import runfiles # not "from runfiles import runfiles"
2223

23-
4. Create a Runfiles object and use rlocation to look up runfile paths:
24+
4. Create a Runfiles object and use rlocation to look up runfile paths::
2425

2526
r = runfiles.Create()
2627
...
@@ -32,15 +33,15 @@ Typical Usage
3233
on the environment variables in os.environ. See `Create()` for more info.
3334

3435
If you want to explicitly create a manifest- or directory-based
35-
implementations, you can do so as follows:
36+
implementations, you can do so as follows::
3637

3738
r1 = runfiles.CreateManifestBased("path/to/foo.runfiles_manifest")
3839

3940
r2 = runfiles.CreateDirectoryBased("path/to/foo.runfiles/")
4041

41-
If you wnat to start subprocesses, and the subprocess can't automatically
42+
If you want to start subprocesses, and the subprocess can't automatically
4243
find the correct runfiles directory, you can explicitly set the right
43-
environment variables for them:
44+
environment variables for them::
4445

4546
import subprocess
4647
import runfiles

python/runfiles/requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
twine

0 commit comments

Comments
 (0)