fix(py_wheel): Avoid reliance on bash in `py_wheel` macro. (#2171)
While trying to modernize an older repo I tried building wheels on
windows and ran into a failure:
```
Windows Subsystem for Linux has no installed distributions.
Use 'wsl.exe --list --online' to list available distributions
and 'wsl.exe --install <Distro>' to install.
Distributions can also be installed by visiting the Microsoft Store:
https://aka.ms/wlstore
Errorcode: Bash/Service/CreateInstance/GetDefaultDistro/WSL_E_DEFAULT_DISTRO_NOT_FOUND
```
This appears to be caused by the `py_wheel_dist` rule which gets caught
by `//...`. This target should be considered a side-effect/optional
target of `py_wheel`. To fix:
1. Mark the target as `manual`, so it's only built when explicitly
requested.
2. Implement copying to the directory with a Python program instead of
shell, so bash
isn't required.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee71951..bca643f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,7 +46,8 @@
* (gazelle) Correctly resolve deps that have top-level module overlap with a gazelle_python.yaml dep module
### Added
-* Nothing yet
+* (py_wheel) Removed use of bash to avoid failures on Windows machines which do not
+ have it installed.
### Removed
* Nothing yet
diff --git a/python/packaging.bzl b/python/packaging.bzl
index a5ac25b..17f72a7 100644
--- a/python/packaging.bzl
+++ b/python/packaging.bzl
@@ -35,25 +35,28 @@
attrs = py_package_lib.attrs,
)
-# Based on https://github.com/aspect-build/bazel-lib/tree/main/lib/private/copy_to_directory.bzl
-# Avoiding a bazelbuild -> aspect-build dependency :(
def _py_wheel_dist_impl(ctx):
- dir = ctx.actions.declare_directory(ctx.attr.out)
+ out = ctx.actions.declare_directory(ctx.attr.out)
name_file = ctx.attr.wheel[PyWheelInfo].name_file
- cmds = [
- "mkdir -p \"%s\"" % dir.path,
- """cp "{}" "{}/$(cat "{}")" """.format(ctx.files.wheel[0].path, dir.path, name_file.path),
- ]
- ctx.actions.run_shell(
- inputs = ctx.files.wheel + [name_file],
- outputs = [dir],
- command = "\n".join(cmds),
- mnemonic = "CopyToDirectory",
- progress_message = "Copying files to directory",
- use_default_shell_env = True,
+ wheel = ctx.attr.wheel[PyWheelInfo].wheel
+
+ args = ctx.actions.args()
+ args.add("--wheel", wheel)
+ args.add("--name_file", name_file)
+ args.add("--output", out.path)
+
+ ctx.actions.run(
+ mnemonic = "PyWheelDistDir",
+ executable = ctx.executable._copier,
+ inputs = [wheel, name_file],
+ outputs = [out],
+ arguments = [args],
)
return [
- DefaultInfo(files = depset([dir]), runfiles = ctx.runfiles([dir])),
+ DefaultInfo(
+ files = depset([out]),
+ runfiles = ctx.runfiles([out]),
+ ),
]
py_wheel_dist = rule(
@@ -67,12 +70,28 @@
""",
implementation = _py_wheel_dist_impl,
attrs = {
- "out": attr.string(doc = "name of the resulting directory", mandatory = True),
- "wheel": attr.label(doc = "a [py_wheel target](#py_wheel)", providers = [PyWheelInfo]),
+ "out": attr.string(
+ doc = "name of the resulting directory",
+ mandatory = True,
+ ),
+ "wheel": attr.label(
+ doc = "a [py_wheel target](#py_wheel)",
+ providers = [PyWheelInfo],
+ ),
+ "_copier": attr.label(
+ cfg = "exec",
+ executable = True,
+ default = Label("//python/private:py_wheel_dist"),
+ ),
},
)
-def py_wheel(name, twine = None, twine_binary = Label("//tools/publish:twine") if BZLMOD_ENABLED else None, publish_args = [], **kwargs):
+def py_wheel(
+ name,
+ twine = None,
+ twine_binary = Label("//tools/publish:twine") if BZLMOD_ENABLED else None,
+ publish_args = [],
+ **kwargs):
"""Builds a Python Wheel.
Wheels are Python distribution format defined in https://www.python.org/dev/peps/pep-0427/.
@@ -153,24 +172,31 @@
Note that you can also pass additional args to the bazel run command as in the example above.
**kwargs: other named parameters passed to the underlying [py_wheel rule](#py_wheel_rule)
"""
- _dist_target = "{}.dist".format(name)
+ tags = kwargs.pop("tags", [])
+ manual_tags = depset(tags + ["manual"]).to_list()
+
+ dist_target = "{}.dist".format(name)
py_wheel_dist(
- name = _dist_target,
+ name = dist_target,
wheel = name,
out = kwargs.pop("dist_folder", "{}_dist".format(name)),
+ tags = manual_tags,
**copy_propagating_kwargs(kwargs)
)
- _py_wheel(name = name, **kwargs)
+ _py_wheel(
+ name = name,
+ tags = tags,
+ **kwargs
+ )
twine_args = []
if twine or twine_binary:
twine_args = ["upload"]
twine_args.extend(publish_args)
- twine_args.append("$(rootpath :{})/*".format(_dist_target))
+ twine_args.append("$(rootpath :{})/*".format(dist_target))
if twine_binary:
- twine_kwargs = {"tags": ["manual"]}
native_binary(
name = "{}.publish".format(name),
src = twine_binary,
@@ -179,9 +205,10 @@
"//conditions:default": "{}.publish_script".format(name),
}),
args = twine_args,
- data = [_dist_target],
+ data = [dist_target],
+ tags = manual_tags,
visibility = kwargs.get("visibility"),
- **copy_propagating_kwargs(kwargs, twine_kwargs)
+ **copy_propagating_kwargs(kwargs)
)
elif twine:
if not twine.endswith(":pkg"):
@@ -193,10 +220,11 @@
name = "{}.publish".format(name),
srcs = [twine_main],
args = twine_args,
- data = [_dist_target],
+ data = [dist_target],
imports = ["."],
main = twine_main,
deps = [twine],
+ tags = manual_tags,
visibility = kwargs.get("visibility"),
**copy_propagating_kwargs(kwargs)
)
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index 488862f..7362a4c 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -440,6 +440,12 @@
],
)
+py_binary(
+ name = "py_wheel_dist",
+ srcs = ["py_wheel_dist.py"],
+ visibility = ["//visibility:public"],
+)
+
py_library(
name = "py_console_script_gen_lib",
srcs = ["py_console_script_gen.py"],
diff --git a/python/private/py_wheel_dist.py b/python/private/py_wheel_dist.py
new file mode 100644
index 0000000..3af3345
--- /dev/null
+++ b/python/private/py_wheel_dist.py
@@ -0,0 +1,41 @@
+"""A utility for generating the output directory for `py_wheel_dist`."""
+
+import argparse
+import shutil
+from pathlib import Path
+
+
+def parse_args() -> argparse.Namespace:
+ """Parse command line arguments."""
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument(
+ "--wheel", type=Path, required=True, help="The path to a wheel."
+ )
+ parser.add_argument(
+ "--name_file",
+ type=Path,
+ required=True,
+ help="A file containing the sanitized name of the wheel.",
+ )
+ parser.add_argument(
+ "--output",
+ type=Path,
+ required=True,
+ help="The output location to copy the wheel to.",
+ )
+
+ return parser.parse_args()
+
+
+def main() -> None:
+ """The main entrypoint."""
+ args = parse_args()
+
+ wheel_name = args.name_file.read_text(encoding="utf-8").strip()
+ args.output.mkdir(exist_ok=True, parents=True)
+ shutil.copyfile(args.wheel, args.output / wheel_name)
+
+
+if __name__ == "__main__":
+ main()