Repository: conda/conda-build

Issue:
Conda build of symlink fails since 5f46fdd06ebb19a69a2a0b779ac40fbc6772db32
Build failed due to commit https://github.com/conda/conda-build/commit/5f46fdd06ebb19a69a2a0b779ac40fbc6772db32#

### Actual Behavior
Build is aborted with: "TODO :: Nested symlinks not handled yet".

### Expected Behavior
Build should pass as it used to be prior to last update of conda-build.

### Steps to Reproduce
Build directory that contains symlink to symlink to symlink of file.


##### `Output of conda info`
Not relevant - it's conda 4.8.2



Discussion:
We hit the same issue when building the conda-forge compilers. This regression is really painful. 
https://github.com/conda-forge/ctng-compilers-feedstock/pull/47

Resolving patch:
diff --git a/conda_build/build.py b/conda_build/build.py
--- a/conda_build/build.py
+++ b/conda_build/build.py
@@ -1340,6 +1340,37 @@ def path_type(path):
     return PathType.softlink if islink(path) else PathType.hardlink
 
 
+def _recurse_symlink_to_size(path, seen=None):
+    """recursively follow links to get size of what they finally point to
+
+    `path` is the current path
+    `seen` is a set that holds anything the function has already processed
+
+    By using `seen`, we can avoid getting stuck in cycles.
+    """
+    # set seen to an empty set on first invocation
+    if seen is None:
+        seen = set()
+
+    if path not in seen:
+        seen |= set([path])
+        dest = os.path.normpath(os.path.join(os.path.dirname(path), os.readlink(path)))
+
+        # symlink that points somewhere
+        if isdir(dest) and not islink(dest):
+            return 0
+        elif isfile(dest) and not islink(dest):
+            return os.stat(path).st_size
+        elif islink(dest):
+            return _recurse_symlink_to_size(dest, seen=seen)
+        elif not isfile(dest):
+            # this is a symlink that points to nowhere, so is zero bytes
+            warnings.warn('file %s is a symlink with no target' % path, UserWarning)
+            return 0
+
+    return 0
+
+
 def build_info_files_json_v1(m, prefix, files, files_with_prefix):
     no_link_files = m.get_value('build/no_link')
     files_json = []
@@ -1358,18 +1389,7 @@ def build_info_files_json_v1(m, prefix, files, files_with_prefix):
         if file_info["path_type"] == PathType.hardlink:
             file_info["size_in_bytes"] = os.stat(path).st_size
         elif file_info["path_type"] == PathType.softlink:
-            dest = os.path.normpath(os.path.join(os.path.dirname(path), os.readlink(path)))
-            # symlink that points somewhere
-            if isdir(dest) and not islink(dest):
-                file_info["size_in_bytes"] = 0
-            elif isfile(dest) and not islink(dest):
-                file_info["size_in_bytes"] = os.stat(path).st_size
-            elif islink(dest):
-                sys.exit("TODO :: Nested symlinks not handled yet")
-            elif not isfile(dest):
-                # this is a symlink that points to nowhere, so is zero bytes
-                file_info["size_in_bytes"] = 0
-                warnings.warn('file %s is a symlink with no target' % path, UserWarning)
+            file_info["size_in_bytes"] = _recurse_symlink_to_size(path)
         elif isdir(path):
             file_info["size_in_bytes"] = 0
         no_link = is_no_link(no_link_files, fi)


Test patch:
diff --git a/tests/test_build.py b/tests/test_build.py
--- a/tests/test_build.py
+++ b/tests/test_build.py
@@ -139,19 +139,35 @@ def test_create_info_files_json_symlinks(testing_workdir, testing_metadata):
     path_foo = os.path.join(testing_workdir, "foo")
     path_two_symlink = os.path.join(testing_workdir, "two_sl")
     symlink_to_nowhere = os.path.join(testing_workdir, "nowhere_sl")
+    recursive_symlink = os.path.join(testing_workdir, "recursive_sl")
+    cycle1_symlink = os.path.join(testing_workdir, "cycle1_sl")
+    cycle2_symlink = os.path.join(testing_workdir, "cycle2_sl")
     open(path_one, "a").close()
     open(path_two, "a").close()
     open(path_foo, "a").close()
     os.symlink(path_two, path_two_symlink)
     os.symlink(path_three, symlink_to_nowhere)
+
+    # make some recursive links
+    os.symlink(path_two_symlink, recursive_symlink)
+    os.symlink(cycle1_symlink, cycle2_symlink)
+    os.symlink(cycle2_symlink, cycle1_symlink)
+
     files_with_prefix = [("prefix/path", "text", "foo")]
-    files = ["one", "two", "foo", "two_sl", "nowhere_sl"]
+    files = ["one", "two", "foo", "two_sl", "nowhere_sl", "recursive_sl", "cycle1_sl", "cycle2_sl"]
 
     build.create_info_files_json_v1(testing_metadata, info_dir, testing_workdir, files,
                                     files_with_prefix)
     files_json_path = os.path.join(info_dir, "paths.json")
     expected_output = {
-        "paths": [{"file_mode": "text", "path_type": "hardlink", "_path": "foo",
+        "paths": [
+                  {"path_type": "softlink", "_path": "cycle1_sl",
+                   "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+                   "size_in_bytes": 0},
+                  {"path_type": "softlink", "_path": "cycle2_sl",
+                   "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+                   "size_in_bytes": 0},
+                  {"file_mode": "text", "path_type": "hardlink", "_path": "foo",
                    "prefix_placeholder": "prefix/path",
                    "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                    "size_in_bytes": 0},
@@ -161,6 +177,9 @@ def test_create_info_files_json_symlinks(testing_workdir, testing_metadata):
                   {"path_type": "hardlink", "_path": "one",
                    "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                    "size_in_bytes": 0},
+                  {"path_type": "softlink", "_path": "recursive_sl",
+                   "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+                   "size_in_bytes": 0},
                   {"path_type": "hardlink", "_path": "two",
                    "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                    "size_in_bytes": 0},