diff --git a/e2e/cases/bin-to-bin-deps/BUILD.bazel b/e2e/cases/bin-to-bin-deps/BUILD.bazel new file mode 100644 index 00000000..c4e12216 --- /dev/null +++ b/e2e/cases/bin-to-bin-deps/BUILD.bazel @@ -0,0 +1,32 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_test", "py_library") + +py_library( + name = "lib", + imports = ["."], + srcs = ["lib.py"] +) + +py_binary( + name = "bin_a", + srcs = ["bin_a.py"], + deps = [ + ":lib" + ] +) + +py_binary( + name = "bin_b", + srcs = ["bin_b.py"], + deps = [ + ":bin_a" + ] +) + +py_test( + name = "test", + srcs = ["test.py"], + deps = [ + ":bin_a", + ":bin_b", + ] +) \ No newline at end of file diff --git a/e2e/cases/bin-to-bin-deps/bin_a.py b/e2e/cases/bin-to-bin-deps/bin_a.py new file mode 100644 index 00000000..8b367f3b --- /dev/null +++ b/e2e/cases/bin-to-bin-deps/bin_a.py @@ -0,0 +1 @@ +print("bin_a: Hello!") \ No newline at end of file diff --git a/e2e/cases/bin-to-bin-deps/bin_b.py b/e2e/cases/bin-to-bin-deps/bin_b.py new file mode 100644 index 00000000..1e3d3f8e --- /dev/null +++ b/e2e/cases/bin-to-bin-deps/bin_b.py @@ -0,0 +1,5 @@ +from lib import get_msg + +print("bin_b: Hello!") + +print(get_msg()) \ No newline at end of file diff --git a/e2e/cases/bin-to-bin-deps/lib.py b/e2e/cases/bin-to-bin-deps/lib.py new file mode 100644 index 00000000..2917ee32 --- /dev/null +++ b/e2e/cases/bin-to-bin-deps/lib.py @@ -0,0 +1,3 @@ + +def get_msg(): + return "Hello from lib!" \ No newline at end of file diff --git a/e2e/cases/bin-to-bin-deps/test.py b/e2e/cases/bin-to-bin-deps/test.py new file mode 100644 index 00000000..e5c17241 --- /dev/null +++ b/e2e/cases/bin-to-bin-deps/test.py @@ -0,0 +1,4 @@ +from lib import get_msg + +def test_lib(): + assert "Hello from lib!" == get_msg() \ No newline at end of file diff --git a/py/private/BUILD.bazel b/py/private/BUILD.bazel index eb36e61b..79da4178 100644 --- a/py/private/BUILD.bazel +++ b/py/private/BUILD.bazel @@ -1,7 +1,14 @@ load("@bazel_lib//:bzl_library.bzl", "bzl_library") +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") package(default_visibility = ["//py:__subpackages__"]) +bool_flag( + name = "bin_to_lib_flag", + build_setting_default = True, + visibility = ["//:__subpackages__"] +) + exports_files( [ "run.tmpl.sh", diff --git a/py/private/bin_to_lib.bzl b/py/private/bin_to_lib.bzl new file mode 100644 index 00000000..81885b0c --- /dev/null +++ b/py/private/bin_to_lib.bzl @@ -0,0 +1,75 @@ +""" + +Sometimes it is desirable for a python binary or venv to depend on another binary target or venv. +Here we reify bin-dep-bin semantics by transitioning a binary into a library if it is + depended upon by another binary. + +This functionality has the following benefits: +1. py_venv_binary behaves more like the original py_binary rule. +2. Easier composition of venvs +3. Better merge semantics for existing binary rules. + +""" + +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") +load("@rules_python//python:defs.bzl", "PyInfo") +load("//py/private:providers.bzl", "PyVirtualInfo") +load(":transitions.bzl", "bin_to_lib_transition") + +def _find_provider(providers, prov_type_name): + for provider in providers: + if type(provider) == prov_type_name: + return provider + return None + +def _resolve_providers(ctx, lib_providers, bin_providers): + new_providers = [] + bin_default_info = _find_provider(bin_providers, "DefaultInfo") + for p in lib_providers: + if type(p) == "DefaultInfo": + new_providers.append( + DefaultInfo( + files = p.files, + data_runfiles = p.data_runfiles, + default_runfiles = p.default_runfiles, + executable = ctx.outputs.executable, + ), + ) + continue + new_providers.append(p) + return new_providers + +def wrap_with_bin_to_lib(bin_rule, lib_rule): + def helper(ctx): + if not ctx.attr._binary_mode: + fail("Wrapped rule missing required attributes.") + + bin_providers = bin_rule(ctx) + if ctx.attr._binary_mode[BuildSettingInfo].value: + return bin_providers + + lib_providers = lib_rule(ctx) + + # Looks like we need to extract and propagate some files from the original binary. + return _resolve_providers( + ctx, + lib_providers, + bin_providers, + ) + + return helper + +bin_to_lib = struct( + wrapper = wrap_with_bin_to_lib, + attribs = dict({ + "_binary_mode": attr.label( + default = "//py/private:bin_to_lib_flag", + ), + "deps": attr.label_list( + doc = "Targets that produce Python code, commonly `py_library` rules.", + providers = [[PyInfo], [PyVirtualInfo], [CcInfo]], + cfg = bin_to_lib_transition, + ), + }), +) diff --git a/py/private/py_binary.bzl b/py/private/py_binary.bzl index cfc0ea3f..668fba42 100644 --- a/py/private/py_binary.bzl +++ b/py/private/py_binary.bzl @@ -8,6 +8,8 @@ load("//py/private:py_semantics.bzl", _py_semantics = "semantics") load("//py/private/toolchain:types.bzl", "PY_TOOLCHAIN", "VENV_TOOLCHAIN") load(":transitions.bzl", "python_version_transition") +load(":bin_to_lib.bzl", "bin_to_lib") + def _dict_to_exports(env): return [ "export %s=\"%s\"" % (k, v) @@ -228,8 +230,11 @@ py_base = struct( py_binary = rule( doc = "Run a Python program under Bazel. Most users should use the [py_binary macro](#py_binary) instead of loading this directly.", - implementation = py_base.implementation, - attrs = py_base.attrs, + implementation = bin_to_lib.wrapper( + py_base.implementation, + _py_library.implementation + ), + attrs = py_base.attrs | bin_to_lib.attribs, toolchains = py_base.toolchains, executable = True, cfg = py_base.cfg, diff --git a/py/private/py_venv/py_venv.bzl b/py/private/py_venv/py_venv.bzl index 0f3788da..e56e77b7 100644 --- a/py/private/py_venv/py_venv.bzl +++ b/py/private/py_venv/py_venv.bzl @@ -2,6 +2,7 @@ load("@aspect_bazel_lib//lib:expand_make_vars.bzl", "expand_locations", "expand_variables") load("@aspect_bazel_lib//lib:paths.bzl", "BASH_RLOCATION_FUNCTION", "to_rlocation_path") +load("//py/private:bin_to_lib.bzl", "bin_to_lib") load("//py/private:py_library.bzl", _py_library = "py_library_utils") load("//py/private:py_semantics.bzl", _py_semantics = "semantics") load("//py/private:transitions.bzl", "python_version_transition") @@ -392,8 +393,11 @@ py_venv_base = struct( _py_venv = rule( doc = """Build a Python virtual environment and execute its interpreter.""", - implementation = _py_venv_rule_impl, - attrs = py_venv_base.attrs, + implementation = bin_to_lib.wrapper( + _py_venv_rule_impl, + _py_library.implementation, + ), + attrs = py_venv_base.attrs | bin_to_lib.attribs, toolchains = py_venv_base.toolchains, executable = True, cfg = py_venv_base.cfg, @@ -411,8 +415,12 @@ def _wrap_with_debug(rule): _py_venv_binary = rule( doc = """Run a Python program under Bazel using a virtualenv.""", - implementation = _py_venv_binary_impl, - attrs = py_venv_base.attrs | py_venv_base.binary_attrs, + implementation = bin_to_lib.wrapper( + _py_venv_binary_impl, + _py_library.implementation, + ), + #implementation = _py_venv_binary_impl, + attrs = py_venv_base.attrs | py_venv_base.binary_attrs | bin_to_lib.attribs, toolchains = py_venv_base.toolchains, executable = True, cfg = py_venv_base.cfg, diff --git a/py/private/transitions.bzl b/py/private/transitions.bzl index 13660e4e..5d91e370 100644 --- a/py/private/transitions.bzl +++ b/py/private/transitions.bzl @@ -30,5 +30,20 @@ python_transition = transition( ], ) +def _bin_to_lib_transition_impl(settings, attr): + return { + "//py/private:bin_to_lib_flag": False, + } + +bin_to_lib_transition = transition( + implementation = _bin_to_lib_transition_impl, + inputs = [ + "//py/private:bin_to_lib_flag", + ], + outputs = [ + "//py/private:bin_to_lib_flag", + ], +) + # The old name, FIXME: refactor this out python_version_transition = python_transition