diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index f1ca6da147376c..a08758877cb361 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -1386,6 +1386,49 @@ def test_init_pybuilddir_win32(self): api=API_COMPAT, env=env, ignore_stderr=False, cwd=tmpdir) + @unittest.skipUnless(MS_WINDOWS, 'Windows only') + def test_repeated_init_pybuilddir_pythonhome_win32(self): + # Test an out-of-build-tree layout with PYTHONHOME override, + # repeating path calculation (gh-91985). This layout is cited + # from test_buildtree_pythonhome_win32 in test_getpath. + config = self._get_expected_config() + paths = config['config']['module_search_paths'] + + for path in paths: + if not os.path.isdir(path): + continue + if os.path.exists(os.path.join(path, 'os.py')): + home = os.path.dirname(path) + break + else: + self.fail(f"Unable to find home in {paths!r}") + + with self.tmpdir_with_python() as tmpdir: + filename = os.path.join(tmpdir, 'pybuilddir.txt') + with open(filename, "w", encoding="utf8") as fp: + fp.write(tmpdir) + + config = { + 'home': home, + 'base_exec_prefix': home, + 'base_prefix': home, + 'base_executable': self.test_exe, + 'executable': self.test_exe, + 'prefix': home, + 'exec_prefix': home, + 'stdlib_dir': os.path.join(home, 'Lib'), + 'module_search_paths': [ + os.path.join(tmpdir, os.path.basename(paths[0])), # .zip + os.path.join(home, 'Lib'), + os.path.join(tmpdir), + ], + } + pyd = import_helper.import_module('_testinternalcapi').__file__ + shutil.copyfile(pyd, os.path.join(tmpdir, os.path.basename(pyd))) + self.check_all_configs("test_repeated_init_compat_config", config, + api=API_COMPAT, env=dict(PYTHONHOME=home), + ignore_stderr=False, cwd=tmpdir) + def test_init_pyvenv_cfg(self): # Test path configuration with pyvenv.cfg configuration file diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py index 5208374e20013c..e9984960796692 100644 --- a/Lib/test/test_getpath.py +++ b/Lib/test/test_getpath.py @@ -239,6 +239,54 @@ def test_buildtree_pythonhome_win32(self): actual = getpath(ns, expected) self.assertEqual(expected, actual) + def test_custom_setpythonhome_win32(self): + """Test a deterministic layout on Windows. + + PYTHONHOME, *._pth file, and pybuilddir.txt are ignored when 'home' is + explicitly set on a embedded Python. + """ + ns = MockNTNamespace( + argv0=r"C:\a\embed.exe", + ENV_PYTHONHOME=r"C:\a", + ) + # After PyConfig_SetString() or Py_SetPythonHome() is used + ns["config"]["home"] = r"C:\Python" + + ns.add_known_file(r"C:\a\pybuilddir.txt", [""]) + ns.add_known_file(r"C:\a\python._pth", [""]) + expected = dict( + executable=r"C:\a\embed.exe", + base_executable=r"C:\a\embed.exe", + prefix=r"C:\Python", + exec_prefix=r"C:\Python", + module_search_paths_set=1, + module_search_paths=[ + r"C:\a\python98.zip", + r"C:\Python\Lib", + r"C:\Python\DLLs", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_custom_module_search_paths_set_win32(self): + "Test a deterministic layout on Windows with module_search_paths" + ns = MockNTNamespace( + argv0=r"C:\a\embed.exe", + ENV_PYTHONHOME=r"C:\a", + ) + ns["config"]["home"] = r"C:\Python" + ns["config"]["module_search_paths"] = [r"C:\Python\Lib"] + ns["config"]["module_search_paths_set"] = 1 + ns.add_known_file(r"C:\a\pybuilddir.txt", [""]) + ns.add_known_file(r"C:\a\python._pth", [""]) + expected = dict( + module_search_paths_set=1, + module_search_paths=[r"C:\Python\Lib"], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + def test_normal_posix(self): "Test a 'standard' install layout on *nix" ns = MockPosixNamespace( diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 9d3d0cbddf0e53..953cc322f1e4ea 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -481,6 +481,27 @@ static int test_init_compat_config(void) } +static int test_repeated_init_compat_config(void) +{ + PyConfig config; + _PyConfig_InitCompatConfig(&config); + config_set_program_name(&config); + + for (int i=0; i<3; i++) { + PyStatus status = Py_InitializeFromConfig(&config); + if (PyStatus_Exception(status)) { + Py_ExitStatusException(status); + } + Py_Finalize(); + } + init_from_config_clear(&config); + + dump_config(); + Py_Finalize(); + return 0; +} + + static int test_init_global_config(void) { /* FIXME: test Py_IgnoreEnvironmentFlag */ @@ -1939,6 +1960,7 @@ static struct TestCase TestCases[] = { {"test_init_initialize_config", test_init_initialize_config}, {"test_preinit_compat_config", test_preinit_compat_config}, {"test_init_compat_config", test_init_compat_config}, + {"test_repeated_init_compat_config", test_repeated_init_compat_config}, {"test_init_global_config", test_init_global_config}, {"test_init_from_config", test_init_from_config}, {"test_init_parse_argv", test_init_parse_argv}, diff --git a/Python/pathconfig.c b/Python/pathconfig.c index 4271928571fa1f..c1e88ef6f79a4b 100644 --- a/Python/pathconfig.c +++ b/Python/pathconfig.c @@ -42,7 +42,10 @@ typedef struct _PyPathConfig { {.module_search_path = NULL} -_PyPathConfig _Py_path_config = _PyPathConfig_INIT; +static _PyPathConfig _Py_path_config = _PyPathConfig_INIT; + +// Turned on when _Py_path_config.home is set directly by Py_SetPythonHome() +static int home_is_original = 0; const wchar_t * @@ -76,6 +79,7 @@ _PyPathConfig_ClearGlobal(void) #undef CLEAR PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc); + home_is_original = 0; } PyStatus @@ -103,12 +107,18 @@ _PyPathConfig_ReadGlobal(PyConfig *config) COPY(exec_prefix); COPY(stdlib_dir); COPY(program_name); - COPY(home); COPY2(executable, program_full_path); // module_search_path must be initialised - not read #undef COPY #undef COPY2 + // _Py_path_config.home cannot be reused in getpath.py except when + // the value is set by Py_SetPythonHome(). + if (_Py_path_config.home && !config->home && home_is_original) { + status = PyConfig_SetString(config, &config->home, _Py_path_config.home); + if (_PyStatus_EXCEPTION(status)) goto done; + } + done: return status; } @@ -137,6 +147,12 @@ _PyPathConfig_UpdateGlobal(const PyConfig *config) } \ } while (0) + if (!_Py_path_config.home || + (config->home && wcscmp(_Py_path_config.home, config->home) != 0)) + { + home_is_original = 0; + } + COPY(prefix); COPY(exec_prefix); COPY(stdlib_dir); @@ -242,6 +258,11 @@ Py_SetPythonHome(const wchar_t *home) PyMem_RawFree(_Py_path_config.home); if (has_value) { _Py_path_config.home = _PyMem_RawWcsdup(home); + home_is_original = 1; + } + else { + _Py_path_config.home = NULL; + home_is_original = 0; } PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc);