From: Steve Dower Date: Sat, 29 Jun 2019 17:34:11 +0000 (-0700) Subject: bpo-37369: Fix initialization of sys members when launched via an app container ... X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=9048c49322a5229ff99610aba35913ffa295ebb7;p=python bpo-37369: Fix initialization of sys members when launched via an app container (GH-14428) sys._base_executable is now always defined on all platforms, and can be overridden through configuration. Also adds test.support.PythonSymlink to encapsulate platform-specific logic for symlinking sys.executable --- diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index 67f38e2650..297fbf7079 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -373,10 +373,11 @@ typedef struct { module_search_paths_set is equal to zero. */ - wchar_t *executable; /* sys.executable */ - wchar_t *prefix; /* sys.prefix */ - wchar_t *base_prefix; /* sys.base_prefix */ - wchar_t *exec_prefix; /* sys.exec_prefix */ + wchar_t *executable; /* sys.executable */ + wchar_t *base_executable; /* sys._base_executable */ + wchar_t *prefix; /* sys.prefix */ + wchar_t *base_prefix; /* sys.base_prefix */ + wchar_t *exec_prefix; /* sys.exec_prefix */ wchar_t *base_exec_prefix; /* sys.base_exec_prefix */ /* --- Parameter only used by Py_Main() ---------- */ diff --git a/Include/internal/pycore_pathconfig.h b/Include/internal/pycore_pathconfig.h index be12c6f5cf..9e0ba0b01a 100644 --- a/Include/internal/pycore_pathconfig.h +++ b/Include/internal/pycore_pathconfig.h @@ -27,6 +27,8 @@ typedef struct _PyPathConfig { are ignored when their value are equal to -1 (unset). */ int isolated; int site_import; + /* Set when a venv is detected */ + wchar_t *base_executable; } _PyPathConfig; #define _PyPathConfig_INIT \ diff --git a/Lib/multiprocessing/popen_spawn_win32.py b/Lib/multiprocessing/popen_spawn_win32.py index de4c5ecf1f..ea9c555da3 100644 --- a/Lib/multiprocessing/popen_spawn_win32.py +++ b/Lib/multiprocessing/popen_spawn_win32.py @@ -22,8 +22,7 @@ WINSERVICE = sys.executable.lower().endswith("pythonservice.exe") def _path_eq(p1, p2): return p1 == p2 or os.path.normcase(p1) == os.path.normcase(p2) -WINENV = (hasattr(sys, '_base_executable') and - not _path_eq(sys.executable, sys._base_executable)) +WINENV = not _path_eq(sys.executable, sys._base_executable) def _close_handles(*handles): diff --git a/Lib/site.py b/Lib/site.py index e7aafb7011..a065ab0b5d 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -459,13 +459,6 @@ def venv(known_paths): env = os.environ if sys.platform == 'darwin' and '__PYVENV_LAUNCHER__' in env: executable = sys._base_executable = os.environ['__PYVENV_LAUNCHER__'] - elif sys.platform == 'win32' and '__PYVENV_LAUNCHER__' in env: - executable = sys.executable - import _winapi - sys._base_executable = _winapi.GetModuleFileName(0) - # bpo-35873: Clear the environment variable to avoid it being - # inherited by child processes. - del os.environ['__PYVENV_LAUNCHER__'] else: executable = sys.executable exe_dir, _ = os.path.split(os.path.abspath(executable)) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index b5538d22fb..19ea9764e9 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -12,6 +12,7 @@ import faulthandler import fnmatch import functools import gc +import glob import importlib import importlib.util import io @@ -2500,6 +2501,84 @@ def skip_unless_symlink(test): msg = "Requires functional symlink implementation" return test if ok else unittest.skip(msg)(test) +class PythonSymlink: + """Creates a symlink for the current Python executable""" + def __init__(self, link=None): + self.link = link or os.path.abspath(TESTFN) + self._linked = [] + self.real = os.path.realpath(sys.executable) + self._also_link = [] + + self._env = None + + self._platform_specific() + + def _platform_specific(self): + pass + + if sys.platform == "win32": + def _platform_specific(self): + import _winapi + + if os.path.lexists(self.real) and not os.path.exists(self.real): + # App symlink appears to not exist, but we want the + # real executable here anyway + self.real = _winapi.GetModuleFileName(0) + + dll = _winapi.GetModuleFileName(sys.dllhandle) + src_dir = os.path.dirname(dll) + dest_dir = os.path.dirname(self.link) + self._also_link.append(( + dll, + os.path.join(dest_dir, os.path.basename(dll)) + )) + for runtime in glob.glob(os.path.join(src_dir, "vcruntime*.dll")): + self._also_link.append(( + runtime, + os.path.join(dest_dir, os.path.basename(runtime)) + )) + + self._env = {k.upper(): os.getenv(k) for k in os.environ} + self._env["PYTHONHOME"] = os.path.dirname(self.real) + if sysconfig.is_python_build(True): + self._env["PYTHONPATH"] = os.path.dirname(os.__file__) + + def __enter__(self): + os.symlink(self.real, self.link) + self._linked.append(self.link) + for real, link in self._also_link: + os.symlink(real, link) + self._linked.append(link) + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + for link in self._linked: + try: + os.remove(link) + except IOError as ex: + if verbose: + print("failed to clean up {}: {}".format(link, ex)) + + def _call(self, python, args, env, returncode): + cmd = [python, *args] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=env) + r = p.communicate() + if p.returncode != returncode: + if verbose: + print(repr(r[0])) + print(repr(r[1]), file=sys.stderr) + raise RuntimeError( + 'unexpected return code: {0} (0x{0:08X})'.format(p.returncode)) + return r + + def call_real(self, *args, returncode=0): + return self._call(self.real, args, None, returncode) + + def call_link(self, *args, returncode=0): + return self._call(self.link, args, self._env, returncode) + + _can_xattr = None def can_xattr(): global _can_xattr diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index b89748938b..9c78aa059f 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -362,6 +362,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 'pythonpath_env': None, 'home': None, 'executable': GET_DEFAULT_CONFIG, + 'base_executable': GET_DEFAULT_CONFIG, 'prefix': GET_DEFAULT_CONFIG, 'base_prefix': GET_DEFAULT_CONFIG, @@ -534,14 +535,16 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): if expected['stdio_errors'] is self.GET_DEFAULT_CONFIG: expected['stdio_errors'] = 'surrogateescape' + if sys.platform == 'win32': + default_executable = self.test_exe + elif expected['program_name'] is not self.GET_DEFAULT_CONFIG: + default_executable = os.path.abspath(expected['program_name']) + else: + default_executable = os.path.join(os.getcwd(), '_testembed') if expected['executable'] is self.GET_DEFAULT_CONFIG: - if sys.platform == 'win32': - expected['executable'] = self.test_exe - else: - if expected['program_name'] is not self.GET_DEFAULT_CONFIG: - expected['executable'] = os.path.abspath(expected['program_name']) - else: - expected['executable'] = os.path.join(os.getcwd(), '_testembed') + expected['executable'] = default_executable + if expected['base_executable'] is self.GET_DEFAULT_CONFIG: + expected['base_executable'] = default_executable if expected['program_name'] is self.GET_DEFAULT_CONFIG: expected['program_name'] = './_testembed' diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 8357ee9145..87d4924a34 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -610,9 +610,10 @@ class CGIHTTPServerTestCase(BaseTestCase): # The shebang line should be pure ASCII: use symlink if possible. # See issue #7668. + self._pythonexe_symlink = None if support.can_symlink(): self.pythonexe = os.path.join(self.parent_dir, 'python') - os.symlink(sys.executable, self.pythonexe) + self._pythonexe_symlink = support.PythonSymlink(self.pythonexe).__enter__() else: self.pythonexe = sys.executable @@ -655,8 +656,8 @@ class CGIHTTPServerTestCase(BaseTestCase): def tearDown(self): try: os.chdir(self.cwd) - if self.pythonexe != sys.executable: - os.remove(self.pythonexe) + if self._pythonexe_symlink: + self._pythonexe_symlink.__exit__(None, None, None) if self.nocgi_path: os.remove(self.nocgi_path) if self.file1_path: diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 9cf17726d9..8b64923e17 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -20,37 +20,9 @@ class PlatformTest(unittest.TestCase): @support.skip_unless_symlink def test_architecture_via_symlink(self): # issue3762 - # On Windows, the EXE needs to know where pythonXY.dll and *.pyd is at - # so we add the directory to the path, PYTHONHOME and PYTHONPATH. - env = None - if sys.platform == "win32": - env = {k.upper(): os.environ[k] for k in os.environ} - env["PATH"] = "{};{}".format( - os.path.dirname(sys.executable), env.get("PATH", "")) - env["PYTHONHOME"] = os.path.dirname(sys.executable) - if sysconfig.is_python_build(True): - env["PYTHONPATH"] = os.path.dirname(os.__file__) - - def get(python, env=None): - cmd = [python, '-c', - 'import platform; print(platform.architecture())'] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=env) - r = p.communicate() - if p.returncode: - print(repr(r[0])) - print(repr(r[1]), file=sys.stderr) - self.fail('unexpected return code: {0} (0x{0:08X})' - .format(p.returncode)) - return r - - real = os.path.realpath(sys.executable) - link = os.path.abspath(support.TESTFN) - os.symlink(real, link) - try: - self.assertEqual(get(real), get(link, env=env)) - finally: - os.remove(link) + with support.PythonSymlink() as py: + cmd = "-c", "import platform; print(platform.architecture())" + self.assertEqual(py.call_real(*cmd), py.call_link(*cmd)) def test_platform(self): for aliased in (False, True): @@ -275,6 +247,11 @@ class PlatformTest(unittest.TestCase): os.path.exists(sys.executable+'.exe'): # Cygwin horror executable = sys.executable + '.exe' + elif sys.platform == "win32" and not os.path.exists(sys.executable): + # App symlink appears to not exist, but we want the + # real executable here anyway + import _winapi + executable = _winapi.GetModuleFileName(0) else: executable = sys.executable platform.libc_ver(executable) diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py index 1b1929885e..44e44bf5ea 100644 --- a/Lib/test/test_sysconfig.py +++ b/Lib/test/test_sysconfig.py @@ -6,7 +6,8 @@ import shutil from copy import copy from test.support import (import_module, TESTFN, unlink, check_warnings, - captured_stdout, skip_unless_symlink, change_cwd) + captured_stdout, skip_unless_symlink, change_cwd, + PythonSymlink) import sysconfig from sysconfig import (get_paths, get_platform, get_config_vars, @@ -232,39 +233,10 @@ class TestSysConfig(unittest.TestCase): self.assertEqual(get_scheme_names(), wanted) @skip_unless_symlink - def test_symlink(self): - # On Windows, the EXE needs to know where pythonXY.dll is at so we have - # to add the directory to the path. - env = None - if sys.platform == "win32": - env = {k.upper(): os.environ[k] for k in os.environ} - env["PATH"] = "{};{}".format( - os.path.dirname(sys.executable), env.get("PATH", "")) - # Requires PYTHONHOME as well since we locate stdlib from the - # EXE path and not the DLL path (which should be fixed) - env["PYTHONHOME"] = os.path.dirname(sys.executable) - if sysconfig.is_python_build(True): - env["PYTHONPATH"] = os.path.dirname(os.__file__) - - # Issue 7880 - def get(python, env=None): - cmd = [python, '-c', - 'import sysconfig; print(sysconfig.get_platform())'] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=env) - out, err = p.communicate() - if p.returncode: - print((out, err)) - self.fail('Non-zero return code {0} (0x{0:08X})' - .format(p.returncode)) - return out, err - real = os.path.realpath(sys.executable) - link = os.path.abspath(TESTFN) - os.symlink(real, link) - try: - self.assertEqual(get(real), get(link, env)) - finally: - unlink(link) + def test_symlink(self): # Issue 7880 + with PythonSymlink() as py: + cmd = "-c", "import sysconfig; print(sysconfig.get_platform())" + self.assertEqual(py.call_real(*cmd), py.call_link(*cmd)) def test_user_similar(self): # Issue #8759: make sure the posix scheme for the users diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 4f6c11b266..ea016b59ae 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -28,8 +28,8 @@ except ImportError: # Platforms that set sys._base_executable can create venvs from within # another venv, so no need to skip tests that require venv.create(). requireVenvCreate = unittest.skipUnless( - hasattr(sys, '_base_executable') - or sys.prefix == sys.base_prefix, + sys.prefix == sys.base_prefix + or sys._base_executable != sys.executable, 'cannot run venv.create from within a venv on this platform') def check_output(cmd, encoding=None): @@ -57,8 +57,14 @@ class BaseTest(unittest.TestCase): self.bindir = 'bin' self.lib = ('lib', 'python%d.%d' % sys.version_info[:2]) self.include = 'include' - executable = getattr(sys, '_base_executable', sys.executable) + executable = sys._base_executable self.exe = os.path.split(executable)[-1] + if (sys.platform == 'win32' + and os.path.lexists(executable) + and not os.path.exists(executable)): + self.cannot_link_exe = True + else: + self.cannot_link_exe = False def tearDown(self): rmtree(self.env_dir) @@ -102,7 +108,7 @@ class BasicTest(BaseTest): else: self.assertFalse(os.path.exists(p)) data = self.get_text_file_contents('pyvenv.cfg') - executable = getattr(sys, '_base_executable', sys.executable) + executable = sys._base_executable path = os.path.dirname(executable) self.assertIn('home = %s' % path, data) fn = self.get_env_file(self.bindir, self.exe) @@ -158,10 +164,6 @@ class BasicTest(BaseTest): """ Test that the prefix values are as expected. """ - #check our prefixes - self.assertEqual(sys.base_prefix, sys.prefix) - self.assertEqual(sys.base_exec_prefix, sys.exec_prefix) - # check a venv's prefixes rmtree(self.env_dir) self.run_with_capture(venv.create, self.env_dir) @@ -169,9 +171,9 @@ class BasicTest(BaseTest): cmd = [envpy, '-c', None] for prefix, expected in ( ('prefix', self.env_dir), - ('prefix', self.env_dir), - ('base_prefix', sys.prefix), - ('base_exec_prefix', sys.exec_prefix)): + ('exec_prefix', self.env_dir), + ('base_prefix', sys.base_prefix), + ('base_exec_prefix', sys.base_exec_prefix)): cmd[2] = 'import sys; print(sys.%s)' % prefix out, err = check_output(cmd) self.assertEqual(out.strip(), expected.encode()) @@ -283,7 +285,12 @@ class BasicTest(BaseTest): # symlinked to 'python3.3' in the env, even when symlinking in # general isn't wanted. if usl: - self.assertTrue(os.path.islink(fn)) + if self.cannot_link_exe: + # Symlinking is skipped when our executable is already a + # special app symlink + self.assertFalse(os.path.islink(fn)) + else: + self.assertTrue(os.path.islink(fn)) # If a venv is created from a source build and that venv is used to # run the test, the pyvenv.cfg in the venv created in the test will diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index b64125fa4f..4ab9cc6d6f 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -112,7 +112,7 @@ class EnvBuilder: prompt = self.prompt if self.prompt is not None else context.env_name context.prompt = '(%s) ' % prompt create_if_needed(env_dir) - executable = getattr(sys, '_base_executable', sys.executable) + executable = sys._base_executable dirname, exename = os.path.split(os.path.abspath(executable)) context.executable = executable context.python_dir = dirname @@ -163,47 +163,66 @@ class EnvBuilder: if self.prompt is not None: f.write(f'prompt = {self.prompt!r}\n') - def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): - """ - Try symlinking a file, and if that fails, fall back to copying. - """ - force_copy = not self.symlinks - if not force_copy: - try: - if not os.path.islink(dst): # can't link to itself! + if os.name != 'nt': + def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): + """ + Try symlinking a file, and if that fails, fall back to copying. + """ + force_copy = not self.symlinks + if not force_copy: + try: + if not os.path.islink(dst): # can't link to itself! + if relative_symlinks_ok: + assert os.path.dirname(src) == os.path.dirname(dst) + os.symlink(os.path.basename(src), dst) + else: + os.symlink(src, dst) + except Exception: # may need to use a more specific exception + logger.warning('Unable to symlink %r to %r', src, dst) + force_copy = True + if force_copy: + shutil.copyfile(src, dst) + else: + def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): + """ + Try symlinking a file, and if that fails, fall back to copying. + """ + bad_src = os.path.lexists(src) and not os.path.exists(src) + if self.symlinks and not bad_src and not os.path.islink(dst): + try: if relative_symlinks_ok: assert os.path.dirname(src) == os.path.dirname(dst) os.symlink(os.path.basename(src), dst) else: os.symlink(src, dst) - except Exception: # may need to use a more specific exception - logger.warning('Unable to symlink %r to %r', src, dst) - force_copy = True - if force_copy: - if os.name == 'nt': - # On Windows, we rewrite symlinks to our base python.exe into - # copies of venvlauncher.exe - basename, ext = os.path.splitext(os.path.basename(src)) - srcfn = os.path.join(os.path.dirname(__file__), - "scripts", - "nt", - basename + ext) - # Builds or venv's from builds need to remap source file - # locations, as we do not put them into Lib/venv/scripts - if sysconfig.is_python_build(True) or not os.path.isfile(srcfn): - if basename.endswith('_d'): - ext = '_d' + ext - basename = basename[:-2] - if basename == 'python': - basename = 'venvlauncher' - elif basename == 'pythonw': - basename = 'venvwlauncher' - src = os.path.join(os.path.dirname(src), basename + ext) - else: - src = srcfn - if not os.path.exists(src): - logger.warning('Unable to copy %r', src) return + except Exception: # may need to use a more specific exception + logger.warning('Unable to symlink %r to %r', src, dst) + + # On Windows, we rewrite symlinks to our base python.exe into + # copies of venvlauncher.exe + basename, ext = os.path.splitext(os.path.basename(src)) + srcfn = os.path.join(os.path.dirname(__file__), + "scripts", + "nt", + basename + ext) + # Builds or venv's from builds need to remap source file + # locations, as we do not put them into Lib/venv/scripts + if sysconfig.is_python_build(True) or not os.path.isfile(srcfn): + if basename.endswith('_d'): + ext = '_d' + ext + basename = basename[:-2] + if basename == 'python': + basename = 'venvlauncher' + elif basename == 'pythonw': + basename = 'venvwlauncher' + src = os.path.join(os.path.dirname(src), basename + ext) + else: + src = srcfn + if not os.path.exists(src): + if not bad_src: + logger.warning('Unable to copy %r', src) + return shutil.copyfile(src, dst) @@ -251,7 +270,7 @@ class EnvBuilder: for suffix in suffixes: src = os.path.join(dirname, suffix) - if os.path.exists(src): + if os.path.lexists(src): copier(src, os.path.join(binpath, suffix)) if sysconfig.is_python_build(True): diff --git a/Misc/NEWS.d/next/Windows/2019-06-28-09-44-08.bpo-37369.1iVpxq.rst b/Misc/NEWS.d/next/Windows/2019-06-28-09-44-08.bpo-37369.1iVpxq.rst new file mode 100644 index 0000000000..5eaed61a92 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2019-06-28-09-44-08.bpo-37369.1iVpxq.rst @@ -0,0 +1 @@ +Fixes path for :data:`sys.executable` when running from the Microsoft Store. diff --git a/PC/getpathp.c b/PC/getpathp.c index e86cf13a49..7465d314b0 100644 --- a/PC/getpathp.c +++ b/PC/getpathp.c @@ -537,14 +537,28 @@ get_program_full_path(const PyConfig *config, wchar_t program_full_path[MAXPATHLEN+1]; memset(program_full_path, 0, sizeof(program_full_path)); + if (!GetModuleFileNameW(NULL, program_full_path, MAXPATHLEN)) { + /* GetModuleFileName should never fail when passed NULL */ + return _PyStatus_ERR("Cannot determine program path"); + } + /* The launcher may need to force the executable path to a * different environment, so override it here. */ pyvenv_launcher = _wgetenv(L"__PYVENV_LAUNCHER__"); if (pyvenv_launcher && pyvenv_launcher[0]) { + /* If overridden, preserve the original full path */ + pathconfig->base_executable = PyMem_RawMalloc( + sizeof(wchar_t) * (MAXPATHLEN + 1)); + PyStatus status = canonicalize(pathconfig->base_executable, + program_full_path); + if (_PyStatus_EXCEPTION(status)) { + return status; + } + wcscpy_s(program_full_path, MAXPATHLEN+1, pyvenv_launcher); - } else if (!GetModuleFileNameW(NULL, program_full_path, MAXPATHLEN)) { - /* GetModuleFileName should never fail when passed NULL */ - return _PyStatus_ERR("Cannot determine program path"); + /* bpo-35873: Clear the environment variable to avoid it being + * inherited by child processes. */ + _wputenv_s(L"__PYVENV_LAUNCHER__", L""); } pathconfig->program_full_path = PyMem_RawMalloc( diff --git a/PC/python_uwp.cpp b/PC/python_uwp.cpp index dd1edde730..73e0d82e55 100644 --- a/PC/python_uwp.cpp +++ b/PC/python_uwp.cpp @@ -6,6 +6,9 @@ #define WIN32_LEAN_AND_MEAN #include #include +#include + +#include #include #include @@ -24,192 +27,219 @@ const wchar_t *PROGNAME = L"python.exe"; #endif #endif -static void -set_user_base() +static std::wstring +get_user_base() { - wchar_t envBuffer[2048]; try { const auto appData = winrt::Windows::Storage::ApplicationData::Current(); if (appData) { const auto localCache = appData.LocalCacheFolder(); if (localCache) { auto path = localCache.Path(); - if (!path.empty() && - !wcscpy_s(envBuffer, path.c_str()) && - !wcscat_s(envBuffer, L"\\local-packages") - ) { - _wputenv_s(L"PYTHONUSERBASE", envBuffer); + if (!path.empty()) { + return std::wstring(path) + L"\\local-packages"; } } } } catch (...) { } + return std::wstring(); } -static const wchar_t * -get_argv0(const wchar_t *argv0) +static std::wstring +get_package_family() { - winrt::hstring installPath; - const wchar_t *launcherPath; - wchar_t *buffer; - size_t len; - - launcherPath = _wgetenv(L"__PYVENV_LAUNCHER__"); - if (launcherPath && launcherPath[0]) { - len = wcslen(launcherPath) + 1; - buffer = (wchar_t *)malloc(sizeof(wchar_t) * len); - if (!buffer) { - Py_FatalError("out of memory"); - return NULL; - } - if (wcscpy_s(buffer, len, launcherPath)) { - Py_FatalError("failed to copy to buffer"); - return NULL; + try { + const auto package = winrt::Windows::ApplicationModel::Package::Current(); + if (package) { + const auto id = package.Id(); + if (id) { + return std::wstring(id.FamilyName()); + } } - return buffer; } + catch (...) { + } + + return std::wstring(); +} +static std::wstring +get_package_home() +{ try { const auto package = winrt::Windows::ApplicationModel::Package::Current(); if (package) { - const auto install = package.InstalledLocation(); - if (install) { - installPath = install.Path(); + const auto path = package.InstalledLocation(); + if (path) { + return std::wstring(path.Path()); } } } catch (...) { } - if (!installPath.empty()) { - len = installPath.size() + wcslen(PROGNAME) + 2; - } else { - len = wcslen(argv0) + wcslen(PROGNAME) + 1; - } + return std::wstring(); +} - buffer = (wchar_t *)malloc(sizeof(wchar_t) * len); - if (!buffer) { - Py_FatalError("out of memory"); - return NULL; - } +static PyStatus +set_process_name(PyConfig *config) +{ + PyStatus status = PyStatus_Ok(); + std::wstring executable; - if (!installPath.empty()) { - if (wcscpy_s(buffer, len, installPath.c_str())) { - Py_FatalError("failed to copy to buffer"); - return NULL; - } - if (wcscat_s(buffer, len, L"\\")) { - Py_FatalError("failed to concatenate backslash"); - return NULL; - } - } else { - if (wcscpy_s(buffer, len, argv0)) { - Py_FatalError("failed to copy argv[0]"); - return NULL; - } + const auto home = get_package_home(); + const auto family = get_package_family(); - wchar_t *name = wcsrchr(buffer, L'\\'); - if (name) { - name[1] = L'\0'; - } else { - buffer[0] = L'\0'; + if (!family.empty()) { + PWSTR localAppData; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, + NULL, &localAppData))) { + executable = std::wstring(localAppData) + + L"\\Microsoft\\WindowsApps\\" + + family + + L"\\" + + PROGNAME; + + CoTaskMemFree(localAppData); } } - if (wcscat_s(buffer, len, PROGNAME)) { - Py_FatalError("failed to concatenate program name"); - return NULL; + /* Only use module filename if we don't have a home */ + if (home.empty() && executable.empty()) { + executable.resize(MAX_PATH); + while (true) { + DWORD len = GetModuleFileNameW( + NULL, executable.data(), (DWORD)executable.size()); + if (len == 0) { + executable.clear(); + break; + } else if (len == executable.size() && + GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + executable.resize(len * 2); + } else { + executable.resize(len); + break; + } + } } - return buffer; -} - -static wchar_t * -get_process_name() -{ - DWORD bufferLen = MAX_PATH; - DWORD len = bufferLen; - wchar_t *r = NULL; - - while (!r) { - r = (wchar_t *)malloc(bufferLen * sizeof(wchar_t)); - if (!r) { - Py_FatalError("out of memory"); - return NULL; + if (!home.empty()) { + status = PyConfig_SetString(config, &config->home, home.c_str()); + if (PyStatus_Exception(status)) { + return status; } - len = GetModuleFileNameW(NULL, r, bufferLen); - if (len == 0) { - free((void *)r); - return NULL; - } else if (len == bufferLen && - GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - free(r); - r = NULL; - bufferLen *= 2; + } + + const wchar_t *launcherPath = _wgetenv(L"__PYVENV_LAUNCHER__"); + if (launcherPath) { + if (!executable.empty()) { + status = PyConfig_SetString(config, &config->base_executable, + executable.c_str()); + if (PyStatus_Exception(status)) { + return status; + } } + + status = PyConfig_SetString( + config, &config->executable, launcherPath); + + /* bpo-35873: Clear the environment variable to avoid it being + * inherited by child processes. */ + _wputenv_s(L"__PYVENV_LAUNCHER__", L""); + } else if (!executable.empty()) { + status = PyConfig_SetString( + config, &config->executable, executable.c_str()); } - return r; + return status; } int wmain(int argc, wchar_t **argv) { - const wchar_t **new_argv; - int new_argc; - const wchar_t *exeName; + PyStatus status; - new_argc = argc; - new_argv = (const wchar_t**)malloc(sizeof(wchar_t *) * (argc + 2)); - if (new_argv == NULL) { - Py_FatalError("out of memory"); - return -1; + PyPreConfig preconfig; + PyConfig config; + + PyPreConfig_InitPythonConfig(&preconfig); + status = Py_PreInitializeFromArgs(&preconfig, argc, argv); + if (PyStatus_Exception(status)) { + goto fail; } - exeName = get_process_name(); + status = PyConfig_InitPythonConfig(&config); + if (PyStatus_Exception(status)) { + goto fail; + } - new_argv[0] = get_argv0(exeName ? exeName : argv[0]); - for (int i = 1; i < argc; ++i) { - new_argv[i] = argv[i]; + status = PyConfig_SetArgv(&config, argc, argv); + if (PyStatus_Exception(status)) { + goto fail; } - set_user_base(); + status = set_process_name(&config); + if (PyStatus_Exception(status)) { + goto fail; + } - if (exeName) { - const wchar_t *p = wcsrchr(exeName, L'\\'); - if (p) { - const wchar_t *moduleName = NULL; - if (*p++ == L'\\') { - if (wcsnicmp(p, L"pip", 3) == 0) { - moduleName = L"pip"; - /* No longer required when pip 19.1 is added */ - _wputenv_s(L"PIP_USER", L"true"); - } else if (wcsnicmp(p, L"idle", 4) == 0) { - moduleName = L"idlelib"; - } - } + const wchar_t *p = _wgetenv(L"PYTHONUSERBASE"); + if (!p || !*p) { + _wputenv_s(L"PYTHONUSERBASE", get_user_base().c_str()); + } - if (moduleName) { - new_argc += 2; - for (int i = argc; i >= 1; --i) { - new_argv[i + 2] = new_argv[i]; - } - new_argv[1] = L"-m"; - new_argv[2] = moduleName; + p = wcsrchr(argv[0], L'\\'); + if (!p) { + p = argv[0]; + } + if (p) { + if (*p == L'\\') { + p++; + } + + const wchar_t *moduleName = NULL; + if (wcsnicmp(p, L"pip", 3) == 0) { + moduleName = L"pip"; + /* No longer required when pip 19.1 is added */ + _wputenv_s(L"PIP_USER", L"true"); + } else if (wcsnicmp(p, L"idle", 4) == 0) { + moduleName = L"idlelib"; + } + + if (moduleName) { + status = PyConfig_SetString(&config, &config.run_module, moduleName); + if (PyStatus_Exception(status)) { + goto fail; + } + status = PyConfig_SetString(&config, &config.run_filename, NULL); + if (PyStatus_Exception(status)) { + goto fail; + } + status = PyConfig_SetString(&config, &config.run_command, NULL); + if (PyStatus_Exception(status)) { + goto fail; } } } - /* Override program_full_path from here so that - sys.executable is set correctly. */ - _Py_SetProgramFullPath(new_argv[0]); - - int result = Py_Main(new_argc, (wchar_t **)new_argv); + status = Py_InitializeFromConfig(&config); + if (PyStatus_Exception(status)) { + goto fail; + } + PyConfig_Clear(&config); - free((void *)exeName); - free((void *)new_argv); + return Py_RunMain(); - return result; +fail: + PyConfig_Clear(&config); + if (PyStatus_IsExit(status)) { + return status.exitcode; + } + assert(PyStatus_Exception(status)); + Py_ExitStatusException(status); + /* Unreachable code */ + return 0; } #ifdef PYTHONW diff --git a/Python/initconfig.c b/Python/initconfig.c index 9c4cfbeb6b..786f6945c1 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -528,6 +528,7 @@ PyConfig_Clear(PyConfig *config) config->module_search_paths_set = 0; CLEAR(config->executable); + CLEAR(config->base_executable); CLEAR(config->prefix); CLEAR(config->base_prefix); CLEAR(config->exec_prefix); @@ -765,6 +766,7 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2) COPY_ATTR(module_search_paths_set); COPY_WSTR_ATTR(executable); + COPY_WSTR_ATTR(base_executable); COPY_WSTR_ATTR(prefix); COPY_WSTR_ATTR(base_prefix); COPY_WSTR_ATTR(exec_prefix); @@ -865,6 +867,7 @@ config_as_dict(const PyConfig *config) SET_ITEM_WSTR(home); SET_ITEM_WSTRLIST(module_search_paths); SET_ITEM_WSTR(executable); + SET_ITEM_WSTR(base_executable); SET_ITEM_WSTR(prefix); SET_ITEM_WSTR(base_prefix); SET_ITEM_WSTR(exec_prefix); @@ -2404,6 +2407,7 @@ PyConfig_Read(PyConfig *config) assert(config->module_search_paths_set != 0); /* don't check config->module_search_paths */ assert(config->executable != NULL); + assert(config->base_executable != NULL); assert(config->prefix != NULL); assert(config->base_prefix != NULL); assert(config->exec_prefix != NULL); diff --git a/Python/pathconfig.c b/Python/pathconfig.c index ec67405a28..79ec4af00d 100644 --- a/Python/pathconfig.c +++ b/Python/pathconfig.c @@ -57,6 +57,7 @@ pathconfig_clear(_PyPathConfig *config) CLEAR(config->module_search_path); CLEAR(config->home); CLEAR(config->program_name); + CLEAR(config->base_executable); #undef CLEAR PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc); @@ -89,6 +90,14 @@ pathconfig_calculate(_PyPathConfig *pathconfig, const PyConfig *config) status = _PyStatus_NO_MEMORY(); goto error; } + if (config->base_executable) { + PyMem_RawFree(new_config.base_executable); + if (copy_wstr(&new_config.base_executable, + config->base_executable) < 0) { + status = _PyStatus_NO_MEMORY(); + goto error; + } + } pathconfig_clear(pathconfig); *pathconfig = new_config; @@ -132,6 +141,7 @@ _PyPathConfig_SetGlobal(const _PyPathConfig *config) COPY_ATTR(module_search_path); COPY_ATTR(program_name); COPY_ATTR(home); + COPY_ATTR(base_executable); pathconfig_clear(&_Py_path_config); /* Steal new_config strings; don't clear new_config */ @@ -224,6 +234,9 @@ _PyConfig_SetPathConfig(const PyConfig *config) if (copy_wstr(&pathconfig.home, config->home) < 0) { goto no_memory; } + if (copy_wstr(&pathconfig.base_executable, config->base_executable) < 0) { + goto no_memory; + } status = _PyPathConfig_SetGlobal(&pathconfig); if (_PyStatus_EXCEPTION(status)) { @@ -321,6 +334,13 @@ config_calculate_pathconfig(PyConfig *config) } } + if (config->base_executable == NULL) { + if (copy_wstr(&config->base_executable, + pathconfig.base_executable) < 0) { + goto no_memory; + } + } + if (pathconfig.isolated != -1) { config->isolated = pathconfig.isolated; } @@ -367,6 +387,14 @@ _PyConfig_InitPathConfig(PyConfig *config) return _PyStatus_NO_MEMORY(); } } + + if (config->base_executable == NULL) { + if (copy_wstr(&config->base_executable, + config->executable) < 0) { + return _PyStatus_NO_MEMORY(); + } + } + return _PyStatus_OK(); } @@ -434,6 +462,8 @@ Py_SetPath(const wchar_t *path) _Py_path_config.home = NULL; new_config.program_name = _Py_path_config.program_name; _Py_path_config.program_name = NULL; + new_config.base_executable = _Py_path_config.base_executable; + _Py_path_config.base_executable = NULL; pathconfig_clear(&_Py_path_config); _Py_path_config = new_config; diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 1c57561412..dc198a5d1e 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2850,6 +2850,7 @@ _PySys_InitMain(_PyRuntimeState *runtime, PyThreadState *tstate) COPY_LIST("path", config->module_search_paths); SET_SYS_FROM_WSTR("executable", config->executable); + SET_SYS_FROM_WSTR("_base_executable", config->base_executable); SET_SYS_FROM_WSTR("prefix", config->prefix); SET_SYS_FROM_WSTR("base_prefix", config->base_prefix); SET_SYS_FROM_WSTR("exec_prefix", config->exec_prefix);