Repository: pex-tool/pex

Issue:
Pex file looses the executable permission of a binary file if packaged 
I have a use case where the pex file I am generating has to package another statically compiled binary. 
When the pex file is exploded the binary file has lost the executable permission. Is there anyway to preserve the permissions?



Discussion:
are you sure the binary is executable before being composed into the pex? how are you including the binary? how are you extracting/invoking it?

we have several cases where we package binaries as resources in libraries and then compose those libraries into a pex  - and AFAICT this has never been a problem.

a repro would be helpful to understand the exact situation you're facing.

Still happening with version 1.2.6:
Reproducible code is in foo.zip.
foo/yo/bar.sh has executable permissions.
After unzip run -- pex . -e bar.console:run -o foobar.pex -v
the pex file wont have executable permissions on bar.sh

[foo.zip](https://github.com/pantsbuild/pex/files/943355/foo.zip)

Is there anyway to preserve the permissions?

Resolving patch:
diff --git a/pex/util.py b/pex/util.py
--- a/pex/util.py
+++ b/pex/util.py
@@ -5,14 +5,13 @@
 
 import contextlib
 import os
-import shutil
 import tempfile
 import uuid
 from hashlib import sha1
 from site import makepath
 from threading import Lock
 
-from pex.common import rename_if_empty, safe_mkdir, safe_mkdtemp, safe_open
+from pex.common import rename_if_empty, safe_mkdir, safe_mkdtemp
 from pex.compatibility import exec_function
 from pex.finders import register_finders
 from pex.third_party.pkg_resources import (
@@ -176,11 +175,9 @@ def cache_distribution(cls, zf, source, target_dir):
       target_dir_tmp = target_dir + '.' + uuid.uuid4().hex
       for name in zf.namelist():
         if name.startswith(source) and not name.endswith('/'):
-          # strip off prefix + '/'
-          target_name = os.path.join(dependency_basename, name[len(source) + 1:])
-          with contextlib.closing(zf.open(name)) as zi:
-            with safe_open(os.path.join(target_dir_tmp, target_name), 'wb') as fp:
-              shutil.copyfileobj(zi, fp)
+          zf.extract(name, target_dir_tmp)
+      os.rename(os.path.join(target_dir_tmp, source),
+                os.path.join(target_dir_tmp, dependency_basename))
 
       rename_if_empty(target_dir_tmp, target_dir)
 


Test patch:
diff --git a/tests/test_pex.py b/tests/test_pex.py
--- a/tests/test_pex.py
+++ b/tests/test_pex.py
@@ -28,7 +28,10 @@
     named_temporary_file,
     run_simple_pex_test,
     temporary_dir,
-    write_simple_pex
+    write_simple_pex,
+    safe_mkdir,
+    temporary_content,
+    run_simple_pex
 )
 from pex.util import DistributionHelper
 
@@ -236,6 +239,56 @@ def test_pex_run():
       assert fake_stdout.read() == b'hellohello'
 
 
+def test_pex_executable():
+  # Tests that pex keeps executable permissions
+  with temporary_dir() as temp_dir:
+    pex_dir = os.path.join(temp_dir, 'pex_dir')
+    safe_mkdir(pex_dir)
+
+    with open(os.path.join(pex_dir, 'exe.py'), 'w') as fp:
+      fp.write(textwrap.dedent('''
+               import subprocess
+               import os
+               import sys
+               import my_package
+               path = os.path.join(os.path.dirname(my_package.__file__), 'bin/start.sh')
+               sys.stdout.write(subprocess.check_output([path]).decode('utf-8'))      
+               '''))
+
+    project_content = {
+      'setup.py': textwrap.dedent('''
+          from setuptools import setup
+
+          setup(
+              name='my_project',
+              version='0.0.0.0',
+              zip_safe=True,
+              packages=['my_package'],
+              package_data={'my_package': ['bin/*']},
+              install_requires=[],
+          )
+      '''),
+      'my_package/__init__.py': 0,
+      'my_package/bin/start.sh': ("#!/usr/bin/env bash\n"
+                                  "echo 'hello world from start.sh!'"),
+      'my_package/my_module.py': 'def do_something():\n  print("hello world!")\n',
+    }
+    pex_builder = PEXBuilder(path=pex_dir)
+    with temporary_content(project_content, perms=0o755) as project_dir:
+      installer = WheelInstaller(project_dir)
+      bdist = DistributionHelper.distribution_from_path(installer.bdist())
+      pex_builder.add_dist_location(bdist.location)
+      pex_builder.set_executable(os.path.join(pex_dir, 'exe.py'))
+      pex_builder.freeze()
+
+      app_pex = os.path.join(os.path.join(temp_dir, 'out_pex_dir'), 'app.pex')
+      pex_builder.build(app_pex)
+      std_out, rc = run_simple_pex(app_pex,
+                                   env={'PEX_ROOT': os.path.join(temp_dir, '.pex')})
+      assert rc == 0
+      assert std_out.decode('utf-8') == 'hello world from start.sh!\n'
+
+
 def test_pex_paths():
   # Tests that PEX_PATH allows importing sources from the referenced pex.
   with named_temporary_file() as fake_stdout: