Repository: conan-io/conan
Pull request: Skip symlink checks
Changelog: Feature: You can disable broken symlinks checks when packaging using `CONAN_SKIP_BROKEN_SYMLINKS_CHECK` env var or `config.skip_broken_symlinks_check=1`
Docs: https://github.com/conan-io/docs/pull/1272

Closes #4990 

Problem statement:
Opt-in skip broken symlinks check packaging
We need to package directories with broken symlinks. We are trying to package yocto sdk and we didn't manage to fix them or remove them, because the sdk stops working. So we would need something like `CONAN_SKIP_BROKEN_SYMLINKS_CHECK` to remove the check in `manifest.py` line 39

Hints from issue discussion:

Commit message: Skip symlink checks

Patch to review:
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index 085ddf9d86f..94d1e56312c 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -104,6 +104,7 @@
 # use_always_short_paths = False      # environment CONAN_USE_ALWAYS_SHORT_PATHS
 # skip_vs_projects_upgrade = False    # environment CONAN_SKIP_VS_PROJECTS_UPGRADE
 # non_interactive = False             # environment CONAN_NON_INTERACTIVE
+# skip_broken_symlinks_check = False  # enviornment CONAN_SKIP_BROKEN_SYMLINKS_CHECK
 
 # conan_make_program = make           # environment CONAN_MAKE_PROGRAM (overrides the make program used in AutoToolsBuildEnvironment.make)
 # conan_cmake_program = cmake         # environment CONAN_CMAKE_PROGRAM (overrides the make program used in CMake.cmake_program)
@@ -174,6 +175,7 @@ def env_vars(self):
                "CONAN_PRINT_RUN_COMMANDS": self._env_c("log.print_run_commands", "CONAN_PRINT_RUN_COMMANDS", "False"),
                "CONAN_COMPRESSION_LEVEL": self._env_c("general.compression_level", "CONAN_COMPRESSION_LEVEL", "9"),
                "CONAN_NON_INTERACTIVE": self._env_c("general.non_interactive", "CONAN_NON_INTERACTIVE", "False"),
+               "CONAN_SKIP_BROKEN_SYMLINKS_CHECK": self._env_c("general.skip_broken_symlinks_check", "CONAN_SKIP_BROKEN_SYMLINKS_CHECK", "False"),
                "CONAN_PYLINTRC": self._env_c("general.pylintrc", "CONAN_PYLINTRC", None),
                "CONAN_CACHE_NO_LOCKS": self._env_c("general.cache_no_locks", "CONAN_CACHE_NO_LOCKS", "False"),
                "CONAN_PYLINT_WERR": self._env_c("general.pylint_werr", "CONAN_PYLINT_WERR", None),
diff --git a/conans/model/manifest.py b/conans/model/manifest.py
index fded7c273fe..766472ff8b0 100644
--- a/conans/model/manifest.py
+++ b/conans/model/manifest.py
@@ -5,6 +5,7 @@
 
 from conans.errors import ConanException
 from conans.paths import CONAN_MANIFEST, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME
+from conans.util.env_reader import get_env
 from conans.util.files import load, md5, md5sum, save, walk
 
 
@@ -36,9 +37,10 @@ def gather_files(folder):
             if os.path.exists(abs_path):
                 file_dict[rel_path] = abs_path
             else:
-                raise ConanException("The file is a broken symlink, verify that "
-                                     "you are packaging the needed destination files: '%s'"
-                                     % abs_path)
+                if not get_env("CONAN_SKIP_BROKEN_SYMLINKS_CHECK", False):
+                    raise ConanException("The file is a broken symlink, verify that "
+                                         "you are packaging the needed destination files: '%s'"
+                                         % abs_path)
 
     return file_dict, symlinks
 
diff --git a/conans/test/functional/configuration/skip_broken_symlinks.py b/conans/test/functional/configuration/skip_broken_symlinks.py
new file mode 100644
index 00000000000..84b6d76470c
--- /dev/null
+++ b/conans/test/functional/configuration/skip_broken_symlinks.py
@@ -0,0 +1,57 @@
+import os
+import platform
+import unittest
+
+from conans.model.ref import ConanFileReference
+from conans.test.utils.tools import TestServer, TurboTestClient
+
+
+class TestSkipBrokenSymlinks(unittest.TestCase):
+
+    @unittest.skipIf(platform.system() == "Windows", "Better to test only in NIX the symlinks")
+    def test_package_broken_symlinks(self):
+        server = TestServer()
+        client = TurboTestClient(servers={"default": server})
+        client2 = TurboTestClient(servers={"default": server})
+
+        conanfile = """
+import os
+from conans import ConanFile, tools
+
+class HelloConan(ConanFile):
+
+    def package(self):
+        # Link to file.txt and then remove it
+        tools.save(os.path.join(self.package_folder, "file.txt"), "contents")
+        os.symlink(os.path.join(self.package_folder, "file.txt"), 
+                   os.path.join(self.package_folder, "link.txt"))    
+        os.unlink(os.path.join(self.package_folder, "file.txt"))
+
+"""
+        ref = ConanFileReference.loads("lib/1.0@conan/stable")
+        # By default it is not allowed
+        client.create(ref, conanfile=conanfile, assert_error=True)
+        self.assertIn("The file is a broken symlink", client.out)
+
+        # Until we deactivate the checks
+        client.run("config set general.skip_broken_symlinks_check=True")
+        pref = client.create(ref, conanfile=conanfile)
+        self.assertIn("Created package", client.out)
+        p_folder = client.cache.package_layout(pref.ref).package(pref)
+
+        # The link is there
+        link_path = os.path.join(p_folder, "link.txt")
+        self.assertTrue(os.path.islink(link_path))
+
+        # The link is broken
+        target_path = os.readlink(link_path)
+        self.assertFalse(os.path.exists(target_path))
+
+        # We can upload the package and reuse it
+        client.upload_all(ref)
+
+        client2.run("install {}".format(ref))
+        self.assertIn("Downloaded package", client2.out)
+
+
+
diff --git a/conans/util/files.py b/conans/util/files.py
index 472fa854e70..79040d1b539 100644
--- a/conans/util/files.py
+++ b/conans/util/files.py
@@ -73,7 +73,9 @@ def touch(fname, times=None):
 def touch_folder(folder):
     for dirname, _, filenames in walk(folder):
         for fname in filenames:
-            os.utime(os.path.join(dirname, fname), None)
+            path = os.path.join(dirname, fname)
+            if not os.path.islink(path):
+                os.utime(path, None)
 
 
 def normalize(text):


Code review comments:
diff_hunk: @@ -73,7 +73,9 @@ def touch(fname, times=None):
 def touch_folder(folder):
     for dirname, _, filenames in walk(folder):
         for fname in filenames:
-            os.utime(os.path.join(dirname, fname), None)
+            path = os.path.join(dirname, fname)
+            if not os.path.islink(path):
path: conans/util/files.py
text: @user1:
Maybe better a try-except, as this will almost always be true, try-except might be faster.