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:

Patch:
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index bba79edea..5e4b244f0 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -30,6 +30,7 @@ os:
         version: ["8.1", "10.0"]
     WindowsCE:
         platform: ANY
+        version: ["5.0", "6.0", "7.0", "8.0"]
     Linux:
     Macos:
         version: [None, "10.6", "10.7", "10.8", "10.9", "10.10", "10.11", "10.12", "10.13", "10.14"]
@@ -93,6 +94,7 @@ default_profile = %s
 compression_level = 9                 # environment CONAN_COMPRESSION_LEVEL
 sysrequires_sudo = True               # environment CONAN_SYSREQUIRES_SUDO
 request_timeout = 60                  # environment CONAN_REQUEST_TIMEOUT (seconds)
+default_package_id_mode = semver_direct_mode # environment CONAN_DEFAULT_PACKAGE_ID_MODE
 # sysrequires_mode = enabled          # environment CONAN_SYSREQUIRES_MODE (allowed modes enabled/verify/disabled)
 # vs_installation_preference = Enterprise, Professional, Community, BuildTools # environment CONAN_VS_INSTALLATION_PREFERENCE
 # verbose_traceback = False           # environment CONAN_VERBOSE_TRACEBACK
@@ -106,6 +108,7 @@ request_timeout = 60                  # environment CONAN_REQUEST_TIMEOUT (secon
 # 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)
@@ -176,6 +179,7 @@ class ConanClientConfigParser(ConfigParser, object):
                "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),
@@ -221,7 +225,9 @@ class ConanClientConfigParser(ConfigParser, object):
                "CONAN_MSBUILD_VERBOSITY": self._env_c("general.msbuild_verbosity",
                                                       "CONAN_MSBUILD_VERBOSITY",
                                                       None),
-               "CONAN_CACERT_PATH": self._env_c("general.cacert_path", "CONAN_CACERT_PATH", None)
+               "CONAN_CACERT_PATH": self._env_c("general.cacert_path", "CONAN_CACERT_PATH", None),
+               "CONAN_DEFAULT_PACKAGE_ID_MODE": self._env_c("general.default_package_id_mode",
+                                                            "CONAN_DEFAULT_PACKAGE_ID_MODE", None),
                }
 
         # Filter None values
@@ -363,9 +369,12 @@ class ConanClientConfigParser(ConfigParser, object):
     @property
     def default_package_id_mode(self):
         try:
-            return self.get_item("general.default_package_id_mode")
+            default_package_id_mode = get_env("CONAN_DEFAULT_PACKAGE_ID_MODE")
+            if default_package_id_mode is None:
+                default_package_id_mode = self.get_item("general.default_package_id_mode")
         except ConanException:
             return "semver_direct_mode"
+        return default_package_id_mode
 
     @property
     def storage_path(self):
diff --git a/conans/model/manifest.py b/conans/model/manifest.py
index fded7c273..766472ff8 100644
--- a/conans/model/manifest.py
+++ b/conans/model/manifest.py
@@ -5,6 +5,7 @@ import time
 
 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
index 472fa854e..dc3824f77 100644
--- 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/command/config_install_test.py b/conans/test/functional/command/config_install_test.py
index b71897f97..8dfcdbf35 100644
--- a/conans/test/functional/command/config_install_test.py
+++ b/conans/test/functional/command/config_install_test.py
@@ -48,6 +48,7 @@ level = 10                  # environment CONAN_LOGGING_LEVEL
 [general]
 compression_level = 6                 # environment CONAN_COMPRESSION_LEVEL
 cpu_count = 1             # environment CONAN_CPU_COUNT
+default_package_id_mode = full_package_mode # environment CONAN_DEFAULT_PACKAGE_ID_MODE
 
 [proxies]
 # Empty section will try to use system proxies.
@@ -131,6 +132,7 @@ class ConfigInstallTest(unittest.TestCase):
         self.assertEqual(conan_conf.get_item("log.run_to_file"), "False")
         self.assertEqual(conan_conf.get_item("log.level"), "10")
         self.assertEqual(conan_conf.get_item("general.compression_level"), "6")
+        self.assertEqual(conan_conf.get_item("general.default_package_id_mode"), "full_package_mode")
         self.assertEqual(conan_conf.get_item("general.sysrequires_sudo"), "True")
         self.assertEqual(conan_conf.get_item("general.cpu_count"), "1")
         with six.assertRaisesRegex(self, ConanException, "'config_install' doesn't exist"):
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 000000000..84b6d7647
--- /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/test/functional/graph/package_id_modes_test.py b/conans/test/functional/graph/package_id_modes_test.py
index b8faeffe5..3795aac9b 100644
--- a/conans/test/functional/graph/package_id_modes_test.py
+++ b/conans/test/functional/graph/package_id_modes_test.py
@@ -1,3 +1,4 @@
+from conans.client.tools import environment_append
 from conans.test.functional.graph.graph_manager_base import GraphManagerTest
 from conans.test.utils.conanfile import TestConanFile
 
@@ -19,11 +20,10 @@ class PackageIDGraphTests(GraphManagerTest):
         configs.append((mode, "liba/1.1.2@user/testing", "42a9ff9024adabbd54849331cf01be7d95139948"))
         configs.append((mode, "liba/1.1.1@user/stable", "b41d6c026473cffed4abded4b0eaa453497be1d2"))
 
-        for package_id_mode, ref, package_id in configs:
-            self.cache.config.set_item("general.default_package_id_mode", package_id_mode)
+        def _assert_recipe_mode(ref_arg, package_id_arg):
             libb_ref = "libb/0.1@user/testing"
             self._cache_recipe(ref, TestConanFile("liba", "0.1.1"))
-            self._cache_recipe(libb_ref, TestConanFile("libb", "0.1", requires=[ref]))
+            self._cache_recipe(libb_ref, TestConanFile("libb", "0.1", requires=[ref_arg]))
             deps_graph = self.build_graph(TestConanFile("app", "0.1", requires=[libb_ref]))
 
             self.assertEqual(3, len(deps_graph.nodes))
@@ -32,4 +32,12 @@ class PackageIDGraphTests(GraphManagerTest):
             liba = libb.dependencies[0].dst
 
             self.assertEqual(liba.package_id, "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9")
-            self.assertEqual(libb.package_id, package_id)
+            self.assertEqual(libb.package_id, package_id_arg)
+
+        for package_id_mode, ref, package_id in configs:
+            self.cache.config.set_item("general.default_package_id_mode", package_id_mode)
+            _assert_recipe_mode(ref, package_id)
+
+        for package_id_mode, ref, package_id in configs:
+            with environment_append({"CONAN_DEFAULT_PACKAGE_ID_MODE": package_id_mode}):
+                _assert_recipe_mode(ref, package_id)
diff --git a/conans/test/unittests/client/build/cmake_test.py b/conans/test/unittests/client/build/cmake_test.py
index 3df6a16cf..450d6df9c 100644
--- a/conans/test/unittests/client/build/cmake_test.py
+++ b/conans/test/unittests/client/build/cmake_test.py
@@ -691,10 +691,24 @@ class CMakeTest(unittest.TestCase):
               '-DCMAKE_EXPORT_NO_PACKAGE_REGISTRY="ON" -Wno-dev',
               "--config Debug")
 
+        settings.compiler.version = "15"
+        settings.arch = "armv8"
+        check('-G "Visual Studio 15 2017 ARM" -DCONAN_EXPORTED="1" -DCONAN_IN_LOCAL_CACHE="OFF" '
+              '-DCONAN_COMPILER="Visual Studio" -DCONAN_COMPILER_VERSION="15" '
+              '-DCMAKE_EXPORT_NO_PACKAGE_REGISTRY="ON" -Wno-dev',
+              "--config Debug")
+
+        settings.arch = "x86_64"
+        check('-G "Visual Studio 15 2017 Win64" -DCONAN_EXPORTED="1" -DCONAN_IN_LOCAL_CACHE="OFF" '
+              '-DCONAN_COMPILER="Visual Studio" -DCONAN_COMPILER_VERSION="15" '
+              '-DCMAKE_EXPORT_NO_PACKAGE_REGISTRY="ON" -Wno-dev',
+              "--config Debug")
+
         settings.compiler = "Visual Studio"
         settings.compiler.version = "9"
         settings.os = "WindowsCE"
         settings.os.platform = "Your platform name (ARMv4I)"
+        settings.os.version = "7.0"
         settings.build_type = "Debug"
         check('-G "Visual Studio 9 2008" '
               '-A "Your platform name (ARMv4I)" '
@@ -1340,3 +1354,13 @@ build_type: [ Release]
         self.assertIn("WARN: CMake generator could not be deduced from settings", conanfile.output)
         cmake.configure()
         cmake.build()
+
+    def test_cmake_system_version_windowsce(self):
+        settings = Settings.loads(default_settings_yml)
+        settings.os = "WindowsCE"
+        settings.os.version = "8.0"
+
+        conan_file = ConanFileMock()
+        conan_file.settings = settings
+        cmake = CMake(conan_file)
+        self.assertEqual(cmake.definitions["CMAKE_SYSTEM_VERSION"], "8.0")