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(rules): add PyExecutableInfo #2166

Merged
merged 2 commits into from
Sep 2, 2024
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ A brief description of the categories of changes:
* (gazelle) Correctly resolve deps that have top-level module overlap with a gazelle_python.yaml dep module

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

Expand Down
1 change: 1 addition & 0 deletions docs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ sphinx_stardocs(
"//python:pip_bzl",
"//python:py_binary_bzl",
"//python:py_cc_link_params_info_bzl",
"//python:py_executable_info_bzl",
"//python:py_library_bzl",
"//python:py_runtime_bzl",
"//python:py_runtime_info_bzl",
Expand Down
6 changes: 6 additions & 0 deletions python/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ bzl_library(
],
)

bzl_library(
name = "py_executable_info_bzl",
srcs = ["py_executable_info.bzl"],
deps = ["//python/private:py_executable_info_bzl"],
)

bzl_library(
name = "py_import_bzl",
srcs = ["py_import.bzl"],
Expand Down
5 changes: 5 additions & 0 deletions python/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ bzl_library(
],
)

bzl_library(
name = "py_executable_info_bzl",
srcs = ["py_executable_info.bzl"],
)

bzl_library(
name = "py_interpreter_program_bzl",
srcs = ["py_interpreter_program.bzl"],
Expand Down
2 changes: 2 additions & 0 deletions python/private/common/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,11 @@ bzl_library(
":providers_bzl",
":py_internal_bzl",
"//python/private:flags_bzl",
"//python/private:py_executable_info_bzl",
"//python/private:rules_cc_srcs_bzl",
"//python/private:toolchain_types_bzl",
"@bazel_skylib//lib:dicts",
"@bazel_skylib//lib:structs",
"@bazel_skylib//rules:common_settings",
],
)
Expand Down
38 changes: 28 additions & 10 deletions python/private/common/py_executable.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
"""Common functionality between test/binary executables."""

load("@bazel_skylib//lib:dicts.bzl", "dicts")
load("@bazel_skylib//lib:structs.bzl", "structs")
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load("@rules_cc//cc:defs.bzl", "cc_common")
load("//python/private:flags.bzl", "PrecompileAddToRunfilesFlag")
load("//python/private:py_executable_info.bzl", "PyExecutableInfo")
load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")
load(
"//python/private:toolchain_types.bzl",
Expand Down Expand Up @@ -221,10 +223,14 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
extra_exec_runfiles = exec_result.extra_runfiles.merge(
ctx.runfiles(transitive_files = exec_result.extra_files_to_build),
)
runfiles_details = struct(
default_runfiles = runfiles_details.default_runfiles.merge(extra_exec_runfiles),
data_runfiles = runfiles_details.data_runfiles.merge(extra_exec_runfiles),
)

# Copy any existing fields in case of company patches.
runfiles_details = struct(**(
structs.to_dict(runfiles_details) | dict(
default_runfiles = runfiles_details.default_runfiles.merge(extra_exec_runfiles),
data_runfiles = runfiles_details.data_runfiles.merge(extra_exec_runfiles),
)
))

return _create_providers(
ctx = ctx,
Expand Down Expand Up @@ -400,8 +406,8 @@ def _get_base_runfiles_for_binary(
semantics):
"""Returns the set of runfiles necessary prior to executable creation.

NOTE: The term "common runfiles" refers to the runfiles that both the
default and data runfiles have in common.
NOTE: The term "common runfiles" refers to the runfiles that are common to
runfiles_without_exe, default_runfiles, and data_runfiles.

Args:
ctx: The rule ctx.
Expand All @@ -418,6 +424,8 @@ def _get_base_runfiles_for_binary(
struct with attributes:
* default_runfiles: The default runfiles
* data_runfiles: The data runfiles
* runfiles_without_exe: The default runfiles, but without the executable
or files specific to the original program/executable.
"""
common_runfiles_depsets = [main_py_files]

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

common_runfiles = collect_runfiles(ctx, depset(
direct = [executable],
transitive = common_runfiles_depsets,
))
if extra_deps:
Expand All @@ -447,22 +454,27 @@ def _get_base_runfiles_for_binary(
runfiles = common_runfiles,
)

# Don't include build_data.txt in the non-exe runfiles. The build data
# may contain program-specific content (e.g. target name).
runfiles_with_exe = common_runfiles.merge(ctx.runfiles([executable]))

# Don't include build_data.txt in data runfiles. This allows binaries to
# contain other binaries while still using the same fixed location symlink
# for the build_data.txt file. Really, the fixed location symlink should be
# removed and another way found to locate the underlying build data file.
data_runfiles = common_runfiles
data_runfiles = runfiles_with_exe

if is_stamping_enabled(ctx, semantics) and semantics.should_include_build_data(ctx):
default_runfiles = common_runfiles.merge(_create_runfiles_with_build_data(
default_runfiles = runfiles_with_exe.merge(_create_runfiles_with_build_data(
ctx,
semantics.get_central_uncachable_version_file(ctx),
semantics.get_extra_write_build_data_env(ctx),
))
else:
default_runfiles = common_runfiles
default_runfiles = runfiles_with_exe

return struct(
runfiles_without_exe = common_runfiles,
default_runfiles = default_runfiles,
data_runfiles = data_runfiles,
)
Expand Down Expand Up @@ -814,6 +826,11 @@ def _create_providers(
),
create_instrumented_files_info(ctx),
_create_run_environment_info(ctx, inherited_environment),
PyExecutableInfo(
main = main_py,
runfiles_without_exe = runfiles_details.runfiles_without_exe,
interpreter_path = runtime_details.executable_interpreter_path,
),
]

# TODO(b/265840007): Make this non-conditional once Google enables
Expand Down Expand Up @@ -904,6 +921,7 @@ def create_base_executable_rule(*, attrs, fragments = [], **kwargs):
if "py" not in fragments:
# The list might be frozen, so use concatentation
fragments = fragments + ["py"]
kwargs.setdefault("provides", []).append(PyExecutableInfo)
return rule(
# TODO: add ability to remove attrs, i.e. for imports attr
attrs = dicts.add(EXECUTABLE_ATTRS, attrs),
Expand Down
35 changes: 35 additions & 0 deletions python/private/py_executable_info.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Implementation of PyExecutableInfo provider."""

PyExecutableInfo = provider(
doc = """
Information about an executable.

This provider is for executable-specific information (e.g. tests and binaries).

:::{versionadded} 0.36.0
:::
""",
fields = {
"interpreter_path": """
:type: None | str

Path to the Python interpreter to use for running the executable itself (not the
bootstrap script). Either an absolute path (which means it is
platform-specific), or a runfiles-relative path (which means the interpreter
should be within `runtime_files`)
""",
"main": """
:type: File

The user-level entry point file. Usually a `.py` file, but may also be `.pyc`
file if precompiling is enabled.
""",
"runfiles_without_exe": """
:type: runfiles

The runfiles the program needs, but without the original executable,
files only added to support the original executable, or files specific to the
original program.
""",
},
)
12 changes: 12 additions & 0 deletions python/py_executable_info.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Provider for executable-specific information.

The `PyExecutableInfo` provider contains information about an executable that
isn't otherwise available from its public attributes or other providers.

It exposes information primarily useful for consumers to package the executable,
or derive a new executable from the base binary.
"""

load("//python/private:py_executable_info.bzl", _PyExecutableInfo = "PyExecutableInfo")

PyExecutableInfo = _PyExecutableInfo
7 changes: 7 additions & 0 deletions sphinxdocs/inventories/bazel_inventory.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ native.package_name bzl:function 1 rules/lib/toplevel/native#package_name -
native.package_relative_label bzl:function 1 rules/lib/toplevel/native#package_relative_label -
native.repo_name bzl:function 1 rules/lib/toplevel/native#repo_name -
native.repository_name bzl:function 1 rules/lib/toplevel/native#repository_name -
runfiles bzl:type 1 rules/lib/builtins/runfiles -
runfiles.empty_filenames bzl:type 1 rules/lib/builtins/runfiles#empty_filenames -
runfiles.files bzl:type 1 rules/lib/builtins/runfiles#files -
runfiles.merge bzl:type 1 rules/lib/builtins/runfiles#merge -
runfiles.merge_all bzl:type 1 rules/lib/builtins/runfiles#merge_all -
runfiles.root_symlinks bzl:type 1 rules/lib/builtins/runfiles#root_symlinks -
runfiles.symlinks bzl:type 1 rules/lib/builtins/runfiles#symlinks -
str bzl:type 1 rules/lib/string -
struct bzl:type 1 rules/lib/builtins/struct -
toolchain_type bzl:type 1 ules/lib/builtins/toolchain_type.html -
Expand Down
12 changes: 11 additions & 1 deletion tests/base_rules/py_executable_base_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
load("@rules_testing//lib:truth.bzl", "matching")
load("@rules_testing//lib:util.bzl", rt_util = "util")
load("//python:py_executable_info.bzl", "PyExecutableInfo")
load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility
load("//tests/base_rules:base_tests.bzl", "create_base_tests")
load("//tests/base_rules:util.bzl", "WINDOWS_ATTR", pt_util = "util")
load("//tests/support:py_executable_info_subject.bzl", "PyExecutableInfoSubject")
load("//tests/support:support.bzl", "LINUX_X86_64", "WINDOWS_X86_64")

_BuiltinPyRuntimeInfo = PyRuntimeInfo
Expand Down Expand Up @@ -132,11 +134,19 @@ def _test_executable_in_runfiles_impl(env, target):
exe = ".exe"
else:
exe = ""

env.expect.that_target(target).runfiles().contains_at_least([
"{workspace}/{package}/{test_name}_subject" + exe,
])

if rp_config.enable_pystar:
py_exec_info = env.expect.that_target(target).provider(PyExecutableInfo, factory = PyExecutableInfoSubject.new)
py_exec_info.main().path().contains("_subject.py")
py_exec_info.interpreter_path().contains("python")
py_exec_info.runfiles_without_exe().contains_none_of([
"{workspace}/{package}/{test_name}_subject" + exe,
"{workspace}/{package}/{test_name}_subject",
])

def _test_default_main_can_be_generated(name, config):
rt_util.helper_target(
config.rule,
Expand Down
70 changes: 70 additions & 0 deletions tests/support/py_executable_info_subject.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright 2024 The Bazel Authors. All rights reserved.
#
# 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
#
# http://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.
"""PyExecutableInfo testing subject."""

load("@rules_testing//lib:truth.bzl", "subjects")

def _py_executable_info_subject_new(info, *, meta):
"""Creates a new `PyExecutableInfoSubject` for a PyExecutableInfo provider instance.

Method: PyExecutableInfoSubject.new

Args:
info: The PyExecutableInfo object
meta: ExpectMeta object.

Returns:
A `PyExecutableInfoSubject` struct
"""

# buildifier: disable=uninitialized
public = struct(
# go/keep-sorted start
actual = info,
interpreter_path = lambda *a, **k: _py_executable_info_subject_interpreter_path(self, *a, **k),
main = lambda *a, **k: _py_executable_info_subject_main(self, *a, **k),
runfiles_without_exe = lambda *a, **k: _py_executable_info_subject_runfiles_without_exe(self, *a, **k),
# go/keep-sorted end
)
self = struct(
actual = info,
meta = meta,
)
return public

def _py_executable_info_subject_interpreter_path(self):
"""Returns a subject for `PyExecutableInfo.interpreter_path`."""
return subjects.str(
self.actual.interpreter_path,
meta = self.meta.derive("interpreter_path()"),
)

def _py_executable_info_subject_main(self):
"""Returns a subject for `PyExecutableInfo.main`."""
return subjects.file(
self.actual.main,
meta = self.meta.derive("main()"),
)

def _py_executable_info_subject_runfiles_without_exe(self):
"""Returns a subject for `PyExecutableInfo.runfiles_without_exe`."""
return subjects.runfiles(
self.actual.runfiles_without_exe,
meta = self.meta.derive("runfiles_without_exe()"),
)

# buildifier: disable=name-conventions
PyExecutableInfoSubject = struct(
new = _py_executable_info_subject_new,
)