Repository: pypa/virtualenv

Issue:
virtualenv_directory/include is read-only on Pypy
If I run
```
$ pypy ./virtualenv-16.0.0/virtualenv.py zzz
```
then `zzz/include` is a symlink to `/usr/lib/pypy/include` and hence read-only. This makes
```
$ pip install pybind11
```
fail, which (seemingly, legitimately, by way of the `setup(headers=....)` mechanism) wants to write to that location. I should note that on CPython, the `include` directory is not a symlink, allowing modification.

Prior bug: https://github.com/pybind/pybind11/issues/1491


Discussion:
@inducer can you try to replicate with latest (``16.2.0``) and if still is true provide a PR patch?

Thanks!
Yes, the problem persists with 16.2.0 and Pypy 6.0.
As I understand this is expected because Python include directory is symlinked but the difference is that for CPython it is ``/usr/include/pythonX.Yz`` but for PyPy it is ``<pypy-dir>/include``. And this difference makes ``<venv-dir>/include`` symlink in case of PyPy. I think it's possible to work around that by disabling symlinking in such case or by always copyiing includes if such a workaround worth it.
Yes, I agree--and based on this, I would suggest that `--always-copy` should probably be the default.

Resolving patch:
diff --git a/virtualenv.py b/virtualenv.py
--- a/virtualenv.py
+++ b/virtualenv.py
@@ -1268,6 +1268,19 @@ def copy_required_files(src_dir, lib_dir, symlink):
             copyfile(join(src_dir, fn), join(lib_dir, fn), symlink)
 
 
+def copy_include_dir(include_src, include_dest, symlink):
+    """Copy headers from *include_src* to *include_dest* symlinking if required"""
+    if not os.path.isdir(include_src):
+        return
+    # PyPy headers are located in ``pypy-dir/include`` and following code
+    # avoids making ``venv-dir/include`` symlink to it
+    if IS_PYPY:
+        for fn in os.listdir(include_src):
+            copyfile(join(include_src, fn), join(include_dest, fn), symlink)
+    else:
+        copyfile(include_src, include_dest, symlink)
+
+
 def copy_tcltk(src, dest, symlink):
     """ copy tcl/tk libraries on Windows (issue #93) """
     for lib_version in "8.5", "8.6":
@@ -1354,7 +1367,7 @@ def install_python(home_dir, lib_dir, inc_dir, bin_dir, site_packages, clear, sy
     else:
         standard_lib_include_dir = join(prefix, "include", PY_VERSION + ABI_FLAGS)
     if os.path.exists(standard_lib_include_dir):
-        copyfile(standard_lib_include_dir, inc_dir, symlink)
+        copy_include_dir(standard_lib_include_dir, inc_dir, symlink)
     else:
         logger.debug("No include dir %s", standard_lib_include_dir)
 
@@ -1370,7 +1383,7 @@ def install_python(home_dir, lib_dir, inc_dir, bin_dir, site_packages, clear, sy
             # (traversing virtualenvs), whereas the platinc_dir is relative to
             # the inner virtualenv and ignores the prefix argument.
             # This seems more evolved than designed.
-            copyfile(platform_include_dir, platform_include_dest, symlink)
+            copy_include_dir(platform_include_dir, platform_include_dest, symlink)
 
     # pypy never uses exec_prefix, just ignore it
     if os.path.realpath(sys.exec_prefix) != os.path.realpath(prefix) and not IS_PYPY:


Test patch:
diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py
--- a/tests/test_virtualenv.py
+++ b/tests/test_virtualenv.py
@@ -295,6 +295,10 @@ def test_install_python_bin():
 
         for pth in required_executables:
             assert os.path.exists(os.path.join(bin_dir, pth)), "%s should exist in bin_dir" % pth
+        if not virtualenv.IS_JYTHON:
+            assert os.path.exists(inc_dir)
+        root_inc_dir = os.path.join(home_dir, "include")
+        assert not os.path.islink(root_inc_dir)
     finally:
         shutil.rmtree(tmp_virtualenv)