diff --git a/Doc/library/venv.rst b/Doc/library/venv.rst --- a/Doc/library/venv.rst +++ b/Doc/library/venv.rst @@ -81,7 +81,7 @@ * ``symlinks`` -- a Boolean value indicating whether to attempt to symlink the Python binary (and any necessary DLLs or other binaries, e.g. ``pythonw.exe``), rather than copying. Defaults to ``True`` on Linux and - Unix systems, but ``False`` on Windows and Mac OS X. + Unix systems, but ``False`` on Windows. * ``upgrade`` -- a Boolean value which, if True, will upgrade an existing environment with the running Python - for use when that Python has been diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -154,17 +154,41 @@ """ for usl in (False, True): builder = venv.EnvBuilder(clear=True, symlinks=usl) - if (usl and sys.platform == 'darwin' and - '__PYVENV_LAUNCHER__' in os.environ): - self.assertRaises(ValueError, builder.create, self.env_dir) - else: - builder.create(self.env_dir) - fn = self.get_env_file(self.bindir, self.exe) - # Don't test when False, because e.g. 'python' is always - # symlinked to 'python3.3' in the env, even when symlinking in - # general isn't wanted. - if usl: - self.assertTrue(os.path.islink(fn)) + builder.create(self.env_dir) + fn = self.get_env_file(self.bindir, self.exe) + # Don't test when False, because e.g. 'python' is always + # symlinked to 'python3.3' in the env, even when symlinking in + # general isn't wanted. + if usl: + self.assertTrue(os.path.islink(fn)) + + def test_executable(self): + """ + Test that the sys.executable value is as expected. + """ + shutil.rmtree(self.env_dir) + self.run_with_capture(venv.create, self.env_dir) + envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe) + cmd = [envpy, '-c', 'import sys; print(sys.executable)'] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = p.communicate() + self.assertEqual(out[:-1], envpy.encode()) + + @unittest.skipUnless(can_symlink(), 'Needs symlinks') + def test_executable_symlinks(self): + """ + Test that the sys.executable value is as expected. + """ + shutil.rmtree(self.env_dir) + builder = venv.EnvBuilder(clear=True, symlinks=True) + builder.create(self.env_dir) + envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe) + cmd = [envpy, '-c', 'import sys; print(sys.executable)'] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = p.communicate() + self.assertEqual(out[:-1], envpy.encode()) def test_main(): run_unittest(BasicTest) diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -82,13 +82,6 @@ :param env_dir: The target directory to create an environment in. """ - if (self.symlinks and - sys.platform == 'darwin' and - sysconfig.get_config_var('PYTHONFRAMEWORK')): - # Symlinking the stub executable in an OSX framework build will - # result in a broken virtual environment. - raise ValueError( - 'Symlinking is not supported on OSX framework Python.') env_dir = os.path.abspath(env_dir) context = self.ensure_directories(env_dir) self.create_configuration(context) @@ -366,8 +359,7 @@ action='store_true', dest='system_site', help='Give the virtual environment access to the ' 'system site-packages dir.') - if os.name == 'nt' or (sys.platform == 'darwin' and - sysconfig.get_config_var('PYTHONFRAMEWORK')): + if os.name == 'nt': use_symlinks = False else: use_symlinks = True diff --git a/Mac/Tools/pythonw.c b/Mac/Tools/pythonw.c --- a/Mac/Tools/pythonw.c +++ b/Mac/Tools/pythonw.c @@ -28,6 +28,7 @@ #include #include #include +#include extern char** environ; @@ -158,9 +159,45 @@ /* Set the original executable path in the environment. */ status = _NSGetExecutablePath(path, &size); if (status == 0) { - if (realpath(path, real_path) != NULL) { - setenv("__PYVENV_LAUNCHER__", real_path, 1); - } + /* + * Note: don't call 'realpath', that will + * erase symlink information, and that + * breaks "pyvenv --symlink" + * + * It is nice to have the directory name + * as a cleaned up absolute path though, + * therefore call realpath on dirname(path) + */ + char* slash = strrchr(path, '/'); + if (slash) { + char replaced; + replaced = slash[1]; + slash[1] = 0; + if (realpath(path, real_path) == NULL) { + err(1, "realpath: %s", path); + } + slash[1] = replaced; + if (strlcat(real_path, slash, sizeof(real_path)) > sizeof(real_path)) { + errno = EINVAL; + err(1, "realpath: %s", path); + } + + } else { + if (realpath(".", real_path) == NULL) { + err(1, "realpath: %s", path); + } + if (strlcat(real_path, "/", sizeof(real_path)) > sizeof(real_path)) { + errno = EINVAL; + err(1, "realpath: %s", path); + } + if (strlcat(real_path, path, sizeof(real_path)) > sizeof(real_path)) { + errno = EINVAL; + err(1, "realpath: %s", path); + } + + } + + setenv("__PYVENV_LAUNCHER__", real_path, 1); } /* diff --git a/Modules/getpath.c b/Modules/getpath.c --- a/Modules/getpath.c +++ b/Modules/getpath.c @@ -474,6 +474,7 @@ wchar_t *defpath; #ifdef WITH_NEXT_FRAMEWORK NSModule pythonModule; + const char* modPath; #endif #ifdef __APPLE__ #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 @@ -568,8 +569,8 @@ */ pythonModule = NSModuleForSymbol(NSLookupAndBindSymbol("_Py_Initialize")); /* Use dylib functions to find out where the framework was loaded from */ - buf = (wchar_t *)NSLibraryNameForModule(pythonModule); - if (buf != NULL) { + modPath = NSLibraryNameForModule(pythonModule); + if (modPath != NULL) { /* We're in a framework. */ /* See if we might be in the build directory. The framework in the ** build directory is incomplete, it only has the .dylib and a few @@ -578,7 +579,12 @@ ** be running the interpreter in the build directory, so we use the ** build-directory-specific logic to find Lib and such. */ - wcsncpy(argv0_path, buf, MAXPATHLEN); + wchar_t* wbuf = _Py_char2wchar(modPath, NULL); + if (wbuf == NULL) { + Py_FatalError("Cannot decode framework location"); + } + + wcsncpy(argv0_path, wbuf, MAXPATHLEN); reduce(argv0_path); joinpath(argv0_path, lib_python); joinpath(argv0_path, LANDMARK); @@ -589,8 +595,9 @@ } else { /* Use the location of the library as the progpath */ - wcsncpy(argv0_path, buf, MAXPATHLEN); + wcsncpy(argv0_path, wbuf, MAXPATHLEN); } + PyMem_Free(wbuf); } #endif @@ -629,6 +636,7 @@ FILE * env_file = NULL; wcscpy(tmpbuffer, argv0_path); + joinpath(tmpbuffer, env_cfg); env_file = _Py_wfopen(tmpbuffer, L"r"); if (env_file == NULL) { diff --git a/Modules/main.c b/Modules/main.c --- a/Modules/main.c +++ b/Modules/main.c @@ -616,7 +616,29 @@ Py_SetProgramName(buffer); /* buffer is now handed off - do not free */ } else { +#ifdef WITH_NEXT_FRAMEWORK + char* pyvenv_launcher = getenv("__PYVENV_LAUNCHER__"); + + if (pyvenv_launcher && *pyvenv_launcher) { + /* Used by Mac/Tools/pythonw.c to forward + * the argv0 of the stub executable + */ + wchar_t* wbuf = _Py_char2wchar(pyvenv_launcher, NULL); + + if (wbuf == NULL) { + Py_FatalError("Cannot decode __PYVENV_LAUNCHER__"); + } + Py_SetProgramName(wbuf); + + /* Don't free wbuf, the argument to Py_SetProgramName + * must remain valid until the Py_Finalize is called. + */ + } else { + Py_SetProgramName(argv[0]); + } +#else Py_SetProgramName(argv[0]); +#endif } #else Py_SetProgramName(argv[0]);