-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
pw_build: Add pw_py_importable_runfile
Adds a rule that exposes runfiles as importable Python libraries to make them easier to use. Bug: b/388526733 Change-Id: Ic1f36077ba492fc06f5540bad320ac41b7558887 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/267335 Presubmit-Verified: CQ Bot Account <[email protected]> Lint: Lint 🤖 <[email protected]> Pigweed-Auto-Submit: Armando Montanez <[email protected]> Reviewed-by: Ted Pudlik <[email protected]> Commit-Queue: Auto-Submit <[email protected]>
- Loading branch information
1 parent
d1c3488
commit 335316a
Showing
11 changed files
with
409 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
# Copyright 2025 The Pigweed Authors | ||
# | ||
# 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 | ||
# | ||
# https://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 rule defining importable Python modules that simplify locating runfiles.""" | ||
|
||
load("@rules_python//python:defs.bzl", "py_library") | ||
|
||
_TEMPLATE = """# FILE GENERATED BY {label}, DO NOT EDIT! | ||
from pw_build.python_runfiles import PythonRunfilesLabelAdapter | ||
RLOCATION = PythonRunfilesLabelAdapter( | ||
runfiles_path = "{runfiles_path}", | ||
source_repo = {source_repo}, | ||
) | ||
""" | ||
|
||
def _generated_runfile_import_impl(ctx): | ||
import_path = "/".join( | ||
[ctx.attr.import_dir] + ctx.attr.import_location.split("."), | ||
) | ||
|
||
generated_import = ctx.actions.declare_file(import_path + ".py") | ||
|
||
if ctx.file.target: | ||
f = ctx.file.target | ||
else: | ||
target_files = ctx.attr.target[DefaultInfo].files.to_list() | ||
if len(target_files) != 1: | ||
fail( | ||
ctx.attr.target.label, | ||
"provides multiple files, which is currently not supported by", | ||
"pw_py_importable_runfile. Please ensure", | ||
ctx.label, | ||
"specifies a `target` with a single file", | ||
) | ||
f = ctx.attr.target[DefaultInfo].files.to_list()[0] | ||
|
||
target_repo_name = ctx.attr.target.label.repo_name | ||
if not target_repo_name: | ||
target_repo_name = ctx.attr.module_name | ||
|
||
# It's valid for this to be None. For example, the repo_name of the | ||
# root repo defaults to None to support the canonical name of "@@". | ||
current_repo_name = ctx.label.repo_name | ||
current_repo_name = '"{}"'.format(current_repo_name) if current_repo_name else None | ||
|
||
runfile_path = f.short_path | ||
runfile_path = "{}/{}".format(target_repo_name, runfile_path) | ||
|
||
ctx.actions.write( | ||
output = generated_import, | ||
content = _TEMPLATE.format( | ||
label = ctx.label, | ||
runfiles_path = runfile_path, | ||
source_repo = current_repo_name, | ||
), | ||
) | ||
|
||
runfiles = ctx.runfiles([ctx.file.target]) | ||
runfiles = runfiles.merge(ctx.attr.target[DefaultInfo].data_runfiles) | ||
|
||
return DefaultInfo( | ||
files = depset(direct = [generated_import]), | ||
runfiles = runfiles, | ||
) | ||
|
||
_generated_runfile_import = rule( | ||
implementation = _generated_runfile_import_impl, | ||
attrs = { | ||
"import_dir": attr.string(mandatory = True), | ||
"import_location": attr.string(mandatory = True), | ||
"module_name": attr.string(), | ||
"target": attr.label(mandatory = True, allow_single_file = True), | ||
}, | ||
) | ||
|
||
def pw_py_importable_runfile(*, name, target = None, import_location = None, **kwargs): | ||
"""An importable py_library that makes loading runfiles easier. | ||
When using Bazel runfiles from Python, ``Rlocation()`` takes two arguments: | ||
1. The ``path`` of the runfiles. This is the apparent repo name joined with | ||
the path within that repo. | ||
2. The ``source_repo`` to evaluate ``path`` from. This is related to how | ||
apparent repo names and canonical repo names are handled by Bazel. | ||
Unfortunately, it's easy to get these arguments wrong. | ||
This generated Python library short-circuits this problem by letting Bazel | ||
generate the correct arguments to ``Rlocation()`` so users don't even have | ||
to think about what to pass. | ||
For example: | ||
``` | ||
# In @bloaty//:BUILD.bazel, or wherever is convenient: | ||
pw_py_importable_runfile( | ||
name = "bloaty_runfiles", | ||
target = "//:bin/bloaty", | ||
import_location = "bloaty.bloaty_binary", | ||
) | ||
# Using the pw_py_importable_runfile from a py_binary in a | ||
# BUILD.bazel file: | ||
py_binary( | ||
name = "my_binary", | ||
srcs = ["my_binary.py"], | ||
main = "my_binary.py", | ||
deps = ["@bloaty//:bloaty_runfiles"], | ||
) | ||
# In my_binary.py: | ||
import bloaty.bloaty_binary | ||
from python.runfiles import runfiles # type: ignore | ||
r = runfiles.Create() | ||
bloaty_path = r.Rlocation(*bloaty.bloaty_binary.RLOCATION) | ||
``` | ||
Note: Because this exposes runfiles as importable Python modules, | ||
the import paths of the generated libraries may collide with existing | ||
Python libraries. When this occurs, you need to extend the import path | ||
of modules with generated files. | ||
Args: | ||
name: name of the target. | ||
import_location: The final Python import path of the generated module. | ||
By default, this is ``path.to.package.label_name``. | ||
target: The file this library exposes as runfiles. | ||
**kwargs: Common attributes to forward both underlying targets. | ||
""" | ||
_generated_py_file = "{}._generated_py_import".format(name) | ||
_virtual_import_dir = "_{}_virtual_imports".format((name)) | ||
if not import_location: | ||
import_location = "/".join(( | ||
native.package_relative_label(":{}".format(name)).package, | ||
name, | ||
)) | ||
_generated_runfile_import( | ||
name = _generated_py_file, | ||
target = target, | ||
import_location = import_location, | ||
import_dir = _virtual_import_dir, | ||
module_name = native.module_name(), | ||
**kwargs | ||
) | ||
py_library( | ||
name = name, | ||
imports = [_virtual_import_dir], | ||
srcs = [":" + _generated_py_file], | ||
data = [target], | ||
deps = [ | ||
Label("//pw_build/py:python_runfiles"), | ||
Label("@rules_python//python/runfiles"), | ||
], | ||
**kwargs | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Copyright 2025 The Pigweed Authors | ||
# | ||
# 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 | ||
# | ||
# https://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. | ||
"""Pigweed's core build-system integration libraries.""" | ||
|
||
from pkgutil import extend_path # type: ignore | ||
|
||
__path__ = extend_path(__path__, __name__) # type: ignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Copyright 2025 The Pigweed Authors | ||
# | ||
# 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 | ||
# | ||
# https://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. | ||
"""Adapters and helpers for Bazel runfiles handling.""" | ||
|
||
from dataclasses import dataclass | ||
from pathlib import Path | ||
|
||
|
||
@dataclass | ||
class PythonRunfilesLabelAdapter: | ||
"""A object that represents Bazel-provided runtime file resource. | ||
This isn't intended for direct instantiation in a codebase. Bazel will | ||
use this as the generated type for pw_py_importable_runfile libraries. | ||
""" | ||
|
||
runfiles_path: Path | ||
source_repo: str | ||
|
||
def __iter__(self): | ||
"""Custom iterator to support passing an adapter to Rlocation().""" | ||
yield self.runfiles_path | ||
yield self.source_repo |
Oops, something went wrong.