Repository: conan-io/conan

Issue:
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


Discussion:

Resolving patch:
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
--- 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
--- 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/util/files.py b/conans/util/files.py
--- a/conans/util/files.py
+++ b/conans/util/files.py
@@ -73,7 +73,10 @@ 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)
+            try:
+                os.utime(os.path.join(dirname, fname), None)
+            except Exception:
+                pass
 
 
 def normalize(text):


Test patch:
diff --git a/conans/test/functional/configuration/skip_broken_symlinks.py b/conans/test/functional/configuration/skip_broken_symlinks.py
new file mode 100644
--- /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)
+
+
+