@@ -200,13 +200,37 @@ a system having the necessary Python installed.
200200 doc = """
201201An absolute path or program name on the `PATH` env var.
202202
203+ *Mutually exclusive with `interpreter_target`.*
204+
203205Values with slashes are assumed to be the path to a program. Otherwise, it is
204206treated as something to search for on `PATH`
205207
206208Note that, when a plain program name is used, the path to the interpreter is
207209resolved at repository evalution time, not runtime of any resulting binaries.
210+
211+ If not set, defaults to `python3`.
212+
213+ :::{seealso}
214+ The {obj}`interpreter_target` attribute for getting the interpreter from
215+ a label
216+ :::
217+ """ ,
218+ default = "" ,
219+ ),
220+ "interpreter_target" : attr .label (
221+ doc = """
222+ A label to a Python interpreter executable.
223+
224+ *Mutually exclusive with `interpreter_path`.*
225+
226+ On Windows, if the path doesn't exist, various suffixes will be tried to
227+ find a usable path.
228+
229+ :::{seealso}
230+ The {obj}`interpreter_path` attribute for getting the interpreter from
231+ a path or PATH environment lookup.
232+ :::
208233""" ,
209- default = "python3" ,
210234 ),
211235 "on_failure" : attr .string (
212236 default = _OnFailure .SKIP ,
@@ -247,6 +271,37 @@ def _expand_incompatible_template():
247271 os = "@platforms//:incompatible" ,
248272 )
249273
274+ def _find_python_exe_from_target (rctx ):
275+ base_path = rctx .path (rctx .attr .interpreter_target )
276+ if base_path .exists :
277+ return base_path , None
278+ attempted_paths = [base_path ]
279+
280+ # Try to convert a unix-y path to a Windows path. On Linux/Mac,
281+ # the path is usually `bin/python3`. On Windows, it's simply
282+ # `python.exe`.
283+ basename = base_path .basename .rstrip ("3" )
284+ path = base_path .dirname .dirname .get_child (basename )
285+ path = rctx .path ("{}.exe" .format (path ))
286+ if path .exists :
287+ return path , None
288+ attempted_paths .append (path )
289+
290+ # Try adding .exe to the base path
291+ path = rctx .path ("{}.exe" .format (base_path ))
292+ if path .exists :
293+ return path , None
294+ attempted_paths .append (path )
295+
296+ describe_failure = lambda : (
297+ "Target '{target}' could not be resolved to a valid path. " +
298+ "Attempted paths: {paths}"
299+ ).format (
300+ target = rctx .attr .interpreter_target ,
301+ paths = "\n " .join ([str (p ) for p in attempted_paths ]),
302+ )
303+ return None , describe_failure
304+
250305def _resolve_interpreter_path (rctx ):
251306 """Find the absolute path for an interpreter.
252307
@@ -260,20 +315,27 @@ def _resolve_interpreter_path(rctx):
260315 returns a description of why it couldn't be resolved
261316 A path object or None. The path may not exist.
262317 """
263- if "/" not in rctx .attr .interpreter_path and "\\ " not in rctx .attr .interpreter_path :
264- # Provide a bit nicer integration with pyenv: recalculate the runtime if the
265- # user changes the python version using e.g. `pyenv shell`
266- repo_utils .getenv (rctx , "PYENV_VERSION" )
267- result = repo_utils .which_unchecked (rctx , rctx .attr .interpreter_path )
268- resolved_path = result .binary
269- describe_failure = result .describe_failure
318+ if rctx .attr .interpreter_path and rctx .attr .interpreter_target :
319+ fail ("interpreter_path and interpreter_target are mutually exclusive" )
320+
321+ if rctx .attr .interpreter_target :
322+ resolved_path , describe_failure = _find_python_exe_from_target (rctx )
270323 else :
271- rctx .watch (rctx .attr .interpreter_path )
272- resolved_path = rctx .path (rctx .attr .interpreter_path )
273- if not resolved_path .exists :
274- describe_failure = lambda : "Path not found: {}" .format (repr (rctx .attr .interpreter_path ))
324+ interpreter_path = rctx .attr .interpreter_path or "python3"
325+ if "/" not in interpreter_path and "\\ " not in interpreter_path :
326+ # Provide a bit nicer integration with pyenv: recalculate the runtime if the
327+ # user changes the python version using e.g. `pyenv shell`
328+ repo_utils .getenv (rctx , "PYENV_VERSION" )
329+ result = repo_utils .which_unchecked (rctx , interpreter_path )
330+ resolved_path = result .binary
331+ describe_failure = result .describe_failure
275332 else :
276- describe_failure = None
333+ rctx .watch (interpreter_path )
334+ resolved_path = rctx .path (interpreter_path )
335+ if not resolved_path .exists :
336+ describe_failure = lambda : "Path not found: {}" .format (repr (interpreter_path ))
337+ else :
338+ describe_failure = None
277339
278340 return struct (
279341 resolved_path = resolved_path ,
0 commit comments