Skip to content

Commit 36bb556

Browse files
authored
feat(rules): add PyExecutableInfo (bazel-contrib#2166)
The PyExecutableInfo provider exposes executable-specific information that isn't easily accessible outside the target. The main purpose of this provider is to facilitate packaging a binary or deriving a new binary based upon the original. Within rules_python, this will be used to pull the zip-building logic out of executables and into separate rules. Within Google, this will be used for a similar "package a binary" tool. Along the way: * Add runfiles references to sphinx inventory
1 parent b97a5d6 commit 36bb556

11 files changed

+180
-11
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ A brief description of the categories of changes:
4646
* (gazelle) Correctly resolve deps that have top-level module overlap with a gazelle_python.yaml dep module
4747

4848
### Added
49+
* (rules) Executables provide {obj}`PyExecutableInfo`, which contains
50+
executable-specific information useful for packaging an executable or
51+
or deriving a new one from the original.
4952
* (py_wheel) Removed use of bash to avoid failures on Windows machines which do not
5053
have it installed.
5154

docs/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ sphinx_stardocs(
8484
"//python:pip_bzl",
8585
"//python:py_binary_bzl",
8686
"//python:py_cc_link_params_info_bzl",
87+
"//python:py_executable_info_bzl",
8788
"//python:py_library_bzl",
8889
"//python:py_runtime_bzl",
8990
"//python:py_runtime_info_bzl",

python/BUILD.bazel

+6
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ bzl_library(
139139
],
140140
)
141141

142+
bzl_library(
143+
name = "py_executable_info_bzl",
144+
srcs = ["py_executable_info.bzl"],
145+
deps = ["//python/private:py_executable_info_bzl"],
146+
)
147+
142148
bzl_library(
143149
name = "py_import_bzl",
144150
srcs = ["py_import.bzl"],

python/private/BUILD.bazel

+5
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ bzl_library(
220220
],
221221
)
222222

223+
bzl_library(
224+
name = "py_executable_info_bzl",
225+
srcs = ["py_executable_info.bzl"],
226+
)
227+
223228
bzl_library(
224229
name = "py_interpreter_program_bzl",
225230
srcs = ["py_interpreter_program.bzl"],

python/private/common/BUILD.bazel

+2
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,11 @@ bzl_library(
132132
":providers_bzl",
133133
":py_internal_bzl",
134134
"//python/private:flags_bzl",
135+
"//python/private:py_executable_info_bzl",
135136
"//python/private:rules_cc_srcs_bzl",
136137
"//python/private:toolchain_types_bzl",
137138
"@bazel_skylib//lib:dicts",
139+
"@bazel_skylib//lib:structs",
138140
"@bazel_skylib//rules:common_settings",
139141
],
140142
)

python/private/common/py_executable.bzl

+28-10
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
"""Common functionality between test/binary executables."""
1515

1616
load("@bazel_skylib//lib:dicts.bzl", "dicts")
17+
load("@bazel_skylib//lib:structs.bzl", "structs")
1718
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
1819
load("@rules_cc//cc:defs.bzl", "cc_common")
1920
load("//python/private:flags.bzl", "PrecompileAddToRunfilesFlag")
21+
load("//python/private:py_executable_info.bzl", "PyExecutableInfo")
2022
load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")
2123
load(
2224
"//python/private:toolchain_types.bzl",
@@ -221,10 +223,14 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
221223
extra_exec_runfiles = exec_result.extra_runfiles.merge(
222224
ctx.runfiles(transitive_files = exec_result.extra_files_to_build),
223225
)
224-
runfiles_details = struct(
225-
default_runfiles = runfiles_details.default_runfiles.merge(extra_exec_runfiles),
226-
data_runfiles = runfiles_details.data_runfiles.merge(extra_exec_runfiles),
227-
)
226+
227+
# Copy any existing fields in case of company patches.
228+
runfiles_details = struct(**(
229+
structs.to_dict(runfiles_details) | dict(
230+
default_runfiles = runfiles_details.default_runfiles.merge(extra_exec_runfiles),
231+
data_runfiles = runfiles_details.data_runfiles.merge(extra_exec_runfiles),
232+
)
233+
))
228234

229235
return _create_providers(
230236
ctx = ctx,
@@ -400,8 +406,8 @@ def _get_base_runfiles_for_binary(
400406
semantics):
401407
"""Returns the set of runfiles necessary prior to executable creation.
402408
403-
NOTE: The term "common runfiles" refers to the runfiles that both the
404-
default and data runfiles have in common.
409+
NOTE: The term "common runfiles" refers to the runfiles that are common to
410+
runfiles_without_exe, default_runfiles, and data_runfiles.
405411
406412
Args:
407413
ctx: The rule ctx.
@@ -418,6 +424,8 @@ def _get_base_runfiles_for_binary(
418424
struct with attributes:
419425
* default_runfiles: The default runfiles
420426
* data_runfiles: The data runfiles
427+
* runfiles_without_exe: The default runfiles, but without the executable
428+
or files specific to the original program/executable.
421429
"""
422430
common_runfiles_depsets = [main_py_files]
423431

@@ -431,7 +439,6 @@ def _get_base_runfiles_for_binary(
431439
common_runfiles_depsets.append(dep[PyInfo].transitive_pyc_files)
432440

433441
common_runfiles = collect_runfiles(ctx, depset(
434-
direct = [executable],
435442
transitive = common_runfiles_depsets,
436443
))
437444
if extra_deps:
@@ -447,22 +454,27 @@ def _get_base_runfiles_for_binary(
447454
runfiles = common_runfiles,
448455
)
449456

457+
# Don't include build_data.txt in the non-exe runfiles. The build data
458+
# may contain program-specific content (e.g. target name).
459+
runfiles_with_exe = common_runfiles.merge(ctx.runfiles([executable]))
460+
450461
# Don't include build_data.txt in data runfiles. This allows binaries to
451462
# contain other binaries while still using the same fixed location symlink
452463
# for the build_data.txt file. Really, the fixed location symlink should be
453464
# removed and another way found to locate the underlying build data file.
454-
data_runfiles = common_runfiles
465+
data_runfiles = runfiles_with_exe
455466

456467
if is_stamping_enabled(ctx, semantics) and semantics.should_include_build_data(ctx):
457-
default_runfiles = common_runfiles.merge(_create_runfiles_with_build_data(
468+
default_runfiles = runfiles_with_exe.merge(_create_runfiles_with_build_data(
458469
ctx,
459470
semantics.get_central_uncachable_version_file(ctx),
460471
semantics.get_extra_write_build_data_env(ctx),
461472
))
462473
else:
463-
default_runfiles = common_runfiles
474+
default_runfiles = runfiles_with_exe
464475

465476
return struct(
477+
runfiles_without_exe = common_runfiles,
466478
default_runfiles = default_runfiles,
467479
data_runfiles = data_runfiles,
468480
)
@@ -814,6 +826,11 @@ def _create_providers(
814826
),
815827
create_instrumented_files_info(ctx),
816828
_create_run_environment_info(ctx, inherited_environment),
829+
PyExecutableInfo(
830+
main = main_py,
831+
runfiles_without_exe = runfiles_details.runfiles_without_exe,
832+
interpreter_path = runtime_details.executable_interpreter_path,
833+
),
817834
]
818835

819836
# TODO(b/265840007): Make this non-conditional once Google enables
@@ -904,6 +921,7 @@ def create_base_executable_rule(*, attrs, fragments = [], **kwargs):
904921
if "py" not in fragments:
905922
# The list might be frozen, so use concatentation
906923
fragments = fragments + ["py"]
924+
kwargs.setdefault("provides", []).append(PyExecutableInfo)
907925
return rule(
908926
# TODO: add ability to remove attrs, i.e. for imports attr
909927
attrs = dicts.add(EXECUTABLE_ATTRS, attrs),

python/private/py_executable_info.bzl

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Implementation of PyExecutableInfo provider."""
2+
3+
PyExecutableInfo = provider(
4+
doc = """
5+
Information about an executable.
6+
7+
This provider is for executable-specific information (e.g. tests and binaries).
8+
9+
:::{versionadded} 0.36.0
10+
:::
11+
""",
12+
fields = {
13+
"interpreter_path": """
14+
:type: None | str
15+
16+
Path to the Python interpreter to use for running the executable itself (not the
17+
bootstrap script). Either an absolute path (which means it is
18+
platform-specific), or a runfiles-relative path (which means the interpreter
19+
should be within `runtime_files`)
20+
""",
21+
"main": """
22+
:type: File
23+
24+
The user-level entry point file. Usually a `.py` file, but may also be `.pyc`
25+
file if precompiling is enabled.
26+
""",
27+
"runfiles_without_exe": """
28+
:type: runfiles
29+
30+
The runfiles the program needs, but without the original executable,
31+
files only added to support the original executable, or files specific to the
32+
original program.
33+
""",
34+
},
35+
)

python/py_executable_info.bzl

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Provider for executable-specific information.
2+
3+
The `PyExecutableInfo` provider contains information about an executable that
4+
isn't otherwise available from its public attributes or other providers.
5+
6+
It exposes information primarily useful for consumers to package the executable,
7+
or derive a new executable from the base binary.
8+
"""
9+
10+
load("//python/private:py_executable_info.bzl", _PyExecutableInfo = "PyExecutableInfo")
11+
12+
PyExecutableInfo = _PyExecutableInfo

sphinxdocs/inventories/bazel_inventory.txt

+7
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ native.package_name bzl:function 1 rules/lib/toplevel/native#package_name -
6565
native.package_relative_label bzl:function 1 rules/lib/toplevel/native#package_relative_label -
6666
native.repo_name bzl:function 1 rules/lib/toplevel/native#repo_name -
6767
native.repository_name bzl:function 1 rules/lib/toplevel/native#repository_name -
68+
runfiles bzl:type 1 rules/lib/builtins/runfiles -
69+
runfiles.empty_filenames bzl:type 1 rules/lib/builtins/runfiles#empty_filenames -
70+
runfiles.files bzl:type 1 rules/lib/builtins/runfiles#files -
71+
runfiles.merge bzl:type 1 rules/lib/builtins/runfiles#merge -
72+
runfiles.merge_all bzl:type 1 rules/lib/builtins/runfiles#merge_all -
73+
runfiles.root_symlinks bzl:type 1 rules/lib/builtins/runfiles#root_symlinks -
74+
runfiles.symlinks bzl:type 1 rules/lib/builtins/runfiles#symlinks -
6875
str bzl:type 1 rules/lib/string -
6976
struct bzl:type 1 rules/lib/builtins/struct -
7077
toolchain_type bzl:type 1 ules/lib/builtins/toolchain_type.html -

tests/base_rules/py_executable_base_tests.bzl

+11-1
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
1818
load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
1919
load("@rules_testing//lib:truth.bzl", "matching")
2020
load("@rules_testing//lib:util.bzl", rt_util = "util")
21+
load("//python:py_executable_info.bzl", "PyExecutableInfo")
2122
load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility
2223
load("//tests/base_rules:base_tests.bzl", "create_base_tests")
2324
load("//tests/base_rules:util.bzl", "WINDOWS_ATTR", pt_util = "util")
25+
load("//tests/support:py_executable_info_subject.bzl", "PyExecutableInfoSubject")
2426
load("//tests/support:support.bzl", "LINUX_X86_64", "WINDOWS_X86_64")
2527

2628
_BuiltinPyRuntimeInfo = PyRuntimeInfo
@@ -132,11 +134,19 @@ def _test_executable_in_runfiles_impl(env, target):
132134
exe = ".exe"
133135
else:
134136
exe = ""
135-
136137
env.expect.that_target(target).runfiles().contains_at_least([
137138
"{workspace}/{package}/{test_name}_subject" + exe,
138139
])
139140

141+
if rp_config.enable_pystar:
142+
py_exec_info = env.expect.that_target(target).provider(PyExecutableInfo, factory = PyExecutableInfoSubject.new)
143+
py_exec_info.main().path().contains("_subject.py")
144+
py_exec_info.interpreter_path().contains("python")
145+
py_exec_info.runfiles_without_exe().contains_none_of([
146+
"{workspace}/{package}/{test_name}_subject" + exe,
147+
"{workspace}/{package}/{test_name}_subject",
148+
])
149+
140150
def _test_default_main_can_be_generated(name, config):
141151
rt_util.helper_target(
142152
config.rule,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""PyExecutableInfo testing subject."""
15+
16+
load("@rules_testing//lib:truth.bzl", "subjects")
17+
18+
def _py_executable_info_subject_new(info, *, meta):
19+
"""Creates a new `PyExecutableInfoSubject` for a PyExecutableInfo provider instance.
20+
21+
Method: PyExecutableInfoSubject.new
22+
23+
Args:
24+
info: The PyExecutableInfo object
25+
meta: ExpectMeta object.
26+
27+
Returns:
28+
A `PyExecutableInfoSubject` struct
29+
"""
30+
31+
# buildifier: disable=uninitialized
32+
public = struct(
33+
# go/keep-sorted start
34+
actual = info,
35+
interpreter_path = lambda *a, **k: _py_executable_info_subject_interpreter_path(self, *a, **k),
36+
main = lambda *a, **k: _py_executable_info_subject_main(self, *a, **k),
37+
runfiles_without_exe = lambda *a, **k: _py_executable_info_subject_runfiles_without_exe(self, *a, **k),
38+
# go/keep-sorted end
39+
)
40+
self = struct(
41+
actual = info,
42+
meta = meta,
43+
)
44+
return public
45+
46+
def _py_executable_info_subject_interpreter_path(self):
47+
"""Returns a subject for `PyExecutableInfo.interpreter_path`."""
48+
return subjects.str(
49+
self.actual.interpreter_path,
50+
meta = self.meta.derive("interpreter_path()"),
51+
)
52+
53+
def _py_executable_info_subject_main(self):
54+
"""Returns a subject for `PyExecutableInfo.main`."""
55+
return subjects.file(
56+
self.actual.main,
57+
meta = self.meta.derive("main()"),
58+
)
59+
60+
def _py_executable_info_subject_runfiles_without_exe(self):
61+
"""Returns a subject for `PyExecutableInfo.runfiles_without_exe`."""
62+
return subjects.runfiles(
63+
self.actual.runfiles_without_exe,
64+
meta = self.meta.derive("runfiles_without_exe()"),
65+
)
66+
67+
# buildifier: disable=name-conventions
68+
PyExecutableInfoSubject = struct(
69+
new = _py_executable_info_subject_new,
70+
)

0 commit comments

Comments
 (0)