Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
More reliable command selection algorithm
  • Loading branch information
zooba committed Jan 20, 2026
commit 8a772d9fbba4f5e46418ef07a943335713da7872
62 changes: 49 additions & 13 deletions src/manage/scriptutils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
"""This module has functions for looking into scripts to decide how to launch.

Currently, this is primarily shebang lines. This support is intended to allow
scripts to be somewhat portable between POSIX (where they are natively handled)
and Windows, when launching in Python. They are not intended to provide generic
shebang support, although for historical/compatibility reasons it is possible.

Shebang commands shaped like '/usr/bin/<command>' or '/usr/local/bin/<command>'
will have the command matched to an alias or executable name for detected
runtimes, with the first match being selected.
A command of 'py', 'pyw', 'python' or 'pythonw' will match the default runtime.
If the install manager has been launched in windowed mode, and the selected
alias is not marked as windowed, then the first windowed 'run-for' target will
be substituted (if present - otherwise, it will just not run windowed). Aliases
that map to windowed targets are launched windowed.
If no matching command is found, the default install will be used.

Shebang commands shaped like '/usr/bin/env <command>' will do the same lookup as
above. If no matching command is found, the current PATH environment variable
will be searched for a matching command. It will be launched with a warning,
configuration permitting.

Other shebangs will be treated directly as the command, doing the same lookup
and the same PATH search.

It is not yet implemented, but this is also where a search for PEP 723 inline
script metadata would go. Find the comment mentioning PEP 723 below.
"""

import re

from .logging import LOGGER
Expand Down Expand Up @@ -25,22 +54,29 @@ def _find_shebang_command(cmd, full_cmd, *, windowed=None):
# Internal logic error, but non-fatal, if it has no value
assert windowed is not None
Comment thread
zooba marked this conversation as resolved.

# Ensure we use the default install for a default name. Otherwise, a
# "higher" runtime may claim it via an alias, which is not the intent.
if is_default:
for i in cmd.get_installs():
if i.get("default"):
exe = i["executable"]
if is_wdefault or windowed:
target = [t for t in i.get("run-for", []) if t.get("windowed")]
if target:
exe = target[0]["target"]
return {**i, "executable": i["prefix"] / exe}

for i in cmd.get_installs():
if is_default and i.get("default"):
if is_wdefault or windowed:
target = [t for t in i.get("run-for", []) if t.get("windowed")]
if target:
return {**i, "executable": i["prefix"] / target[0]["target"]}
return {**i, "executable": i["prefix"] / i["executable"]}
for a in i.get("alias", ()):
if sh_cmd.match(a["name"]):
exe = a["target"]
LOGGER.debug("Matched alias %s in %s", a["name"], i["id"])
if windowed and not a.get("windowed"):
for a2 in i.get("alias", ()):
if a2.get("windowed"):
LOGGER.debug("Substituting alias %s for windowed=1", a2["name"])
return {**i, "executable": i["prefix"] / a2["target"]}
return {**i, "executable": i["prefix"] / a["target"]}
target = [t for t in i.get("run-for", []) if t.get("windowed")]
if target:
exe = target[0]["target"]
LOGGER.debug("Substituting target %s for windowed=1", exe)
return {**i, "executable": i["prefix"] / exe}
if sh_cmd.full_match(PurePath(i["executable"]).name):
LOGGER.debug("Matched executable name %s in %s", i["executable"], i["id"])
return i
Expand Down Expand Up @@ -115,7 +151,7 @@ def _parse_shebang(cmd, line, *, windowed=None):
"Python runtimes, set 'shebang_can_run_anything' to "
"'false' in your configuration file.")
return i

else:
LOGGER.warn("A shebang '%s' was found, but could not be matched "
"to an installed runtime.", full_cmd)
Expand Down Expand Up @@ -176,7 +212,7 @@ def _read_script(cmd, script, encoding, *, windowed=None):
if coding and coding.group(1) != encoding:
raise NewEncoding(coding.group(1))

# TODO: Parse inline script metadata
# TODO: Parse inline script metadata (PEP 723)
# This involves finding '# /// script' followed by
# a line with '# requires-python = <spec>'.
# That spec needs to be processed as a version constraint, which
Expand Down
40 changes: 32 additions & 8 deletions tests/test_scriptutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
)

def _fake_install(v, **kwargs):
try:
kwargs["run-for"] = kwargs.pop("run_for")
except LookupError:
pass
return {
"company": kwargs.get("company", "Test"),
"id": f"test-{v}",
Expand All @@ -28,10 +32,19 @@ def _fake_install(v, **kwargs):
}

INSTALLS = [
_fake_install("1.0", alias=[{"name": "test1.0.exe", "target": "./test-binary-1.0.exe"},
{"name": "testw1.0.exe", "target": "./test-binary-w-1.0.exe", "windowed": 1}]),
_fake_install("1.1", alias=[{"name": "test1.1.exe", "target": "./test-binary-1.1.exe"},
{"name": "testw1.1.exe", "target": "./test-binary-w-1.1.exe", "windowed": 1}]),
_fake_install("1.0",
run_for=[dict(tag="1.0", target="./test-binary-1.0.exe"),
dict(tag="1.0", target="./test-binary-1.0-win.exe", windowed=1)],
alias=[dict(name="test1.0.exe", target="./test-binary-1.0.exe"),
dict(name="testw1.0.exe", target="./test-binary-w-1.0.exe", windowed=1)],
),
_fake_install("1.1",
default=1,
run_for=[dict(tag="1.1", target="./test-binary-1.1.exe"),
dict(tag="1.1", target="./test-binary-1.1-win.exe", windowed=1)],
alias=[dict(name="test1.1.exe", target="./test-binary-1.1.exe"),
dict(name="testw1.1.exe", target="./test-binary-w-1.1.exe", windowed=1)],
),
_fake_install("1.3.1", company="PythonCore"),
_fake_install("1.3.2", company="PythonOther"),
_fake_install("2.0", alias=[{"name": "test2.0.exe", "target": "./test-binary-2.0.exe"}]),
Expand Down Expand Up @@ -73,30 +86,41 @@ def test_read_shebang(fake_config, tmp_path, script, expect):


@pytest.mark.parametrize("script, expect, windowed", [
# Non-windowed alias from non-windowed launcher uses default 'executable'
("#! /usr/bin/test1.0\n", "test-binary-1.0.exe", False),
("#! /usr/bin/test1.0\n", "test-binary-w-1.0.exe", True),
# Non-windowed alias from windowed launcher uses first windowed 'run-for'
("#! /usr/bin/test1.0\n", "test-binary-1.0-win.exe", True),
# Windowed alias from either launcher uses the discovered alias
("#! /usr/bin/testw1.0\n", "test-binary-w-1.0.exe", False),
("#! /usr/bin/testw1.0\n", "test-binary-w-1.0.exe", True),

# No windowed option for 2.0, so picks the regular executable
("#! /usr/bin/test2.0\n", "test-binary-2.0.exe", False),
("#! /usr/bin/test2.0\n", "test-binary-2.0.exe", True),
("#! /usr/bin/testw2.0\n", None, False),
("#! /usr/bin/testw2.0\n", None, True),
("#!test1.0.exe\n", "test-binary-1.0.exe", False),
("#!test1.0.exe\n", "test-binary-w-1.0.exe", True),
("#!test1.0.exe\n", "test-binary-1.0-win.exe", True),
("#!testw1.0.exe\n", "test-binary-w-1.0.exe", False),
("#!testw1.0.exe\n", "test-binary-w-1.0.exe", True),
("#!test1.1.exe\n", "test-binary-1.1.exe", False),
("#!test1.1.exe\n", "test-binary-w-1.1.exe", True),
("#!test1.1.exe\n", "test-binary-1.1-win.exe", True),
("#!testw1.1.exe\n", "test-binary-w-1.1.exe", False),
("#!testw1.1.exe\n", "test-binary-w-1.1.exe", True),

# Matching executable name won't be overridden by windowed setting
("#!test-binary-1.1.exe\n", "test-binary-1.1.exe", False),
("#!test-binary-1.1.exe\n", "test-binary-1.1.exe", True),
("#! /usr/bin/env test1.0\n", "test-binary-1.0.exe", False),
("#! /usr/bin/env test1.0\n", "test-binary-w-1.0.exe", True),
("#! /usr/bin/env test1.0\n", "test-binary-1.0-win.exe", True),
("#! /usr/bin/env testw1.0\n", "test-binary-w-1.0.exe", False),
("#! /usr/bin/env testw1.0\n", "test-binary-w-1.0.exe", True),

# Default name will use default 'executable' or first windowed 'run-for'
("#! /usr/bin/python\n", "test-binary-1.1.exe", False),
("#! /usr/bin/python\n", "test-binary-1.1-win.exe", True),
("#! /usr/bin/pythonw\n", "test-binary-1.1-win.exe", False),
("#! /usr/bin/pythonw\n", "test-binary-1.1-win.exe", True),
])
def test_read_shebang_windowed(fake_config, tmp_path, script, expect, windowed):
fake_config.installs.extend(INSTALLS)
Expand Down