Repository: conan-io/conan

Issue:
Conan Create (scm) copy source removing folder symlink
To help us debug your issue please explain:

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.

```
Conan version 1.14.3
Distributor ID:	Ubuntu
Description:	Ubuntu 16.04.6 LTS
Release:	16.04
Codename:	xenial
```

Hi there,
It appears when running `conan create` locally or remotely, folder symlinks are removed when source is copied to the build folder. This is not true for file symlinks. We are using the `scm` feature.

After running `conan create` and going to the build directory:
```
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	deleted:    Libs/prebuilt/Debug
```

```
lrwxrwxrwx  1 mh70023 bluetooth    8 Feb 21 14:14 Debug -> Release/
```

All other file symlinks are working as intended and are not deleted.
I have checked my .gitignore as well for errors.




Discussion:

Resolving patch:
diff --git a/conans/client/file_copier.py b/conans/client/file_copier.py
--- a/conans/client/file_copier.py
+++ b/conans/client/file_copier.py
@@ -38,7 +38,7 @@ def __init__(self, source_folders, root_destination_folder):
         will not be used in the relative names while copying
         param source_folders: list of folders to copy things from, typically the
                                   store build folder
-        param root_destination_folder: The base folder to copy things to, typicall the
+        param root_destination_folder: The base folder to copy things to, typically the
                                        store package folder
         """
         assert isinstance(source_folders, list), "source folders must be a list"
@@ -91,11 +91,12 @@ def _copy(self, base_src, pattern, src, dst, symlinks, ignore_case, excludes, ke
         files_to_copy, link_folders = self._filter_files(src, pattern, symlinks, excludes,
                                                          ignore_case, excluded_folders)
         copied_files = self._copy_files(files_to_copy, src, dst, keep_path, symlinks)
-        self._link_folders(src, dst, link_folders)
+        self.link_folders(src, dst, link_folders)
         self._copied.extend(files_to_copy)
         return copied_files
 
-    def _filter_files(self, src, pattern, links, excludes, ignore_case, excluded_folders):
+    @staticmethod
+    def _filter_files(src, pattern, links, excludes, ignore_case, excluded_folders):
 
         """ return a list of the files matching the patterns
         The list will be relative path names wrt to the root src folder
@@ -155,7 +156,7 @@ def _filter_files(self, src, pattern, links, excludes, ignore_case, excluded_fol
         return files_to_copy, linked_folders
 
     @staticmethod
-    def _link_folders(src, dst, linked_folders):
+    def link_folders(src, dst, linked_folders):
         created_links = []
         for linked_folder in linked_folders:
             src_link = os.path.join(src, linked_folder)
diff --git a/conans/client/source.py b/conans/client/source.py
--- a/conans/client/source.py
+++ b/conans/client/source.py
@@ -5,8 +5,9 @@
 
 from conans.client import tools
 from conans.client.cmd.export import export_recipe, export_source
-from conans.errors import (ConanException, ConanExceptionInUserConanfileMethod,
-                           conanfile_exception_formatter)
+from conans.client.file_copier import FileCopier
+from conans.errors import ConanException, ConanExceptionInUserConanfileMethod, \
+    conanfile_exception_formatter
 from conans.model.conan_file import get_env_context_manager
 from conans.model.scm import SCM, get_scm_data
 from conans.paths import CONANFILE, CONAN_MANIFEST, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME
@@ -53,11 +54,18 @@ def is_excluded(origin_path):
             return True
         return False
 
+    linked_folders = []
     for src_dir, dirs, files in walk(src, followlinks=True):
+
         if is_excluded(src_dir):
             dirs[:] = []
             continue
 
+        if os.path.islink(src_dir):
+            rel_link = os.path.relpath(src_dir, src)
+            linked_folders.append(rel_link)
+            continue
+
         # Overwriting the dirs will prevents walk to get into them
         files[:] = [d for d in files if not is_excluded(os.path.join(src_dir, d))]
 
@@ -73,6 +81,8 @@ def is_excluded(origin_path):
             else:
                 shutil.copy2(src_file, dst_file)
 
+    FileCopier.link_folders(src, dst, linked_folders)
+
 
 def config_source_local(src_folder, conanfile, conanfile_path, hook_manager):
     """ Entry point for the "conan source" command.


Test patch:
diff --git a/conans/test/integration/symlinks_test.py b/conans/test/integration/symlinks_test.py
--- a/conans/test/integration/symlinks_test.py
+++ b/conans/test/integration/symlinks_test.py
@@ -1,9 +1,10 @@
 import os
 import platform
+import textwrap
 import unittest
 
 from conans.model.ref import ConanFileReference, PackageReference
-from conans.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer
+from conans.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer, TurboTestClient
 from conans.util.files import load, mkdir, save
 
 conanfile = """
@@ -233,3 +234,49 @@ def package(self):
         client.run("create . danimtb/testing")
         self.assertTrue(os.path.exists(package_file))
         self.assertTrue(os.path.exists(package_other_dir))
+
+    def test_create_keep_folder_symlink(self):
+        conanfile = textwrap.dedent("""
+            import os
+            from conans import ConanFile
+            class ConanSymlink(ConanFile):
+                name = "ConanSymlink"
+                version = "3.0.0"
+                exports_sources = ["*"]
+                def build(self):
+                    debug_path = os.path.join(self.build_folder, "debug")
+                    assert os.path.exists(debug_path), "Symlinked folder not created!"
+            """)
+
+        client = TurboTestClient()
+        client.save({"conanfile.py": conanfile})
+        client.save({"release/file.cpp": conanfile})
+        real_dir_path = os.path.join(client.current_folder, "release")
+        symlink_path = os.path.join(client.current_folder, "debug")
+        os.symlink(real_dir_path, symlink_path)
+
+        # Verify that the symlink is created correctly
+        self.assertEqual(os.path.realpath(symlink_path), real_dir_path)
+
+        ref = ConanFileReference.loads("ConanSymlink/3.0.0@user/channel")
+        package_layout = client.cache.package_layout(ref)
+        # Export the recipe and check that the symlink is still there
+        client.export(ref, conanfile=conanfile)
+        sf = package_layout.export_sources()
+        sf_symlink = os.path.join(sf, "debug")
+        self.assertTrue(os.path.islink(sf_symlink))
+        self.assertEqual(os.path.realpath(sf_symlink), os.path.join(sf, "release"))
+
+        # Now do the create
+        pref = client.create(ref, conanfile=conanfile)
+        # Assert that the symlink is preserved when copy to source from exports_sources
+        sf = package_layout.source()
+        sf_symlink = os.path.join(sf, "debug")
+        self.assertTrue(os.path.islink(sf_symlink))
+        self.assertEqual(os.path.realpath(sf_symlink), os.path.join(sf, "release"))
+
+        # Assert that the symlink is preserved when copy to build folder
+        bf = client.cache.package_layout(pref.ref).build(pref)
+        bf_symlink = os.path.join(bf, "debug")
+        self.assertTrue(os.path.islink(bf_symlink))
+        self.assertEqual(os.path.realpath(bf_symlink), os.path.join(bf, "release"))