Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-91985: Ensure the same path calculations when repeated with PYTHONHOME #93512

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 48 additions & 0 deletions Lib/test/test_getpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
22 changes: 22 additions & 0 deletions Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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},
Expand Down
25 changes: 23 additions & 2 deletions Python/pathconfig.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -76,6 +79,7 @@ _PyPathConfig_ClearGlobal(void)
#undef CLEAR

PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
home_is_original = 0;
}

PyStatus
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down