bpo-46566: Add new py.exe launcher implementation (GH-32062)

This commit is contained in:
Steve Dower 2022-03-29 00:21:08 +01:00 committed by GitHub
parent 5c30388f3c
commit bad86a621a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2821 additions and 16 deletions

View File

@ -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:

423
Lib/test/test_launcher.py Normal file
View File

@ -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)

View File

@ -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.

31
PC/launcher-usage.txt Normal file
View File

@ -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:

2264
PC/launcher2.c Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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" />

View File

@ -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" />

View File

@ -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>
@ -19,17 +20,26 @@
<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" />

View File

@ -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>