Repository: conan-io/conan

Issue:
Make installation of system packages in system_requirements() optional
Feature request: It would be nice if it would be possible to disable installation of system packages in `system_requirements()`. An environment variable, e.g. `CONAN_DISABLE_SYSREQUIRES`, would be handy.

In general this would be helpful when testing packages in environments without sudo-rights where required packages are preinstalled.


Discussion:
But current ``SystemPackageTool`` already does a check if the packages are installed, and that does not require sudo permissions. Am I missing something? Thanks!
The problem is that if you run this in a CI environment and don't have the package installed by accident, sudo gets executed which will hang. 

Now, one can disable sudo which makes then the package fail as expected using CONAN_SYSREQUIRES_SUDO, but it will still try to install it which is odd. It would be better if the behavior could be specified in a bit more detailed way: 
CONAN_SYSREQUIRES_CHECK="enabled;check_only;disable"

Enabled would be the default and behave like it does now. Check_only would raise a failure if a package is not installed but not attempt to install it (CI). Disable is what the author of the issue proposes, completely disable this as the package could have been hand installed in /usr/local for example. May be make this an option to conan install? conan install --sysrequires=check_only? (poor choice of naming, but I think you get the point)
Yes, yes, I am not opposed to such configuration, I am just trying to understand what are the scenarios, just in case.

I agree with the modes, they seem ok. Regarding the command line argument, I'd say it is something more related to the configuration, the general operating mode for that machine. And the command line is already cluttered with many arguments, so my vote would go for making it a configuration (conan.conf) and environment variable.
I really like the three proposed modes and the implementation as environment variables!!
my colleges also complain that conan recipes install packages to the system, which they want to avoid -they want to control themselves that are installed on their systems, and found it risky if conan recipes downloaded from the internet will start to install stuff into the system without user's permission.
although sudo requires password to enter, there is a chance that sudo was run earlier and in that case conan will start to install stuff without requesting password.
so they prefer if conan just fail and report which packages are required, then carefully install needed packages themselves.

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
@@ -83,6 +83,7 @@
 default_profile = %s
 compression_level = 9                 # environment CONAN_COMPRESSION_LEVEL
 sysrequires_sudo = True               # environment CONAN_SYSREQUIRES_SUDO
+# 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
 # bash_path = ""                      # environment CONAN_BASH_PATH (only windows)
@@ -150,6 +151,7 @@ def env_vars(self):
                "CONAN_PYLINTRC": self._env_c("general.pylintrc", "CONAN_PYLINTRC", None),
                "CONAN_PYLINT_WERR": self._env_c("general.pylint_werr", "CONAN_PYLINT_WERR", None),
                "CONAN_SYSREQUIRES_SUDO": self._env_c("general.sysrequires_sudo", "CONAN_SYSREQUIRES_SUDO", "False"),
+               "CONAN_SYSREQUIRES_MODE": self._env_c("general.sysrequires_mode", "CONAN_SYSREQUIRES_MODE", "enabled"),
                "CONAN_VS_INSTALLATION_PREFERENCE": self._env_c("general.vs_installation_preference", "CONAN_VS_INSTALLATION_PREFERENCE", None),
                "CONAN_RECIPE_LINTER": self._env_c("general.recipe_linter", "CONAN_RECIPE_LINTER", "True"),
                "CONAN_CPU_COUNT": self._env_c("general.cpu_count", "CONAN_CPU_COUNT", None),
diff --git a/conans/client/tools/system_pm.py b/conans/client/tools/system_pm.py
--- a/conans/client/tools/system_pm.py
+++ b/conans/client/tools/system_pm.py
@@ -2,6 +2,7 @@
 from conans.client.runner import ConanRunner
 from conans.client.tools.oss import OSInfo
 from conans.errors import ConanException
+from conans.util.env_reader import get_env
 
 _global_output = None
 
@@ -9,19 +10,31 @@
 class SystemPackageTool(object):
 
     def __init__(self, runner=None, os_info=None, tool=None, recommends=False):
-        env_sudo = os.environ.get("CONAN_SYSREQUIRES_SUDO", None)
-        self._sudo = (env_sudo != "False" and env_sudo != "0")
-        if env_sudo is None and os.name == 'posix' and os.geteuid() == 0:
-            self._sudo = False
-        if env_sudo is None and os.name == 'nt':
-            self._sudo = False
         os_info = os_info or OSInfo()
         self._is_up_to_date = False
         self._tool = tool or self._create_tool(os_info)
-        self._tool._sudo_str = "sudo " if self._sudo else ""
+        self._tool._sudo_str = "sudo " if self._is_sudo_enabled() else ""
         self._tool._runner = runner or ConanRunner()
         self._tool._recommends = recommends
 
+    @staticmethod
+    def _is_sudo_enabled():
+        if "CONAN_SYSREQUIRES_SUDO" not in os.environ:
+            if os.name == 'posix' and os.geteuid() == 0:
+                return False
+            if os.name == 'nt':
+                return False
+        return get_env("CONAN_SYSREQUIRES_SUDO", True)
+
+    @staticmethod
+    def _get_sysrequire_mode():
+        allowed_modes= ("enabled", "verify", "disabled")
+        mode = get_env("CONAN_SYSREQUIRES_MODE", "enabled")
+        mode_lower = mode.lower()
+        if mode_lower not in allowed_modes:
+            raise ConanException("CONAN_SYSREQUIRES_MODE=%s is not allowed, allowed modes=%r" % (mode, allowed_modes))
+        return mode_lower
+
     @staticmethod
     def _create_tool(os_info):
         if os_info.with_apt:
@@ -53,13 +66,33 @@ def install(self, packages, update=True, force=False):
             Get the system package tool install command.
         '"""
         packages = [packages] if isinstance(packages, str) else list(packages)
+
+        mode = self._get_sysrequire_mode()
+
+        if mode in ("verify", "disabled"):
+            # Report to output packages need to be installed
+            if mode == "disabled":
+                _global_output.info("The following packages need to be installed:\n %s" % "\n".join(packages))
+                return
+
+            if mode == "verify" and not self._installed(packages):
+                _global_output.error("The following packages need to be installed:\n %s" % "\n".join(packages))
+                raise ConanException(
+                    "Aborted due to CONAN_SYSREQUIRES_MODE=%s. Some system packages need to be installed" % mode
+                )
+
         if not force and self._installed(packages):
             return
+
+        # From here system packages can be updated/modified
         if update and not self._is_up_to_date:
             self.update()
         self._install_any(packages)
 
     def _installed(self, packages):
+        if not packages:
+            return True
+
         for pkg in packages:
             if self._tool.installed(pkg):
                 _global_output.info("Package already installed: %s" % pkg)


Test patch:
diff --git a/conans/test/util/tools_test.py b/conans/test/util/tools_test.py
--- a/conans/test/util/tools_test.py
+++ b/conans/test/util/tools_test.py
@@ -414,6 +414,75 @@ def __call__(self, command, output):  # @UnusedVariable
                 spt.install(packages)
             self.assertEquals(7, runner.calls)
 
+    def system_package_tool_mode_test(self):
+        """
+        System Package Tool mode is defined by CONAN_SYSREQUIRES_MODE env variable.
+        Allowed values: (enabled, verify, disabled). Parser accepts it in lower/upper case or any combination.
+        """
+
+        class RunnerMultipleMock(object):
+            def __init__(self, expected=None):
+                self.calls = 0
+                self.expected = expected
+
+            def __call__(self, command, *args, **kwargs):
+                self.calls += 1
+                return 0 if command in self.expected else 1
+
+        packages = ["a_package", "another_package", "yet_another_package"]
+
+        # Check invalid mode raises ConanException
+        with tools.environment_append({
+            "CONAN_SYSREQUIRES_MODE": "test_not_valid_mode",
+            "CONAN_SYSREQUIRES_SUDO": "True"
+        }):
+            runner = RunnerMultipleMock([])
+            spt = SystemPackageTool(runner=runner, tool=AptTool())
+            with self.assertRaises(ConanException) as exc:
+                spt.install(packages)
+            self.assertIn("CONAN_SYSREQUIRES_MODE=test_not_valid_mode is not allowed", str(exc.exception))
+            self.assertEquals(0, runner.calls)
+
+        # Check verify mode, a package report should be displayed in output and ConanException raised.
+        # No system packages are installed
+        with tools.environment_append({
+            "CONAN_SYSREQUIRES_MODE": "VeRiFy",
+            "CONAN_SYSREQUIRES_SUDO": "True"
+        }):
+            packages = ["verify_package", "verify_another_package", "verify_yet_another_package"]
+            runner = RunnerMultipleMock(["sudo apt-get update"])
+            spt = SystemPackageTool(runner=runner, tool=AptTool())
+            with self.assertRaises(ConanException) as exc:
+                spt.install(packages)
+            self.assertIn("Aborted due to CONAN_SYSREQUIRES_MODE=", str(exc.exception))
+            self.assertIn('\n'.join(packages), tools.system_pm._global_output)
+            self.assertEquals(3, runner.calls)
+
+        # Check disabled mode, a package report should be displayed in output.
+        # No system packages are installed
+        with tools.environment_append({
+            "CONAN_SYSREQUIRES_MODE": "DiSaBlEd",
+            "CONAN_SYSREQUIRES_SUDO": "True"
+        }):
+            packages = ["disabled_package", "disabled_another_package", "disabled_yet_another_package"]
+            runner = RunnerMultipleMock(["sudo apt-get update"])
+            spt = SystemPackageTool(runner=runner, tool=AptTool())
+            spt.install(packages)
+            self.assertIn('\n'.join(packages), tools.system_pm._global_output)
+            self.assertEquals(0, runner.calls)
+
+        # Check enabled, default mode, system packages must be installed.
+        with tools.environment_append({
+            "CONAN_SYSREQUIRES_MODE": "EnAbLeD",
+            "CONAN_SYSREQUIRES_SUDO": "True"
+        }):
+            runner = RunnerMultipleMock(["sudo apt-get update"])
+            spt = SystemPackageTool(runner=runner, tool=AptTool())
+            with self.assertRaises(ConanException) as exc:
+                spt.install(packages)
+            self.assertNotIn("CONAN_SYSREQUIRES_MODE", str(exc.exception))
+            self.assertEquals(7, runner.calls)
+
     def system_package_tool_installed_test(self):
         if platform.system() != "Linux" and platform.system() != "Macos" and platform.system() != "Windows":
             return