Repository: conan-io/conan

Issue:
Permissions not preserved when uploading tar files
The execute bit is not preserved when uploading the conan_export.tgz and conan_package.tgz. This makes it very difficult to use libraries which ship binaries used for configuration (e.g., llvm-config), since the execute bit will not be set.



Discussion:

Resolving patch:
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -127,21 +127,22 @@ def compress_files(files, name, excluded):
     tgz_contents = StringIO()
     tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_contents)
 
-    def addfile(name, contents, tar):
+    def addfile(name, file_info, tar):
         info = tarfile.TarInfo(name=name)
-        string = StringIO(contents)
-        info.size = len(contents)
+        string = StringIO(file_info["contents"])
+        info.size = len(file_info["contents"])
+        info.mode = file_info["mode"]
         tar.addfile(tarinfo=info, fileobj=string)
 
-    for the_file, content in files.iteritems():
+    for the_file, info in files.iteritems():
         if the_file not in excluded:
-            addfile(the_file, content, tgz)
+            addfile(the_file, info, tgz)
 
     tgz.close()
     ret = {}
     for e in excluded:
         if e in files:
-            ret[e] = files[e]
+            ret[e] = files[e]["contents"]
     ret[name] = tgz_contents.getvalue()
 
     return ret
diff --git a/conans/util/files.py b/conans/util/files.py
--- a/conans/util/files.py
+++ b/conans/util/files.py
@@ -77,7 +77,10 @@ def build_files_set(basedir, rel_files):
     ret = {}
     for filename in rel_files:
         abs_path = os.path.join(basedir, filename)
-        ret[filename] = load(abs_path)
+        ret[filename] = {
+            "contents": load(abs_path),
+            "mode": os.stat(abs_path).st_mode
+        }
 
     return ret
 


Test patch:
diff --git a/conans/server/test/service/service_test.py b/conans/server/test/service/service_test.py
--- a/conans/server/test/service/service_test.py
+++ b/conans/server/test/service/service_test.py
@@ -107,6 +107,7 @@ def test_get_conanfile_snapshot(self):
 
         snap_expected = {'hello.cpp': md5sum(os.path.join(base_path, "hello.cpp")),
                          'conanmanifest.txt': md5sum(os.path.join(base_path, "conanmanifest.txt")),
+                         'executable': md5sum(os.path.join(base_path, "executable")),
                          'main.cpp':  md5sum(os.path.join(base_path, "main.cpp")),
                          'CMakeLists.txt':  md5sum(os.path.join(base_path, "CMakeLists.txt")),
                          'hellotest.h':  md5sum(os.path.join(base_path, "hellotest.h"))}
@@ -123,6 +124,7 @@ def fake_url_build(filename):
 
         expected_urls = {'CMakeLists.txt': fake_url_build('CMakeLists.txt'),
                          'conanmanifest.txt': fake_url_build('conanmanifest.txt'),
+                         'executable': fake_url_build('executable'),
                          'hello.cpp': fake_url_build('hello.cpp'),
                          'hellotest.h': fake_url_build('hellotest.h'),
                          'main.cpp': fake_url_build('main.cpp')}
@@ -138,6 +140,7 @@ def fake_url_build(filename):
                 + "/package/" + self.package_reference.package_id + "/" + filename
 
         expected_urls = {'CMakeLists.txt': fake_url_build('CMakeLists.txt'),
+                         'executable': fake_url_build('executable'),
                          'hello.cpp': fake_url_build('hello.cpp'),
                          'hellopackage.h': fake_url_build('hellopackage.h'),
                          'main.cpp': fake_url_build('main.cpp')}
diff --git a/conans/test/command/export_test.py b/conans/test/command/export_test.py
--- a/conans/test/command/export_test.py
+++ b/conans/test/command/export_test.py
@@ -34,7 +34,8 @@ def test_basic(self):
         expected_sums = {'hello.cpp': '4f005274b2fdb25e6113b69774dac184',
                          'main.cpp': '0479f3c223c9a656a718f3148e044124',
                          'CMakeLists.txt': 'bc3405da4bb0b51a3b9f05aca71e58c8',
-                         'conanfile.py': 'f21a98d974e9294b0d709070042c6e78',
+                         'conanfile.py': '5632cf850a7161388ab24f42b9bdb3fd',
+                         'executable': '68b329da9893e34099c7d8ad5cb9c940',
                          'helloHello0.h': '9448df034392fc8781a47dd03ae71bdd'}
         self.assertEqual(expected_sums, manif.file_sums)
 
@@ -115,7 +116,8 @@ def test_export_the_same_code(self):
         expected_sums = {'hello.cpp': '4f005274b2fdb25e6113b69774dac184',
                          'main.cpp': '0479f3c223c9a656a718f3148e044124',
                          'CMakeLists.txt': 'bc3405da4bb0b51a3b9f05aca71e58c8',
-                         'conanfile.py': 'f21a98d974e9294b0d709070042c6e78',
+                         'conanfile.py': '5632cf850a7161388ab24f42b9bdb3fd',
+                         'executable': '68b329da9893e34099c7d8ad5cb9c940',
                          'helloHello0.h': '9448df034392fc8781a47dd03ae71bdd'}
         self.assertEqual(expected_sums, digest2.file_sums)
 
@@ -148,7 +150,8 @@ def test_export_a_new_version(self):
         expected_sums = {'hello.cpp': '4f005274b2fdb25e6113b69774dac184',
                          'main.cpp': '0479f3c223c9a656a718f3148e044124',
                          'CMakeLists.txt': 'bc3405da4bb0b51a3b9f05aca71e58c8',
-                         'conanfile.py': 'c5889ea6485599c7a0cae02b54270b35',
+                         'conanfile.py': '7f7a5352e781be814b86e8f333593b4f',
+                         'executable': '68b329da9893e34099c7d8ad5cb9c940',
                          'helloHello0.h': '9448df034392fc8781a47dd03ae71bdd'}
         self.assertEqual(expected_sums, digest3.file_sums)
 
diff --git a/conans/test/tgz_md5_test.py b/conans/test/tgz_md5_test.py
--- a/conans/test/tgz_md5_test.py
+++ b/conans/test/tgz_md5_test.py
@@ -11,8 +11,16 @@ class TgzMd5Test(unittest.TestCase):
     """The md5 of a tgz should be the same if the files inside are the same"""
 
     def testMd5Name(self):
-        files = {"one_file.txt": "The contents",
-                 "Two_file.txt": "Two contents"}
+        files = {
+            "one_file.txt": {
+                "contents": "The contents",
+                "mode": 0777
+            },
+            "Two_file.txt": {
+                "contents": "Two contents",
+                "mode": 0777
+            }
+        }
         new_files = compress_files(files, PACKAGE_TGZ_NAME, excluded=[])
         folder = temp_folder()
         file_path = os.path.join(folder, PACKAGE_TGZ_NAME)
diff --git a/conans/test/upload_test.py b/conans/test/upload_test.py
--- a/conans/test/upload_test.py
+++ b/conans/test/upload_test.py
@@ -3,6 +3,8 @@
 from conans.test.utils.test_files import hello_source_files, temp_folder
 from conans.client.manager import CONANFILE
 import os
+import platform
+import stat
 from conans.paths import CONAN_MANIFEST, PACKAGE_TGZ_NAME, EXPORT_TGZ_NAME
 from conans.util.files import save
 from conans.model.ref import ConanFileReference, PackageReference
@@ -44,7 +46,8 @@ def setUp(self):
                        CONAN_MANIFEST: str(conan_digest),
                       "include/math/lib1.h": "//copy",
                       "my_lib/debug/libd.a": "//copy",
-                      "my_data/readme.txt": "//copy"}, path=reg_folder)
+                      "my_data/readme.txt": "//copy",
+                      "my_bin/executable": "//copy"}, path=reg_folder)
 
         self.package_ref = PackageReference(self.conan_ref, "myfakeid")
         self.server_pack_folder = self.test_server.paths.package(self.package_ref)
@@ -54,6 +57,10 @@ def setUp(self):
         save(os.path.join(package_folder, "lib", "my_lib", "libd.a"), "//lib")
         save(os.path.join(package_folder, "res", "shares", "readme.txt"),
              "//res")
+        save(os.path.join(package_folder, "bin", "my_bin", "executable"), "//bin")
+        os.chmod(os.path.join(package_folder, "bin", "my_bin", "executable"),
+                 os.stat(os.path.join(package_folder, "bin", "my_bin", "executable")).st_mode |
+                 stat.S_IRWXU)
 
         self.server_reg_folder = self.test_server.paths.export(self.conan_ref)
         self.assertFalse(os.path.exists(self.server_reg_folder))
@@ -88,7 +95,8 @@ def simple_test(self):
                  CONAN_MANIFEST,
                  'main.cpp',
                  'include/math/lib1.h',
-                 'my_data/readme.txt']
+                 'my_data/readme.txt',
+                 'my_bin/executable']
 
         self.assertTrue(os.path.exists(os.path.join(self.server_reg_folder, CONANFILE)))
         self.assertTrue(os.path.exists(os.path.join(self.server_reg_folder, EXPORT_TGZ_NAME)))
@@ -112,6 +120,12 @@ def simple_test(self):
                                                     "res",
                                                     "shares/readme.txt")))
 
+        if platform.system() != "Windows":
+            self.assertEqual(os.stat(os.path.join(folder,
+                                                  "bin",
+                                                  "my_bin/executable")).st_mode &
+                             stat.S_IRWXU, stat.S_IRWXU)
+
     def upload_all_test(self):
         '''Upload conans and package together'''
         # Try to upload all conans and packages
diff --git a/conans/test/utils/cpp_test_files.py b/conans/test/utils/cpp_test_files.py
--- a/conans/test/utils/cpp_test_files.py
+++ b/conans/test/utils/cpp_test_files.py
@@ -58,6 +58,7 @@ def package_info(self):
     def imports(self):
         self.copy(pattern="*.dylib", dst=".", src="lib")
         self.copy(pattern="*.dll", dst=".", src="bin")
+        self.copy(pattern="*", dst="bin", src="bin")
 """
 
 cmake_file = """
@@ -126,6 +127,9 @@ def imports(self):
 }}
 """
 
+executable = """
+"""
+
 
 def cpp_hello_source_files(name="Hello", deps=None, private_includes=False,
                            msg=None, dll_export=False, need_patch=False, pure_c=False):
@@ -177,6 +181,8 @@ def cpp_hello_source_files(name="Hello", deps=None, private_includes=False,
     if need_patch:
         ret["CMakeLists.txt"] = ret["CMakeLists.txt"].replace("project", "projct")
         ret["main%s" % ext] = ret["main%s" % ext].replace("return", "retunr") 
+    ret["executable"] = executable
+
     return ret