mirror of https://github.com/python/cpython
bpo-46566: Add new py.exe launcher implementation (GH-32062)
This commit is contained in:
parent
5c30388f3c
commit
bad86a621a
|
@ -817,6 +817,13 @@ minor version. I.e. ``/usr/bin/python2.7-32`` will request usage of the
|
||||||
by the "-64" suffix. Furthermore it is possible to specify a major and
|
by the "-64" suffix. Furthermore it is possible to specify a major and
|
||||||
architecture without minor (i.e. ``/usr/bin/python3-64``).
|
architecture without minor (i.e. ``/usr/bin/python3-64``).
|
||||||
|
|
||||||
|
.. versionchanged:: 3.11
|
||||||
|
|
||||||
|
The "-64" suffix is deprecated, and now implies "any architecture that is
|
||||||
|
not provably i386/32-bit". To request a specific environment, use the new
|
||||||
|
``-V:<TAG>`` argument with the complete tag.
|
||||||
|
|
||||||
|
|
||||||
The ``/usr/bin/env`` form of shebang line has one further special property.
|
The ``/usr/bin/env`` form of shebang line has one further special property.
|
||||||
Before looking for installed Python interpreters, this form will search the
|
Before looking for installed Python interpreters, this form will search the
|
||||||
executable :envvar:`PATH` for a Python executable. This corresponds to the
|
executable :envvar:`PATH` for a Python executable. This corresponds to the
|
||||||
|
@ -937,13 +944,65 @@ For example:
|
||||||
Diagnostics
|
Diagnostics
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
If an environment variable ``PYLAUNCH_DEBUG`` is set (to any value), the
|
If an environment variable :envvar:`PYLAUNCHER_DEBUG` is set (to any value), the
|
||||||
launcher will print diagnostic information to stderr (i.e. to the console).
|
launcher will print diagnostic information to stderr (i.e. to the console).
|
||||||
While this information manages to be simultaneously verbose *and* terse, it
|
While this information manages to be simultaneously verbose *and* terse, it
|
||||||
should allow you to see what versions of Python were located, why a
|
should allow you to see what versions of Python were located, why a
|
||||||
particular version was chosen and the exact command-line used to execute the
|
particular version was chosen and the exact command-line used to execute the
|
||||||
target Python.
|
target Python. It is primarily intended for testing and debugging.
|
||||||
|
|
||||||
|
Dry Run
|
||||||
|
-------
|
||||||
|
|
||||||
|
If an environment variable :envvar:`PYLAUNCHER_DRYRUN` is set (to any value),
|
||||||
|
the launcher will output the command it would have run, but will not actually
|
||||||
|
launch Python. This may be useful for tools that want to use the launcher to
|
||||||
|
detect and then launch Python directly. Note that the command written to
|
||||||
|
standard output is always encoded using UTF-8, and may not render correctly in
|
||||||
|
the console.
|
||||||
|
|
||||||
|
Install on demand
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
If an environment variable :envvar:`PYLAUNCHER_ALLOW_INSTALL` is set (to any
|
||||||
|
value), and the requested Python version is not installed but is available on
|
||||||
|
the Microsoft Store, the launcher will attempt to install it. This may require
|
||||||
|
user interaction to complete, and you may need to run the command again.
|
||||||
|
|
||||||
|
An additional :envvar:`PYLAUNCHER_ALWAYS_INSTALL` variable causes the launcher
|
||||||
|
to always try to install Python, even if it is detected. This is mainly intended
|
||||||
|
for testing (and should be used with :envvar:`PYLAUNCHER_DRYRUN`).
|
||||||
|
|
||||||
|
Return codes
|
||||||
|
------------
|
||||||
|
|
||||||
|
The following exit codes may be returned by the Python launcher. Unfortunately,
|
||||||
|
there is no way to distinguish these from the exit code of Python itself.
|
||||||
|
|
||||||
|
The names of codes are as used in the sources, and are only for reference. There
|
||||||
|
is no way to access or resolve them apart from reading this page. Entries are
|
||||||
|
listed in alphabetical order of names.
|
||||||
|
|
||||||
|
+-------------------+-------+-----------------------------------------------+
|
||||||
|
| Name | Value | Description |
|
||||||
|
+===================+=======+===============================================+
|
||||||
|
| RC_BAD_VENV_CFG | 107 | A :file:`pyvenv.cfg` was found but is corrupt.|
|
||||||
|
+-------------------+-------+-----------------------------------------------+
|
||||||
|
| RC_CREATE_PROCESS | 101 | Failed to launch Python. |
|
||||||
|
+-------------------+-------+-----------------------------------------------+
|
||||||
|
| RC_INSTALLING | 111 | An install was started, but the command will |
|
||||||
|
| | | need to be re-run after it completes. |
|
||||||
|
+-------------------+-------+-----------------------------------------------+
|
||||||
|
| RC_INTERNAL_ERROR | 109 | Unexpected error. Please report a bug. |
|
||||||
|
+-------------------+-------+-----------------------------------------------+
|
||||||
|
| RC_NO_COMMANDLINE | 108 | Unable to obtain command line from the |
|
||||||
|
| | | operating system. |
|
||||||
|
+-------------------+-------+-----------------------------------------------+
|
||||||
|
| RC_NO_PYTHON | 103 | Unable to locate the requested version. |
|
||||||
|
+-------------------+-------+-----------------------------------------------+
|
||||||
|
| RC_NO_VENV_CFG | 106 | A :file:`pyvenv.cfg` was required but not |
|
||||||
|
| | | found. |
|
||||||
|
+-------------------+-------+-----------------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
.. _windows_finding_modules:
|
.. _windows_finding_modules:
|
||||||
|
|
|
@ -0,0 +1,423 @@
|
||||||
|
import contextlib
|
||||||
|
import itertools
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import sysconfig
|
||||||
|
import tempfile
|
||||||
|
import textwrap
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from test import support
|
||||||
|
|
||||||
|
if sys.platform != "win32":
|
||||||
|
raise unittest.SkipTest("test only applies to Windows")
|
||||||
|
|
||||||
|
# Get winreg after the platform check
|
||||||
|
import winreg
|
||||||
|
|
||||||
|
|
||||||
|
PY_EXE = "py.exe"
|
||||||
|
if sys.executable.casefold().endswith("_d.exe".casefold()):
|
||||||
|
PY_EXE = "py_d.exe"
|
||||||
|
|
||||||
|
# Registry data to create. On removal, everything beneath top-level names will
|
||||||
|
# be deleted.
|
||||||
|
TEST_DATA = {
|
||||||
|
"PythonTestSuite": {
|
||||||
|
"DisplayName": "Python Test Suite",
|
||||||
|
"SupportUrl": "https://www.python.org/",
|
||||||
|
"3.100": {
|
||||||
|
"DisplayName": "X.Y version",
|
||||||
|
"InstallPath": {
|
||||||
|
None: sys.prefix,
|
||||||
|
"ExecutablePath": "X.Y.exe",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3.100-32": {
|
||||||
|
"DisplayName": "X.Y-32 version",
|
||||||
|
"InstallPath": {
|
||||||
|
None: sys.prefix,
|
||||||
|
"ExecutablePath": "X.Y-32.exe",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3.100-arm64": {
|
||||||
|
"DisplayName": "X.Y-arm64 version",
|
||||||
|
"InstallPath": {
|
||||||
|
None: sys.prefix,
|
||||||
|
"ExecutablePath": "X.Y-arm64.exe",
|
||||||
|
"ExecutableArguments": "-X fake_arg_for_test",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ignored": {
|
||||||
|
"DisplayName": "Ignored because no ExecutablePath",
|
||||||
|
"InstallPath": {
|
||||||
|
None: sys.prefix,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_PY_COMMANDS = textwrap.dedent("""
|
||||||
|
[defaults]
|
||||||
|
py_python=PythonTestSuite/3.100
|
||||||
|
py_python2=PythonTestSuite/3.100-32
|
||||||
|
py_python3=PythonTestSuite/3.100-arm64
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def create_registry_data(root, data):
|
||||||
|
def _create_registry_data(root, key, value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
# For a dict, we recursively create keys
|
||||||
|
with winreg.CreateKeyEx(root, key) as hkey:
|
||||||
|
for k, v in value.items():
|
||||||
|
_create_registry_data(hkey, k, v)
|
||||||
|
elif isinstance(value, str):
|
||||||
|
# For strings, we set values. 'key' may be None in this case
|
||||||
|
winreg.SetValueEx(root, key, None, winreg.REG_SZ, value)
|
||||||
|
else:
|
||||||
|
raise TypeError("don't know how to create data for '{}'".format(value))
|
||||||
|
|
||||||
|
for k, v in data.items():
|
||||||
|
_create_registry_data(root, k, v)
|
||||||
|
|
||||||
|
|
||||||
|
def enum_keys(root):
|
||||||
|
for i in itertools.count():
|
||||||
|
try:
|
||||||
|
yield winreg.EnumKey(root, i)
|
||||||
|
except OSError as ex:
|
||||||
|
if ex.winerror == 259:
|
||||||
|
break
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def delete_registry_data(root, keys):
|
||||||
|
ACCESS = winreg.KEY_WRITE | winreg.KEY_ENUMERATE_SUB_KEYS
|
||||||
|
for key in list(keys):
|
||||||
|
with winreg.OpenKey(root, key, access=ACCESS) as hkey:
|
||||||
|
delete_registry_data(hkey, enum_keys(hkey))
|
||||||
|
winreg.DeleteKey(root, key)
|
||||||
|
|
||||||
|
|
||||||
|
def is_installed(tag):
|
||||||
|
key = rf"Software\Python\PythonCore\{tag}\InstallPath"
|
||||||
|
for root, flag in [
|
||||||
|
(winreg.HKEY_CURRENT_USER, 0),
|
||||||
|
(winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_64KEY),
|
||||||
|
(winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_32KEY),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
winreg.CloseKey(winreg.OpenKey(root, key, access=winreg.KEY_READ | flag))
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class PreservePyIni:
|
||||||
|
def __init__(self, path, content):
|
||||||
|
self.path = Path(path)
|
||||||
|
self.content = content
|
||||||
|
self._preserved = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
self._preserved = self.path.read_bytes()
|
||||||
|
except FileNotFoundError:
|
||||||
|
self._preserved = None
|
||||||
|
self.path.write_text(self.content, encoding="utf-16")
|
||||||
|
|
||||||
|
def __exit__(self, *exc_info):
|
||||||
|
if self._preserved is None:
|
||||||
|
self.path.unlink()
|
||||||
|
else:
|
||||||
|
self.path.write_bytes(self._preserved)
|
||||||
|
|
||||||
|
|
||||||
|
class RunPyMixin:
|
||||||
|
py_exe = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_py(cls):
|
||||||
|
py_exe = None
|
||||||
|
if sysconfig.is_python_build(True):
|
||||||
|
py_exe = Path(sys.executable).parent / PY_EXE
|
||||||
|
else:
|
||||||
|
for p in os.getenv("PATH").split(";"):
|
||||||
|
if p:
|
||||||
|
py_exe = Path(p) / PY_EXE
|
||||||
|
if py_exe.is_file():
|
||||||
|
break
|
||||||
|
if not py_exe:
|
||||||
|
raise unittest.SkipTest(
|
||||||
|
"cannot locate '{}' for test".format(PY_EXE)
|
||||||
|
)
|
||||||
|
return py_exe
|
||||||
|
|
||||||
|
def run_py(self, args, env=None, allow_fail=False, expect_returncode=0):
|
||||||
|
if not self.py_exe:
|
||||||
|
self.py_exe = self.find_py()
|
||||||
|
|
||||||
|
env = {**os.environ, **(env or {}), "PYLAUNCHER_DEBUG": "1", "PYLAUNCHER_DRYRUN": "1"}
|
||||||
|
with subprocess.Popen(
|
||||||
|
[self.py_exe, *args],
|
||||||
|
env=env,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
) as p:
|
||||||
|
p.stdin.close()
|
||||||
|
p.wait(10)
|
||||||
|
out = p.stdout.read().decode("utf-8", "replace")
|
||||||
|
err = p.stderr.read().decode("ascii", "replace")
|
||||||
|
if p.returncode != expect_returncode and support.verbose and not allow_fail:
|
||||||
|
print("++ COMMAND ++")
|
||||||
|
print([self.py_exe, *args])
|
||||||
|
print("++ STDOUT ++")
|
||||||
|
print(out)
|
||||||
|
print("++ STDERR ++")
|
||||||
|
print(err)
|
||||||
|
if allow_fail and p.returncode != expect_returncode:
|
||||||
|
raise subprocess.CalledProcessError(p.returncode, [self.py_exe, *args], out, err)
|
||||||
|
else:
|
||||||
|
self.assertEqual(expect_returncode, p.returncode)
|
||||||
|
data = {
|
||||||
|
s.partition(":")[0]: s.partition(":")[2].lstrip()
|
||||||
|
for s in err.splitlines()
|
||||||
|
if not s.startswith("#") and ":" in s
|
||||||
|
}
|
||||||
|
data["stdout"] = out
|
||||||
|
data["stderr"] = err
|
||||||
|
return data
|
||||||
|
|
||||||
|
def py_ini(self, content):
|
||||||
|
if not self.py_exe:
|
||||||
|
self.py_exe = self.find_py()
|
||||||
|
return PreservePyIni(self.py_exe.with_name("py.ini"), content)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def script(self, content, encoding="utf-8"):
|
||||||
|
file = Path(tempfile.mktemp(dir=os.getcwd()) + ".py")
|
||||||
|
file.write_text(content, encoding=encoding)
|
||||||
|
try:
|
||||||
|
yield file
|
||||||
|
finally:
|
||||||
|
file.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, rf"Software\Python") as key:
|
||||||
|
create_registry_data(key, TEST_DATA)
|
||||||
|
|
||||||
|
if support.verbose:
|
||||||
|
p = subprocess.check_output("reg query HKCU\\Software\\Python /s")
|
||||||
|
print(p.decode('mbcs'))
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, rf"Software\Python", access=winreg.KEY_WRITE | winreg.KEY_ENUMERATE_SUB_KEYS) as key:
|
||||||
|
delete_registry_data(key, TEST_DATA)
|
||||||
|
|
||||||
|
|
||||||
|
def test_version(self):
|
||||||
|
data = self.run_py(["-0"])
|
||||||
|
self.assertEqual(self.py_exe, Path(data["argv0"]))
|
||||||
|
self.assertEqual(sys.version.partition(" ")[0], data["version"])
|
||||||
|
|
||||||
|
def test_help_option(self):
|
||||||
|
data = self.run_py(["-h"])
|
||||||
|
self.assertEqual("True", data["SearchInfo.help"])
|
||||||
|
|
||||||
|
def test_list_option(self):
|
||||||
|
for opt, v1, v2 in [
|
||||||
|
("-0", "True", "False"),
|
||||||
|
("-0p", "False", "True"),
|
||||||
|
("--list", "True", "False"),
|
||||||
|
("--list-paths", "False", "True"),
|
||||||
|
]:
|
||||||
|
with self.subTest(opt):
|
||||||
|
data = self.run_py([opt])
|
||||||
|
self.assertEqual(v1, data["SearchInfo.list"])
|
||||||
|
self.assertEqual(v2, data["SearchInfo.listPaths"])
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
data = self.run_py(["--list"])
|
||||||
|
found = {}
|
||||||
|
expect = {}
|
||||||
|
for line in data["stdout"].splitlines():
|
||||||
|
m = re.match(r"\s*(.+?)\s+(.+)$", line)
|
||||||
|
if m:
|
||||||
|
found[m.group(1)] = m.group(2)
|
||||||
|
for company in TEST_DATA:
|
||||||
|
company_data = TEST_DATA[company]
|
||||||
|
tags = [t for t in company_data if isinstance(company_data[t], dict)]
|
||||||
|
for tag in tags:
|
||||||
|
arg = f"-V:{company}/{tag}"
|
||||||
|
expect[arg] = company_data[tag]["DisplayName"]
|
||||||
|
expect.pop(f"-V:{company}/ignored", None)
|
||||||
|
|
||||||
|
actual = {k: v for k, v in found.items() if k in expect}
|
||||||
|
try:
|
||||||
|
self.assertDictEqual(expect, actual)
|
||||||
|
except:
|
||||||
|
if support.verbose:
|
||||||
|
print("*** STDOUT ***")
|
||||||
|
print(data["stdout"])
|
||||||
|
raise
|
||||||
|
|
||||||
|
def test_list_paths(self):
|
||||||
|
data = self.run_py(["--list-paths"])
|
||||||
|
found = {}
|
||||||
|
expect = {}
|
||||||
|
for line in data["stdout"].splitlines():
|
||||||
|
m = re.match(r"\s*(.+?)\s+(.+)$", line)
|
||||||
|
if m:
|
||||||
|
found[m.group(1)] = m.group(2)
|
||||||
|
for company in TEST_DATA:
|
||||||
|
company_data = TEST_DATA[company]
|
||||||
|
tags = [t for t in company_data if isinstance(company_data[t], dict)]
|
||||||
|
for tag in tags:
|
||||||
|
arg = f"-V:{company}/{tag}"
|
||||||
|
install = company_data[tag]["InstallPath"]
|
||||||
|
try:
|
||||||
|
expect[arg] = install["ExecutablePath"]
|
||||||
|
try:
|
||||||
|
expect[arg] += " " + install["ExecutableArguments"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
except KeyError:
|
||||||
|
expect[arg] = str(Path(install[None]) / Path(sys.executable).name)
|
||||||
|
|
||||||
|
expect.pop(f"-V:{company}/ignored", None)
|
||||||
|
|
||||||
|
actual = {k: v for k, v in found.items() if k in expect}
|
||||||
|
try:
|
||||||
|
self.assertDictEqual(expect, actual)
|
||||||
|
except:
|
||||||
|
if support.verbose:
|
||||||
|
print("*** STDOUT ***")
|
||||||
|
print(data["stdout"])
|
||||||
|
raise
|
||||||
|
|
||||||
|
def test_filter_to_company(self):
|
||||||
|
company = "PythonTestSuite"
|
||||||
|
data = self.run_py([f"-V:{company}/"])
|
||||||
|
self.assertEqual("X.Y.exe", data["LaunchCommand"])
|
||||||
|
self.assertEqual(company, data["env.company"])
|
||||||
|
self.assertEqual("3.100", data["env.tag"])
|
||||||
|
|
||||||
|
def test_filter_to_tag(self):
|
||||||
|
company = "PythonTestSuite"
|
||||||
|
data = self.run_py([f"-V:3.100"])
|
||||||
|
self.assertEqual("X.Y.exe", data["LaunchCommand"])
|
||||||
|
self.assertEqual(company, data["env.company"])
|
||||||
|
self.assertEqual("3.100", data["env.tag"])
|
||||||
|
|
||||||
|
data = self.run_py([f"-V:3.100-3"])
|
||||||
|
self.assertEqual("X.Y-32.exe", data["LaunchCommand"])
|
||||||
|
self.assertEqual(company, data["env.company"])
|
||||||
|
self.assertEqual("3.100-32", data["env.tag"])
|
||||||
|
|
||||||
|
data = self.run_py([f"-V:3.100-a"])
|
||||||
|
self.assertEqual("X.Y-arm64.exe -X fake_arg_for_test", data["LaunchCommand"])
|
||||||
|
self.assertEqual(company, data["env.company"])
|
||||||
|
self.assertEqual("3.100-arm64", data["env.tag"])
|
||||||
|
|
||||||
|
def test_filter_to_company_and_tag(self):
|
||||||
|
company = "PythonTestSuite"
|
||||||
|
data = self.run_py([f"-V:{company}/3.1"])
|
||||||
|
self.assertEqual("X.Y.exe", data["LaunchCommand"])
|
||||||
|
self.assertEqual(company, data["env.company"])
|
||||||
|
self.assertEqual("3.100", data["env.tag"])
|
||||||
|
|
||||||
|
def test_search_major_3(self):
|
||||||
|
try:
|
||||||
|
data = self.run_py(["-3"], allow_fail=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
raise unittest.SkipTest("requires at least one Python 3.x install")
|
||||||
|
self.assertEqual("PythonCore", data["env.company"])
|
||||||
|
self.assertTrue(data["env.tag"].startswith("3."), data["env.tag"])
|
||||||
|
|
||||||
|
def test_search_major_3_32(self):
|
||||||
|
try:
|
||||||
|
data = self.run_py(["-3-32"], allow_fail=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
if not any(is_installed(f"3.{i}-32") for i in range(5, 11)):
|
||||||
|
raise unittest.SkipTest("requires at least one 32-bit Python 3.x install")
|
||||||
|
raise
|
||||||
|
self.assertEqual("PythonCore", data["env.company"])
|
||||||
|
self.assertTrue(data["env.tag"].startswith("3."), data["env.tag"])
|
||||||
|
self.assertTrue(data["env.tag"].endswith("-32"), data["env.tag"])
|
||||||
|
|
||||||
|
def test_search_major_2(self):
|
||||||
|
try:
|
||||||
|
data = self.run_py(["-2"], allow_fail=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
if not is_installed("2.7"):
|
||||||
|
raise unittest.SkipTest("requires at least one Python 2.x install")
|
||||||
|
self.assertEqual("PythonCore", data["env.company"])
|
||||||
|
self.assertTrue(data["env.tag"].startswith("2."), data["env.tag"])
|
||||||
|
|
||||||
|
def test_py_default(self):
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
data = self.run_py(["-arg"])
|
||||||
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
|
self.assertEqual("3.100", data["SearchInfo.tag"])
|
||||||
|
self.assertEqual("X.Y.exe -arg", data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_py2_default(self):
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
data = self.run_py(["-2", "-arg"])
|
||||||
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
|
self.assertEqual("3.100-32", data["SearchInfo.tag"])
|
||||||
|
self.assertEqual("X.Y-32.exe -arg", data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_py3_default(self):
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
data = self.run_py(["-3", "-arg"])
|
||||||
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
|
self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
|
||||||
|
self.assertEqual("X.Y-arm64.exe -X fake_arg_for_test -arg", data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_py_shebang(self):
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
with self.script("#! /usr/bin/env python -prearg") as script:
|
||||||
|
data = self.run_py([script, "-postarg"])
|
||||||
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
|
self.assertEqual("3.100", data["SearchInfo.tag"])
|
||||||
|
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_py2_shebang(self):
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
with self.script("#! /usr/bin/env python2 -prearg") as script:
|
||||||
|
data = self.run_py([script, "-postarg"])
|
||||||
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
|
self.assertEqual("3.100-32", data["SearchInfo.tag"])
|
||||||
|
self.assertEqual(f"X.Y-32.exe -prearg {script} -postarg", data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_py3_shebang(self):
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
with self.script("#! /usr/bin/env python3 -prearg") as script:
|
||||||
|
data = self.run_py([script, "-postarg"])
|
||||||
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
|
self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
|
||||||
|
self.assertEqual(f"X.Y-arm64.exe -X fake_arg_for_test -prearg {script} -postarg", data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_install(self):
|
||||||
|
data = self.run_py(["-V:3.10"], env={"PYLAUNCHER_ALWAYS_INSTALL": "1"}, expect_returncode=111)
|
||||||
|
cmd = data["stdout"].strip()
|
||||||
|
# If winget is runnable, we should find it. Otherwise, we'll be trying
|
||||||
|
# to open the Store.
|
||||||
|
try:
|
||||||
|
subprocess.check_call(["winget.exe", "--version"])
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.assertIn("ms-windows-store://", cmd)
|
||||||
|
else:
|
||||||
|
self.assertIn("winget.exe", cmd)
|
||||||
|
self.assertIn("9PJPW5LDXLZ5", cmd)
|
|
@ -0,0 +1,6 @@
|
||||||
|
Upgraded :ref:`launcher` to support a new ``-V:company/tag`` argument for
|
||||||
|
full :pep:`514` support and to detect ARM64 installs. The ``-64`` suffix on
|
||||||
|
arguments is deprecated, but still selects any non-32-bit install. Setting
|
||||||
|
:envvar:`PYLAUNCHER_ALLOW_INSTALL` and specifying a version that is not
|
||||||
|
installed will attempt to install the requested version from the Microsoft
|
||||||
|
Store.
|
|
@ -0,0 +1,31 @@
|
||||||
|
Python Launcher for Windows Version %s
|
||||||
|
|
||||||
|
usage:
|
||||||
|
%s [launcher-args] [python-args] [script [script-args]]
|
||||||
|
|
||||||
|
Launcher arguments:
|
||||||
|
-2 : Launch the latest Python 2.x version
|
||||||
|
-3 : Launch the latest Python 3.x version
|
||||||
|
-X.Y : Launch the specified Python version
|
||||||
|
|
||||||
|
The above default to an architecture native runtime, but will select any
|
||||||
|
available. Add a "-32" to the argument to only launch 32-bit runtimes,
|
||||||
|
or add "-64" to omit 32-bit runtimes (this latter option is deprecated).
|
||||||
|
|
||||||
|
To select a specific runtime, use the -V: options.
|
||||||
|
|
||||||
|
-V:TAG : Launch a Python runtime with the specified tag
|
||||||
|
-V:COMPANY/TAG : Launch a Python runtime from the specified company and
|
||||||
|
with the specified tag
|
||||||
|
|
||||||
|
-0 --list : List the available pythons
|
||||||
|
-0p --list-paths : List with paths
|
||||||
|
|
||||||
|
If no options are given but a script is specified, the script is checked for a
|
||||||
|
shebang line. Otherwise, an active virtual environment or global default will
|
||||||
|
be selected.
|
||||||
|
|
||||||
|
See https://docs.python.org/using/windows.html#python-launcher-for-windows for
|
||||||
|
additional configuration.
|
||||||
|
|
||||||
|
The following help text is from Python:
|
File diff suppressed because it is too large
Load Diff
|
@ -25,6 +25,9 @@
|
||||||
7 ICON DISCARDABLE "icons\setup.ico"
|
7 ICON DISCARDABLE "icons\setup.ico"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
1 USAGE "launcher-usage.txt"
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Version
|
// Version
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
<PropertyGroup Label="Configuration">
|
<PropertyGroup Label="Configuration">
|
||||||
<ConfigurationType>Application</ConfigurationType>
|
<ConfigurationType>Application</ConfigurationType>
|
||||||
<CharacterSet>MultiByte</CharacterSet>
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||||
<ImportGroup Label="ExtensionSettings">
|
<ImportGroup Label="ExtensionSettings">
|
||||||
|
@ -95,12 +95,12 @@
|
||||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>version.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>shell32.lib;pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<SubSystem>Console</SubSystem>
|
<SubSystem>Console</SubSystem>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="..\PC\launcher.c" />
|
<ClCompile Include="..\PC\launcher2.c" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\PC\launcher.ico" />
|
<None Include="..\PC\launcher.ico" />
|
||||||
|
|
|
@ -95,12 +95,12 @@
|
||||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>version.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>shell32.lib;pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="..\PC\launcher.c" />
|
<ClCompile Include="..\PC\launcher2.c" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\PC\launcher.ico" />
|
<None Include="..\PC\launcher.ico" />
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<DefineConstants>UpgradeCode=1B68A0EC-4DD3-5134-840E-73854B0863F1;SuppressUpgradeTable=1;$(DefineConstants)</DefineConstants>
|
<DefineConstants>UpgradeCode=1B68A0EC-4DD3-5134-840E-73854B0863F1;SuppressUpgradeTable=1;$(DefineConstants)</DefineConstants>
|
||||||
<IgnoreCommonWxlTemplates>true</IgnoreCommonWxlTemplates>
|
<IgnoreCommonWxlTemplates>true</IgnoreCommonWxlTemplates>
|
||||||
<SuppressICEs>ICE80</SuppressICEs>
|
<SuppressICEs>ICE80</SuppressICEs>
|
||||||
|
<_Rebuild>Build</_Rebuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="..\msi.props" />
|
<Import Project="..\msi.props" />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -18,18 +19,27 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="*.wxl" />
|
<EmbeddedResource Include="*.wxl" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="_EnsurePyEx86" Condition="!Exists('$(BuildPath32)py.exe')" BeforeTargets="PrepareForBuild">
|
<Target Name="_MarkAsRebuild" BeforeTargets="BeforeRebuild">
|
||||||
<MSBuild Projects="$(PySourcePath)PCbuild\pylauncher.vcxproj" Properties="Platform=Win32" />
|
<PropertyGroup>
|
||||||
|
<_Rebuild>Rebuild</_Rebuild>
|
||||||
|
</PropertyGroup>
|
||||||
</Target>
|
</Target>
|
||||||
<Target Name="_EnsurePywEx86" Condition="!Exists('$(BuildPath32)pyw.exe')" BeforeTargets="PrepareForBuild">
|
|
||||||
<MSBuild Projects="$(PySourcePath)PCbuild\pywlauncher.vcxproj" Properties="Platform=Win32" />
|
<Target Name="_EnsurePyEx86" Condition="!Exists('$(BuildPath32)py.exe') or '$(_Rebuild)' == 'Rebuild'" BeforeTargets="PrepareForBuild">
|
||||||
|
<MSBuild Projects="$(PySourcePath)PCbuild\pylauncher.vcxproj" Properties="Platform=Win32" Targets="$(_Rebuild)" />
|
||||||
</Target>
|
</Target>
|
||||||
<Target Name="_EnsurePyShellExt86" Condition="!Exists('$(BuildPath32)pyshellext.dll')" BeforeTargets="PrepareForBuild">
|
<Target Name="_EnsurePywEx86" Condition="!Exists('$(BuildPath32)pyw.exe') or '$(_Rebuild)' == 'Rebuild'" BeforeTargets="PrepareForBuild">
|
||||||
<MSBuild Projects="$(PySourcePath)PCbuild\pyshellext.vcxproj" Properties="Platform=Win32" />
|
<MSBuild Projects="$(PySourcePath)PCbuild\pywlauncher.vcxproj" Properties="Platform=Win32" Targets="$(_Rebuild)" />
|
||||||
</Target>
|
</Target>
|
||||||
<Target Name="_EnsurePyShellExt64" Condition="!Exists('$(BuildPath64)pyshellext.dll')" BeforeTargets="PrepareForBuild">
|
<Target Name="_EnsurePyShellExt86" Condition="!Exists('$(BuildPath32)pyshellext.dll') or '$(_Rebuild)' == 'Rebuild'" BeforeTargets="PrepareForBuild">
|
||||||
<MSBuild Projects="$(PySourcePath)PCbuild\pyshellext.vcxproj" Properties="Platform=x64" />
|
<MSBuild Projects="$(PySourcePath)PCbuild\pyshellext.vcxproj" Properties="Platform=Win32" Targets="$(_Rebuild)" />
|
||||||
|
</Target>
|
||||||
|
<Target Name="_EnsurePyShellExt64" Condition="!Exists('$(BuildPath64)pyshellext.dll') or '$(_Rebuild)' == 'Rebuild'" BeforeTargets="PrepareForBuild">
|
||||||
|
<MSBuild Projects="$(PySourcePath)PCbuild\pyshellext.vcxproj" Properties="Platform=x64" Targets="$(_Rebuild)" />
|
||||||
|
</Target>
|
||||||
|
<Target Name="_EnsurePyShellExtARM64" Condition="!Exists('$(BuildPathARM64)pyshellext.dll') or '$(_Rebuild)' == 'Rebuild'" BeforeTargets="PrepareForBuild">
|
||||||
|
<MSBuild Projects="$(PySourcePath)PCbuild\pyshellext.vcxproj" Properties="Platform=ARM64" Targets="$(_Rebuild)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Import Project="..\msi.targets" />
|
<Import Project="..\msi.targets" />
|
||||||
|
|
|
@ -33,6 +33,15 @@
|
||||||
<Class Id="{BEA218D2-6950-497B-9434-61683EC065FE}" Advertise="no" Context="InprocServer32" ThreadingModel="apartment" />
|
<Class Id="{BEA218D2-6950-497B-9434-61683EC065FE}" Advertise="no" Context="InprocServer32" ThreadingModel="apartment" />
|
||||||
</File>
|
</File>
|
||||||
</Component>
|
</Component>
|
||||||
|
<!--
|
||||||
|
Currently unclear how to detect ARM64 device at this point.
|
||||||
|
In any case, the shell extension doesn't appear to work, so installing a non-functional
|
||||||
|
pyshellext_amd64.dll for a different platform isn't any worse.
|
||||||
|
<Component Id="pyshellext_arm64.dll" Directory="LauncherInstallDirectory" Guid="{C591963D-7FC6-4FCE-8642-5E01E6B8848F}">
|
||||||
|
<File Id="pyshellext_arm64.dll" Name="pyshellext.arm64.dll" Source="!(bindpath.BuildARM64)\pyshellext.dll">
|
||||||
|
<Class Id="{BEA218D2-6950-497B-9434-61683EC065FE}" Advertise="no" Context="InprocServer32" ThreadingModel="apartment" />
|
||||||
|
</File>
|
||||||
|
</Component>-->
|
||||||
</ComponentGroup>
|
</ComponentGroup>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</Wix>
|
</Wix>
|
||||||
|
|
Loading…
Reference in New Issue