Skip to content

Commit 012a826

Browse files
committed
Remove determine_main rule
This inlines this into the executable rules instead. Fixes #605 Fixes #621
1 parent 30d82dc commit 012a826

File tree

8 files changed

+76
-138
lines changed

8 files changed

+76
-138
lines changed

docs/py_binary.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/py_test.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

py/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ bzl_library(
3030
visibility = ["//visibility:public"],
3131
deps = [
3232
"//py/private:py_binary",
33-
"//py/private:py_executable",
3433
"//py/private:py_image_layer",
3534
"//py/private:py_library",
3635
"//py/private:py_pex_binary",

py/defs.bzl

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ python.toolchain(python_version = "3.9", is_default = True)
3535
```
3636
"""
3737

38-
load("@aspect_bazel_lib//lib:utils.bzl", "propagate_common_rule_attributes")
3938
load("//py/private:py_binary.bzl", _py_binary = "py_binary", _py_test = "py_test")
40-
load("//py/private:py_executable.bzl", "determine_main")
4139
load("//py/private:py_image_layer.bzl", _py_image_layer = "py_image_layer")
4240
load("//py/private:py_library.bzl", _py_library = "py_library")
4341
load("//py/private:py_pex_binary.bzl", _py_pex_binary = "py_pex_binary")
@@ -63,31 +61,16 @@ py_image_layer = _py_image_layer
6361
resolutions = _resolutions
6462

6563
def _py_binary_or_test(name, rule, srcs, main, data = [], deps = [], resolutions = {}, **kwargs):
66-
exec_properties = kwargs.pop("exec_properties", {})
67-
non_test_exec_properties = {k: v for k, v in exec_properties.items() if not k.startswith("test.")}
68-
69-
# Compatibility with rules_python, see docs in py_executable.bzl
70-
main_target = "{}.find_main".format(name)
71-
determine_main(
72-
name = main_target,
73-
target_name = name,
74-
main = main,
75-
srcs = srcs,
76-
exec_properties = non_test_exec_properties,
77-
**propagate_common_rule_attributes(kwargs)
78-
)
79-
8064
package_collisions = kwargs.pop("package_collisions", None)
8165

8266
rule(
8367
name = name,
8468
srcs = srcs,
85-
main = main_target,
69+
main = main,
8670
data = data,
8771
deps = deps,
8872
resolutions = resolutions,
8973
package_collisions = package_collisions,
90-
exec_properties = exec_properties,
9174
**kwargs
9275
)
9376

py/private/BUILD.bazel

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,6 @@ bzl_library(
9393
visibility = ["//py:__subpackages__"],
9494
)
9595

96-
bzl_library(
97-
name = "py_executable",
98-
srcs = ["py_executable.bzl"],
99-
visibility = ["//py:__subpackages__"],
100-
)
101-
10296
bzl_library(
10397
name = "py_pex_binary",
10498
srcs = ["py_pex_binary.bzl"],

py/private/py_binary.bzl

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,71 @@ def _dict_to_exports(env):
1414
for (k, v) in env.items()
1515
]
1616

17+
def _csv(values):
18+
"""Convert a list of strings to comma separated value string."""
19+
return ", ".join(sorted(values))
20+
21+
def _path_endswith(path, endswith):
22+
# Use slash to anchor each path to prevent e.g.
23+
# "ab/c.py".endswith("b/c.py") from incorrectly matching.
24+
return ("/" + path).endswith("/" + endswith)
25+
26+
def _determine_main(ctx):
27+
"""Determine the main entry point .py source file.
28+
29+
Args:
30+
ctx: The rule ctx.
31+
32+
Returns:
33+
Artifact; the main file. If one can't be found, an error is raised.
34+
"""
35+
if ctx.attr.main:
36+
# Deviation from rules_python: allow a leading colon, e.g. `main = ":my_target"`
37+
proposed_main = ctx.attr.main.removeprefix(":")
38+
if not proposed_main.endswith(".py"):
39+
fail("main {} must end in '.py'".format(proposed_main))
40+
else:
41+
if ctx.label.name.endswith(".py"):
42+
fail("name {} must not end in '.py'".format(ctx.label.name))
43+
proposed_main = ctx.label.name + ".py"
44+
45+
main_files = [src for src in ctx.files.srcs if _path_endswith(src.short_path, proposed_main)]
46+
47+
# Deviation from logic in rules_python: rules_py is a bit more permissive.
48+
# Allow a srcs of length one to determine the main, if the target name didn't match anything.
49+
if not main_files and len(ctx.files.srcs) == 1:
50+
main_files = ctx.files.srcs
51+
52+
if not main_files:
53+
if ctx.attr.main:
54+
fail("could not find '{}' as specified by 'main' attribute".format(proposed_main))
55+
else:
56+
fail(("corresponding default '{}' does not appear in srcs. Add " +
57+
"it or override default file name with a 'main' attribute").format(
58+
proposed_main,
59+
))
60+
61+
elif len(main_files) > 1:
62+
if ctx.attr.main:
63+
fail(("file name '{}' specified by 'main' attributes matches multiple files. " +
64+
"Matches: {}").format(
65+
proposed_main,
66+
_csv([f.short_path for f in main_files]),
67+
))
68+
else:
69+
fail(("default main file '{}' matches multiple files in srcs. Perhaps specify " +
70+
"an explicit file with 'main' attribute? Matches were: {}").format(
71+
proposed_main,
72+
_csv([f.short_path for f in main_files]),
73+
))
74+
return main_files[0]
75+
1776
def _py_binary_rule_impl(ctx):
1877
venv_toolchain = ctx.toolchains[VENV_TOOLCHAIN]
1978
py_toolchain = _py_semantics.resolve_toolchain(ctx)
2079

80+
main_file = _determine_main(ctx)
81+
2182
# Check for duplicate virtual dependency names. Those that map to the same resolution target would have been merged by the depset for us.
2283
virtual_resolution = _py_library.resolve_virtuals(ctx)
2384
imports_depset = _py_library.make_imports_depset(ctx, extra_imports_depsets = virtual_resolution.imports)
@@ -78,7 +139,7 @@ def _py_binary_rule_impl(ctx):
78139
"{{ARG_PYTHON}}": to_rlocation_path(ctx, py_toolchain.python) if py_toolchain.runfiles_interpreter else py_toolchain.python.path,
79140
"{{ARG_VENV_NAME}}": ".{}.venv".format(ctx.attr.name),
80141
"{{ARG_PTH_FILE}}": to_rlocation_path(ctx, site_packages_pth_file),
81-
"{{ENTRYPOINT}}": to_rlocation_path(ctx, ctx.file.main),
142+
"{{ENTRYPOINT}}": to_rlocation_path(ctx, main_file),
82143
"{{PYTHON_ENV}}": "\n".join(_dict_to_exports(default_env)).strip(),
83144
"{{EXEC_PYTHON_BIN}}": "python{}".format(
84145
py_toolchain.interpreter_version_info.major,
@@ -107,14 +168,13 @@ def _py_binary_rule_impl(ctx):
107168

108169
instrumented_files_info = _py_library.make_instrumented_files_info(
109170
ctx,
110-
extra_source_attributes = ["main"],
111171
)
112172

113173
return [
114174
DefaultInfo(
115175
files = depset([
116176
executable_launcher,
117-
ctx.file.main,
177+
main_file,
118178
site_packages_pth_file,
119179
]),
120180
executable = executable_launcher,
@@ -139,10 +199,13 @@ _attrs = dict({
139199
doc = "Environment variables to set when running the binary.",
140200
default = {},
141201
),
142-
"main": attr.label(
143-
doc = "Script to execute with the Python interpreter.",
144-
allow_single_file = True,
145-
mandatory = True,
202+
"main": attr.string(
203+
doc = """Script to execute with the Python interpreter.
204+
Like rules_python, this is treated as a suffix of a file that should appear among the srcs.
205+
If absent, then `[name].py` is tried. As a final fallback, if the srcs has a single file,
206+
that is used as the main.
207+
""",
208+
default = "",
146209
),
147210
"python_version": attr.string(
148211
doc = """Whether to build this target and its transitive deps for a specific python version.""",

py/private/py_executable.bzl

Lines changed: 0 additions & 93 deletions
This file was deleted.

py/tests/external-deps/custom-macro/macro.bzl

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
# buildifier: disable=module-docstring
2-
load("//py:defs.bzl", "py_binary_rule", "py_library")
2+
load("//py:defs.bzl", "py_binary")
33

44
def click_cli_binary(name, deps = [], **kwargs):
5-
py_library(
6-
name = name + "_lib",
7-
srcs = ["//py/tests/external-deps/custom-macro:__main__.py"],
8-
)
9-
10-
# NB: we don't use the py_binary macro here, because we want our `main` attribute to be used
11-
# exactly as specified here, rather than follow rules_python semantics.
12-
py_binary_rule(
5+
py_binary(
136
name = name,
14-
main = "//py/tests/external-deps/custom-macro:__main__.py",
7+
srcs = ["//py/tests/external-deps/custom-macro:__main__.py"],
158
deps = deps + [
16-
name + "_lib",
179
"@pypi_click//:pkg",
1810
],
1911
**kwargs

0 commit comments

Comments
 (0)