Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(proto): shim to upstream for py_proto_library #2581

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Unreleased changes template.
env var.
* (pypi) Downgraded versions of packages: `pip` from `24.3.2` to `24.0.0` and
`packaging` from `24.2` to `24.0`.
* (proto) now `rules_python` just uses the implementation of `py_proto_library`
from the [upstream](https://github.com/protocolbuffers/protobuf/blob/main/bazel/py_proto_library.bzl).

{#v0-0-0-fixed}
### Fixed
Expand Down
3 changes: 1 addition & 2 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(name = "rules_cc", version = "0.0.16")
bazel_dep(name = "platforms", version = "0.0.4")

# Those are loaded only when using py_proto_library
bazel_dep(name = "rules_proto", version = "7.0.2")
# Those should be loaded only when using py_proto_library
bazel_dep(name = "protobuf", version = "29.0-rc2", repo_name = "com_google_protobuf")

internal_deps = use_extension("//python/private:internal_deps.bzl", "internal_deps")
Expand Down
8 changes: 1 addition & 7 deletions examples/bzlmod/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ local_path_override(
bazel_dep(name = "rules_proto", version = "6.0.0-rc1")

# (py_proto_library specific) Add the protobuf library for well-known types (e.g. `Any`, `Timestamp`, etc)
bazel_dep(name = "protobuf", version = "27.0", repo_name = "com_google_protobuf")
bazel_dep(name = "protobuf", version = "29.3", repo_name = "com_google_protobuf")

# Only needed to make rules_python's CI happy. rules_java 8.3.0+ is needed so
# that --java_runtime_version=remotejdk_11 works with Bazel 8.
Expand Down Expand Up @@ -273,11 +273,5 @@ local_path_override(
path = "other_module",
)

bazel_dep(name = "foo_external", version = "")
local_path_override(
module_name = "foo_external",
path = "py_proto_library/foo_external",
)

# example test dependencies
bazel_dep(name = "rules_shell", version = "0.3.0", dev_dependency = True)
16 changes: 0 additions & 16 deletions examples/bzlmod/py_proto_library/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
load("@bazel_skylib//rules:native_binary.bzl", "native_test")
load("@rules_python//python:py_test.bzl", "py_test")

py_test(
Expand All @@ -17,18 +16,3 @@ py_test(
"//py_proto_library/example.com/another_proto:message_proto_py_pb2",
],
)

# Regression test for https://github.com/bazelbuild/rules_python/issues/2515
#
# This test failed before https://github.com/bazelbuild/rules_python/pull/2516
# when ran with --legacy_external_runfiles=False (default in Bazel 8.0.0).
native_test(
name = "external_import_test",
src = "@foo_external//:py_binary_with_proto",
# Incompatible with Windows: native_test wrapping a py_binary doesn't work
# on Windows.
target_compatible_with = select({
"@platforms//os:windows": ["@platforms//:incompatible"],
"//conditions:default": [],
}),
)
22 changes: 0 additions & 22 deletions examples/bzlmod/py_proto_library/foo_external/BUILD.bazel

This file was deleted.

8 changes: 0 additions & 8 deletions examples/bzlmod/py_proto_library/foo_external/MODULE.bazel

This file was deleted.

Empty file.

This file was deleted.

This file was deleted.

7 changes: 0 additions & 7 deletions internal_dev_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,6 @@ def rules_python_internal_deps():
],
)

http_archive(
name = "rules_proto",
sha256 = "904a8097fae42a690c8e08d805210e40cccb069f5f9a0f6727cf4faa7bed2c9c",
strip_prefix = "rules_proto-6.0.0-rc1",
url = "https://github.com/bazelbuild/rules_proto/releases/download/6.0.0-rc1/rules_proto-6.0.0-rc1.tar.gz",
)

http_archive(
name = "com_google_protobuf",
sha256 = "23082dca1ca73a1e9c6cbe40097b41e81f71f3b4d6201e36c134acc30a1b3660",
Expand Down
16 changes: 1 addition & 15 deletions python/private/proto/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# limitations under the License.

load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("@com_google_protobuf//bazel/toolchains:proto_lang_toolchain.bzl", "proto_lang_toolchain")

package(default_visibility = ["//visibility:private"])

Expand All @@ -30,19 +29,6 @@ bzl_library(
srcs = ["py_proto_library.bzl"],
visibility = ["//python:__pkg__"],
deps = [
"//python:py_info_bzl",
"@com_google_protobuf//bazel/common:proto_common_bzl",
"@com_google_protobuf//bazel/common:proto_info_bzl",
"@rules_proto//proto:defs",
"@com_google_protobuf//bazel:py_proto_library_bzl",
],
)

proto_lang_toolchain(
name = "python_toolchain",
command_line = "--python_out=%s",
progress_message = "Generating Python proto_library %{label}",
runtime = "@com_google_protobuf//:protobuf_python",
# NOTE: This isn't *actually* public. It's an implicit dependency of py_proto_library,
# so must be public so user usages of the rule can reference it.
visibility = ["//visibility:public"],
)
239 changes: 13 additions & 226 deletions python/private/proto/py_proto_library.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,231 +14,18 @@

"""The implementation of the `py_proto_library` rule and its aspect."""

load("@com_google_protobuf//bazel/common:proto_common.bzl", "proto_common")
load("@com_google_protobuf//bazel/common:proto_info.bzl", "ProtoInfo")
load("//python:py_info.bzl", "PyInfo")
load("//python/api:api.bzl", _py_common = "py_common")

PY_PROTO_TOOLCHAIN = "@rules_python//python/proto:toolchain_type"

_PyProtoInfo = provider(
doc = "Encapsulates information needed by the Python proto rules.",
fields = {
"imports": """
(depset[str]) The field forwarding PyInfo.imports coming from
the proto language runtime dependency.""",
"py_info": "PyInfo from proto runtime (or other deps) to propagate.",
"runfiles_from_proto_deps": """
(depset[File]) Files from the transitive closure implicit proto
dependencies""",
"transitive_sources": """(depset[File]) The Python sources.""",
},
)

def _filter_provider(provider, *attrs):
return [dep[provider] for attr in attrs for dep in attr if provider in dep]

def _incompatible_toolchains_enabled():
return getattr(proto_common, "INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION", False)

def _py_proto_aspect_impl(target, ctx):
"""Generates and compiles Python code for a proto_library.

The function runs protobuf compiler on the `proto_library` target generating
a .py file for each .proto file.

Args:
target: (Target) A target providing `ProtoInfo`. Usually this means a
`proto_library` target, but not always; you must expect to visit
non-`proto_library` targets, too.
ctx: (RuleContext) The rule context.

Returns:
([_PyProtoInfo]) Providers collecting transitive information about
generated files.
"""
_proto_library = ctx.rule.attr

# Check Proto file names
for proto in target[ProtoInfo].direct_sources:
if proto.is_source and "-" in proto.dirname:
fail("Cannot generate Python code for a .proto whose path contains '-' ({}).".format(
proto.path,
))

if _incompatible_toolchains_enabled():
toolchain = ctx.toolchains[PY_PROTO_TOOLCHAIN]
if not toolchain:
fail("No toolchains registered for '%s'." % PY_PROTO_TOOLCHAIN)
proto_lang_toolchain_info = toolchain.proto
else:
proto_lang_toolchain_info = getattr(ctx.attr, "_aspect_proto_toolchain")[proto_common.ProtoLangToolchainInfo]

py_common = _py_common.get(ctx)
py_info = py_common.PyInfoBuilder().merge_target(
proto_lang_toolchain_info.runtime,
).build()

api_deps = [proto_lang_toolchain_info.runtime]

generated_sources = []
proto_info = target[ProtoInfo]
proto_root = proto_info.proto_source_root
if proto_info.direct_sources:
# Generate py files
generated_sources = proto_common.declare_generated_files(
actions = ctx.actions,
proto_info = proto_info,
extension = "_pb2.py",
name_mapper = lambda name: name.replace("-", "_").replace(".", "/"),
load("@com_google_protobuf//bazel:py_proto_library.bzl", _py_proto_library = "py_proto_library")
load("//python/private:deprecation.bzl", "with_deprecation")
load("//python/private:text_util.bzl", "render")

def py_proto_library(name, **kwargs):
return _py_proto_library(
name = name,
**with_deprecation.symbol(
kwargs,
symbol_name = "py_proto_library",
new_load = "@com_google_protobuf//bazel:py_proto_library.bzl",
old_load = "@rules_python//python:proto.bzl",
snippet = render.call(name, **{k: repr(v) for k, v in kwargs.items()}),
)

# Handles multiple repository and virtual import cases
if proto_root.startswith(ctx.bin_dir.path):
proto_root = proto_root[len(ctx.bin_dir.path) + 1:]

plugin_output = ctx.bin_dir.path + "/" + proto_root

# Import path within the runfiles tree
if proto_root.startswith("external/"):
proto_root = proto_root[len("external") + 1:]
else:
proto_root = ctx.workspace_name + "/" + proto_root

proto_common.compile(
actions = ctx.actions,
proto_info = proto_info,
proto_lang_toolchain_info = proto_lang_toolchain_info,
generated_files = generated_sources,
plugin_output = plugin_output,
)

# Generated sources == Python sources
python_sources = generated_sources

deps = _filter_provider(_PyProtoInfo, getattr(_proto_library, "deps", []))
runfiles_from_proto_deps = depset(
transitive = [dep[DefaultInfo].default_runfiles.files for dep in api_deps] +
[dep.runfiles_from_proto_deps for dep in deps],
)
transitive_sources = depset(
direct = python_sources,
transitive = [dep.transitive_sources for dep in deps],
)

return [
_PyProtoInfo(
imports = depset(
# Adding to PYTHONPATH so the generated modules can be
# imported. This is necessary when there is
# strip_import_prefix, the Python modules are generated under
# _virtual_imports. But it's undesirable otherwise, because it
# will put the repo root at the top of the PYTHONPATH, ahead of
# directories added through `imports` attributes.
[proto_root] if "_virtual_imports" in proto_root else [],
transitive = [dep[PyInfo].imports for dep in api_deps] + [dep.imports for dep in deps],
),
runfiles_from_proto_deps = runfiles_from_proto_deps,
transitive_sources = transitive_sources,
py_info = py_info,
),
]

_py_proto_aspect = aspect(
implementation = _py_proto_aspect_impl,
attrs = _py_common.API_ATTRS | (
{} if _incompatible_toolchains_enabled() else {
"_aspect_proto_toolchain": attr.label(
default = ":python_toolchain",
),
}
),
attr_aspects = ["deps"],
required_providers = [ProtoInfo],
provides = [_PyProtoInfo],
toolchains = [PY_PROTO_TOOLCHAIN] if _incompatible_toolchains_enabled() else [],
)

def _py_proto_library_rule(ctx):
"""Merges results of `py_proto_aspect` in `deps`.

Args:
ctx: (RuleContext) The rule context.
Returns:
([PyInfo, DefaultInfo, OutputGroupInfo])
"""
if not ctx.attr.deps:
fail("'deps' attribute mustn't be empty.")

pyproto_infos = _filter_provider(_PyProtoInfo, ctx.attr.deps)
default_outputs = depset(
transitive = [info.transitive_sources for info in pyproto_infos],
)

py_common = _py_common.get(ctx)

py_info = py_common.PyInfoBuilder()
py_info.set_has_py2_only_sources(False)
py_info.set_has_py3_only_sources(False)
py_info.transitive_sources.add(default_outputs)
py_info.imports.add([info.imports for info in pyproto_infos])
py_info.merge_all([
pyproto_info.py_info
for pyproto_info in pyproto_infos
])
return [
DefaultInfo(
files = default_outputs,
default_runfiles = ctx.runfiles(transitive_files = depset(
transitive =
[default_outputs] +
[info.runfiles_from_proto_deps for info in pyproto_infos],
)),
),
OutputGroupInfo(
default = depset(),
),
py_info.build(),
]

py_proto_library = rule(
implementation = _py_proto_library_rule,
doc = """
Use `py_proto_library` to generate Python libraries from `.proto` files.

The convention is to name the `py_proto_library` rule `foo_py_pb2`,
when it is wrapping `proto_library` rule `foo_proto`.

`deps` must point to a `proto_library` rule.

Example:

```starlark
py_library(
name = "lib",
deps = [":foo_py_pb2"],
)

py_proto_library(
name = "foo_py_pb2",
deps = [":foo_proto"],
)

proto_library(
name = "foo_proto",
srcs = ["foo.proto"],
)
```""",
attrs = {
"deps": attr.label_list(
doc = """
The list of `proto_library` rules to generate Python libraries for.

Usually this is just the one target: the proto library of interest.
It can be any target providing `ProtoInfo`.""",
providers = [ProtoInfo],
aspects = [_py_proto_aspect],
),
} | _py_common.API_ATTRS,
provides = [PyInfo],
)