Skip to content

Commit

Permalink
Fixes to get pex to work on windows.
Browse files Browse the repository at this point in the history
The fixes are:
 - os.link doesn't exist on windows. Always copy instead.
 - NamedTemporaryFile doesn't work correctly on windows (see https://bugs.python.org/issue14243)
 - sys.prefix is part of site.getsitepackages() on windows. Don't remove it.
  • Loading branch information
mikekap committed Jan 12, 2016
1 parent 8283de1 commit 9571a20
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 24 deletions.
41 changes: 20 additions & 21 deletions pex/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,26 @@ def do_copy():
shutil.copyfile(source, temp_dest)
os.rename(temp_dest, dest)

try:
os.link(source, dest)
except OSError as e:
if e.errno == errno.EEXIST:
# File already exists. If overwrite=True, write otherwise skip.
if overwrite:
# If the platform supports hard-linking, use that and fall back to copying.
# Windows does not support hard-linking.
if hasattr(os, 'link'):
try:
os.link(source, dest)
except OSError as e:
if e.errno == errno.EEXIST:
# File already exists. If overwrite=True, write otherwise skip.
if overwrite:
do_copy()
elif e.errno == errno.EXDEV:
# Hard link across devices, fall back on copying
do_copy()
elif e.errno == errno.EXDEV:
# Hard link across devices, fall back on copying
else:
raise
elif os.path.exists(dest):
if overwrite:
do_copy()
else:
raise
else:
do_copy()


# See http://stackoverflow.com/questions/2572172/referencing-other-modules-in-atexit
Expand Down Expand Up @@ -264,17 +272,8 @@ def link(self, src, dst, label=None):
self._ensure_parent(dst)
abs_src = src
abs_dst = os.path.join(self.chroot, dst)
try:
os.link(abs_src, abs_dst)
except OSError as e:
if e.errno == errno.EEXIST:
# File already exists, skip XXX -- ensure target and dest are same?
pass
elif e.errno == errno.EXDEV:
# Hard link across devices, fall back on copying
shutil.copyfile(abs_src, abs_dst)
else:
raise
safe_copy(abs_src, abs_dst, overwrite=False)
# TODO: Ensure the target and dest are the same if the file already exists.

def write(self, data, dst, label=None, mode='wb'):
"""Write data to ``chroot/dst`` with optional label.
Expand Down
4 changes: 2 additions & 2 deletions pex/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from __future__ import absolute_import

import subprocess
import tempfile

from .compatibility import to_bytes
from .util import named_temporary_file


_COMPILER_MAIN = """
Expand Down Expand Up @@ -78,7 +78,7 @@ def compile(self, root, relpaths):
:returns: A list of relative paths of the compiled bytecode files.
:raises: A :class:`Compiler.Error` if there was a problem bytecode compiling any of the files.
"""
with tempfile.NamedTemporaryFile() as fp:
with named_temporary_file() as fp:
fp.write(to_bytes(_COMPILER_MAIN % {'root': root, 'relpaths': relpaths}, encoding='utf-8'))
fp.flush()
process = subprocess.Popen([self._interpreter.binary, fp.name],
Expand Down
3 changes: 3 additions & 0 deletions pex/pex.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ def _site_libs(cls):
site_libs = set()
site_libs.update([sysconfig.get_python_lib(plat_specific=False),
sysconfig.get_python_lib(plat_specific=True)])
# On windows getsitepackages() returns the python stdlib too.
if sys.prefix in site_libs:
site_libs.remove(sys.prefix)
real_site_libs = set(os.path.realpath(path) for path in site_libs)
return site_libs | real_site_libs

Expand Down
17 changes: 17 additions & 0 deletions pex/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import errno
import os
import shutil
import tempfile
import uuid
from hashlib import sha1
from threading import Lock
Expand Down Expand Up @@ -200,3 +201,19 @@ def get(self, key, default=None):
def store(self, key, value):
with self._lock:
self._data[key] = value


@contextlib.contextmanager
def named_temporary_file(*args, **kwargs):
"""
Due to a bug in python (https://bugs.python.org/issue14243), we need
this to be able to use the temporary file without deleting it.
"""
assert 'delete' not in kwargs
kwargs['delete'] = False
fp = tempfile.NamedTemporaryFile(*args, **kwargs)
try:
with fp:
yield fp
finally:
os.remove(fp.name)
15 changes: 14 additions & 1 deletion tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from pex.installer import EggInstaller, WheelInstaller
from pex.pex_builder import PEXBuilder
from pex.testing import make_bdist, temporary_content, write_zipfile
from pex.util import CacheHelper, DistributionHelper
from pex.util import CacheHelper, DistributionHelper, named_temporary_file

try:
from unittest import mock
Expand Down Expand Up @@ -159,3 +159,16 @@ def test_access_zipped_assets_integration():
pass
assert output == 'accessed\n'
assert po.returncode == 0


def test_named_temporary_file():
name = ''
with named_temporary_file() as fp:
name = fp.name
fp.write(b'hi')
fp.flush()
assert os.path.exists(name)
with open(name) as new_fp:
assert new_fp.read() == b'hi'

assert not os.path.exists(name)

0 comments on commit 9571a20

Please sign in to comment.