@@ -14,10 +14,71 @@ def _dict_to_exports(env):
14
14
for (k , v ) in env .items ()
15
15
]
16
16
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
+
17
76
def _py_binary_rule_impl (ctx ):
18
77
venv_toolchain = ctx .toolchains [VENV_TOOLCHAIN ]
19
78
py_toolchain = _py_semantics .resolve_toolchain (ctx )
20
79
80
+ main_file = _determine_main (ctx )
81
+
21
82
# Check for duplicate virtual dependency names. Those that map to the same resolution target would have been merged by the depset for us.
22
83
virtual_resolution = _py_library .resolve_virtuals (ctx )
23
84
imports_depset = _py_library .make_imports_depset (ctx , extra_imports_depsets = virtual_resolution .imports )
@@ -78,7 +139,7 @@ def _py_binary_rule_impl(ctx):
78
139
"{{ARG_PYTHON}}" : to_rlocation_path (ctx , py_toolchain .python ) if py_toolchain .runfiles_interpreter else py_toolchain .python .path ,
79
140
"{{ARG_VENV_NAME}}" : ".{}.venv" .format (ctx .attr .name ),
80
141
"{{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 ),
82
143
"{{PYTHON_ENV}}" : "\n " .join (_dict_to_exports (default_env )).strip (),
83
144
"{{EXEC_PYTHON_BIN}}" : "python{}" .format (
84
145
py_toolchain .interpreter_version_info .major ,
@@ -107,14 +168,13 @@ def _py_binary_rule_impl(ctx):
107
168
108
169
instrumented_files_info = _py_library .make_instrumented_files_info (
109
170
ctx ,
110
- extra_source_attributes = ["main" ],
111
171
)
112
172
113
173
return [
114
174
DefaultInfo (
115
175
files = depset ([
116
176
executable_launcher ,
117
- ctx . file . main ,
177
+ main_file ,
118
178
site_packages_pth_file ,
119
179
]),
120
180
executable = executable_launcher ,
@@ -139,10 +199,13 @@ _attrs = dict({
139
199
doc = "Environment variables to set when running the binary." ,
140
200
default = {},
141
201
),
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 = "" ,
146
209
),
147
210
"python_version" : attr .string (
148
211
doc = """Whether to build this target and its transitive deps for a specific python version.""" ,
0 commit comments