8000 Closes #15307: symlinks now work on OS X with framework Python build… · python/cpython@90db661 · GitHub
[go: up one dir, main page]

Skip to content

Commit 90db661

Browse files
committed
Closes #15307: symlinks now work on OS X with framework Python builds. Patch by Ronald Oussoren.
1 parent 1171862 commit 90db661

File tree

6 files changed

+115
-27
lines changed

6 files changed

+115
-27
lines changed

Doc/library/venv.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ creation according to their needs, the :class:`EnvBuilder` class.
8181
* ``symlinks`` -- a Boolean value indicating whether to attempt to symlink the
8282
Python binary (and any necessary DLLs or other binaries,
8383
e.g. ``pythonw.exe``), rather than copying. Defaults to ``True`` on Linux and
84-
Unix systems, but ``False`` on Windows and Mac OS X.
84+
Unix systems, but ``False`` on Windows.
8585

8686
* ``upgrade`` -- a Boolean value which, if True, will upgrade an existing
8787
environment with the running Python - for use when that Python has been

Lib/test/test_venv.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,47 @@ def test_symlinking(self):
154154
"""
155155
for usl in (False, True):
156156
builder = venv.EnvBuilder(clear=True, symlinks=usl)
157-
if (usl and sys.platform == 'darwin' and
158-
'__PYVENV_LAUNCHER__' in os.environ):
159-
self.assertRaises(ValueError, builder.create, self.env_dir)
160-
else:
161-
builder.create(self.env_dir)
162-
fn = self.get_env_file(self.bindir, self.exe)
163-
# Don't test when False, because e.g. 'python' is always
164-
# symlinked to 'python3.3' in the env, even when symlinking in
165-
# general isn't wanted.
166-
if usl:
167-
self.assertTrue(os.path.islink(fn))
157+
builder.create(self.env_dir)
158+
fn = self.get_env_file(self.bindir, self.exe)
159+
# Don't test when False, because e.g. 'python' is always
160+
# symlinked to 'python3.3' in the env, even when symlinking in
161+
# general isn't wanted.
162+
if usl:
163+
self.assertTrue(os.path.islink(fn))
164+
165+
# If a venv is created from a source build and that venv is used to
166+
# run the test, the pyvenv.cfg in the venv created in the test will
167+
# point to the venv being used to run the test, and we lose the link
168+
# to the source build - so Python can't initialise properly.
169+
@unittest.skipIf(sys.prefix != sys.base_prefix, 'Test not appropriate '
170+
'in a venv')
171+
def test_executable(self):
172+
"""
173+
Test that the sys.executable value is as expected.
174+
"""
175+
shutil.rmtree(self.env_dir)
176+
self.run_with_capture(venv.create, self.env_dir)
177+
envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe)
178+
cmd = [envpy, '-c', 'import sys; print(sys.executable)']
179+
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
180+
stderr=subprocess.PIPE)
181+
out, err = p.communicate()
182+
self.assertEqual(out[:-1], envpy.encode())
183+
184+
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
185+
def test_executable_symlinks(self):
186+
"""
187+
Test that the sys.executable value is as expected.
188+
"""
189+
shutil.rmtree(self.env_dir)
190+
builder = venv.EnvBuilder(clear=True, symlinks=True)
191+
builder.create(self.env_dir)
192+
envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe)
193+
cmd = [envpy, '-c', 'import sys; print(sys.executable)']
194+
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
195+
stderr=subprocess.PIPE)
196+
out, err = p.communicate()
197+
self.assertEqual(out[:-1], envpy.encode())
168198

169199
def test_main():
170200
run_unittest(BasicTest)

Lib/venv/__init__.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,6 @@ def create(self, env_dir):
8282
:param env_dir: The target directory to create an environment in.
8383
8484
"""
85-
if (self.symlinks and
86-
sys.platform == 'darwin' and
87-
sysconfig.get_config_var('PYTHONFRAMEWORK')):
88-
# Symlinking the stub executable in an OSX framework build will
89-
# result in a broken virtual environment.
90-
raise ValueError(
91-
'Symlinking is not supported on OSX framework Python.')
9285
env_dir = os.path.abspath(env_dir)
9386
context = self.ensure_directories(env_dir)
9487
self.create_configuration(context)
@@ -366,8 +359,7 @@ def main(args=None):
366359
action='store_true', dest='system_site',
367360
help='Give the virtual environment access to the '
368361
'system site-packages dir.')
369-
if os.name == 'nt' or (sys.platform == 'darwin' and
370-
sysconfig.get_config_var('PYTHONFRAMEWORK')):
362+
if os.name == 'nt':
371363
use_symlinks = False
372364
else:
373365
use_symlinks = True

Mac/Tools/pythonw.c

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <dlfcn.h>
2929
#include <stdlib.h>
3030
#include <Python.h>
31+
#include <mach-o/dyld.h>
3132

3233

3334
extern char** environ;
@@ -158,9 +159,44 @@ main(int argc, char **argv) {
158159
/* Set the original executable path in the environment. */
159160
status = _NSGetExecutablePath(path, &size);
160161
if (status == 0) {
161-
if (realpath(path, real_path) != NULL) {
162-
setenv("__PYVENV_LAUNCHER__", real_path, 1);
162+
/*
163+
* Note: don't call 'realpath', that will
164+
* erase symlink information, and that
165+
* breaks "pyvenv --symlink"
166+
*
167+
* It is nice to have the directory name
168+
* as a cleaned up absolute path though,
169+
* therefore call realpath on dirname(path)
170+
*/
171+
char* slash = strrchr(path, '/');
172+
if (slash) {
173+
char replaced;
174+
replaced = slash[1];
175+
slash[1] = 0;
176+
if (realpath(path, real_path) == NULL) {
177+
err(1, "realpath: %s", path);
178+
}
179+
slash[1] = replaced;
180+
if (strlcat(real_path, slash, sizeof(real_path)) > sizeof(real_path)) {
181+
errno = EINVAL;
182+
err(1, "realpath: %s", path);
183+
}
184+
185+
} else {
186+
if (realpath(".", real_path) == NULL) {
187+
err(1, "realpath: %s", path);
188+
}
189+
if (strlcat(real_path, "/", sizeof(real_path)) > sizeof(real_path)) {
190+
errno = EINVAL;
191+
err(1, "realpath: %s", path);
192+
}
193+
if (strlcat(real_path, path, sizeof(real_path)) > sizeof(real_path)) {
194+
errno = EINVAL;
195+
err(1, "realpath: %s", path);
196+
}
163197
}
198+
199+
setenv("__PYVENV_LAUNCHER__", real_path, 1);
164200
}
165201

166202
/*

Modules/getpath.c

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ calculate_path(void)
474474
wchar_t *defpath;
475475
#ifdef WITH_NEXT_FRAMEWORK
476476
NSModule pythonModule;
477+
const char* modPath;
477478
#endif
478479
#ifdef __APPLE__
479480
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4
@@ -568,8 +569,8 @@ calculate_path(void)
568569
*/
569570
pythonModule = NSModuleForSymbol(NSLookupAndBindSymbol("_Py_Initialize"));
570571
/* Use dylib functions to find out where the framework was loaded from */
571-
buf = (wchar_t *)NSLibraryNameForModule(pythonModule);
572-
if (buf != NULL) {
572+
modPath = NSLibraryNameForModule(pythonModule);
573+
if (modPath != NULL) {
573574
/* We're in a framework. */
574575
/* See if we might be in the build directory. The framework in the
575576
** build directory is incomplete, it only has the .dylib and a few
@@ -578,7 +579,12 @@ calculate_path(void)
578579
** be running the interpreter in the build directory, so we use the
579580
** build-directory-specific logic to find Lib and such.
580581
*/
581-
wcsncpy(argv0_path, buf, MAXPATHLEN);
582+
wchar_t* wbuf = _Py_char2wchar(modPath, NULL);
583+
if (wbuf == NULL) {
584+
Py_FatalError("Cannot decode framework location");
585+
}
586+
587+
wcsncpy(argv0_path, wbuf, MAXPATHLEN);
582588
reduce(argv0_path);
583589
joinpath(argv0_path, lib_python);
584590
joinpath(argv0_path, LANDMARK);
@@ -589,8 +595,9 @@ calculate_path(void)
589595
}
590596
else {
591597
/* Use the location of the library as the progpath */
592-
wcsncpy(argv0_path, buf, MAXPATHLEN);
598+
wcsncpy(argv0_path, wbuf, MAXPATHLEN);
593599
}
600+
PyMem_Free(wbuf);
594601
}
595602
#endif
596603

@@ -629,6 +636,7 @@ calculate_path(void)
629636
FILE * env_file = NULL;
630637

631638
wcscpy(tmpbuffer, argv0_path);
639+
632640
joinpath(tmpbuffer, env_cfg);
633641
env_file = _Py_wfopen(tmpbuffer, L"r");
634642
if (env_file == NULL) {

Modules/main.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,29 @@ Py_Main(int argc, wchar_t **argv)
616616
Py_SetProgramName(buffer);
617617
/* buffer is now handed off - do not free */
618618
} else {
619+
#ifdef WITH_NEXT_FRAMEWORK
620+
char* pyvenv_launcher = getenv("__PYVENV_LAUNCHER__");
621+
622+
if (pyvenv_launcher && *pyvenv_launcher) {
623+
/* Used by Mac/Tools/pythonw.c to forward
624+
* the argv0 of the stub executable
625+
*/
626+
wchar_t* wbuf = _Py_char2wchar(pyvenv_launcher, NULL);
627+
628+
if (wbuf == NULL) {
629+
Py_FatalError("Cannot decode __PYVENV_LAUNCHER__");
630+
}
631+
Py_SetProgramName(wbuf);
632+
633+
/* Don't free wbuf, the argument to Py_SetProgramName
634+
* must remain valid until the Py_Finalize is called.
635+
*/
636+
} else {
637+
Py_SetProgramName(argv[0]);
638+
}
639+
#else
619640
Py_SetProgramName(argv[0]);
641+
#endif
620642
}
621643
#else
622644
Py_SetProgramName(argv[0]);

0 commit comments

Comments
 (0)
0