[Python-checkins] bpo-37369: Fix initialization of sys members when launched via an app container (GH-14467)

Steve Dower webhook-mailer at python.org
Sat Jun 29 17:29:03 EDT 2019


https://github.com/python/cpython/commit/323e743d4879f1cd861d0b252775797fb7938755
commit: 323e743d4879f1cd861d0b252775797fb7938755
branch: 3.8
author: Steve Dower <steve.dower at python.org>
committer: GitHub <noreply at github.com>
date: 2019-06-29T14:28:59-07:00
summary:

bpo-37369: Fix initialization of sys members when launched via an app container (GH-14467)

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

files:
A Misc/NEWS.d/next/Windows/2019-06-28-09-44-08.bpo-37369.1iVpxq.rst
M Include/cpython/initconfig.h
M Include/internal/pycore_pathconfig.h
M Lib/multiprocessing/popen_spawn_win32.py
M Lib/site.py
M Lib/test/support/__init__.py
M Lib/test/test_embed.py
M Lib/test/test_httpservers.py
M Lib/test/test_platform.py
M Lib/test/test_sysconfig.py
M Lib/test/test_venv.py
M Lib/venv/__init__.py
M PC/getpathp.c
M PC/python_uwp.cpp
M Python/initconfig.c
M Python/pathconfig.c
M Python/sysmodule.c

diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h
index 67f38e26505c..297fbf70792f 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 be12c6f5cf32..9e0ba0b01a80 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 de4c5ecf1fa0..ea9c555da39a 100644
--- a/Lib/multiprocessing/popen_spawn_win32.py
+++ b/Lib/multiprocessing/popen_spawn_win32.py
@@ -22,8 +22,7 @@
 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 e7aafb7011cf..a065ab0b5db5 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 7c0efc783edb..ef623db87e04 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -12,6 +12,7 @@
 import fnmatch
 import functools
 import gc
+import glob
 import importlib
 import importlib.util
 import io
@@ -2498,6 +2499,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 1bc8d3aaee02..5a90b9f6f247 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 @@ def get_expected_config(self, expected_preconfig, expected, env, api,
             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 8357ee9145d7..87d4924a34b3 100644
--- a/Lib/test/test_httpservers.py
+++ b/Lib/test/test_httpservers.py
@@ -610,9 +610,10 @@ def setUp(self):
 
         # 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 @@ def setUp(self):
     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 9cf17726d92e..8b64923e174c 100644
--- a/Lib/test/test_platform.py
+++ b/Lib/test/test_platform.py
@@ -20,37 +20,9 @@ def test_architecture(self):
 
     @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 @@ def test_libc_ver(self):
            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 1b1929885edd..44e44bf5ea99 100644
--- a/Lib/test/test_sysconfig.py
+++ b/Lib/test/test_sysconfig.py
@@ -6,7 +6,8 @@
 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 @@ def test_get_scheme_names(self):
         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 24d3a69b1878..6aafa1b14c28 100644
--- a/Lib/test/test_venv.py
+++ b/Lib/test/test_venv.py
@@ -28,8 +28,8 @@
 # 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 @@ def setUp(self):
             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 @@ def test_defaults(self):
         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)
@@ -136,10 +142,6 @@ def test_prefixes(self):
         """
         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)
@@ -147,9 +149,9 @@ def test_prefixes(self):
         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())
@@ -261,7 +263,12 @@ def test_symlinking(self):
             # 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 4a49b240b8e2..caa7285b8c89 100644
--- a/Lib/venv/__init__.py
+++ b/Lib/venv/__init__.py
@@ -105,8 +105,7 @@ def create_if_needed(d):
         prompt = self.prompt if self.prompt is not None else context.env_name
         context.prompt = '(%s) ' % prompt
         create_if_needed(env_dir)
-        env = os.environ
-        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
@@ -157,47 +156,66 @@ def create_configuration(self, context):
             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)
 
@@ -245,7 +263,7 @@ def setup_python(self, context):
 
             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 000000000000..5eaed61a9261
--- /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 e86cf13a4910..7465d314b043 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 dd1edde73092..2352f45e8a3d 100644
--- a/PC/python_uwp.cpp
+++ b/PC/python_uwp.cpp
@@ -6,6 +6,9 @@
 #define WIN32_LEAN_AND_MEAN
 #include <Windows.h>
 #include <shellapi.h>
+#include <shlobj.h>
+
+#include <string>
 
 #include <winrt\Windows.ApplicationModel.h>
 #include <winrt\Windows.Storage.h>
@@ -24,192 +27,220 @@ 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_without_config;
     }
 
-    exeName = get_process_name();
+    status = PyConfig_InitPythonConfig(&config);
+    if (PyStatus_Exception(status)) {
+        goto fail_without_config;
+    }
 
-    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);
+fail_without_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 66b1b305a560..e791a0d6a09e 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);
@@ -2357,6 +2360,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 ec67405a28d0..79ec4af00d83 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 fbccea3c14fe..6a49d8992324 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -2879,6 +2879,7 @@ _PySys_InitMain(_PyRuntimeState *runtime, PyInterpreterState *interp)
     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);



More information about the Python-checkins mailing list