@@ -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+
1776def _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.""" ,
0 commit comments