Repository: conan-io/conan-package-tools (python)

Issue:
Support modern package_manager mode
### Description of Problem, Request, or Question

So far, [CONAN_SYSREQUIRES_MODE](https://docs.conan.io/en/latest/reference/env_vars.html#conan-sysrequires-mode) is the environment variable which rules the legacy [SystemPackageTool](https://docs.conan.io/en/latest/reference/conanfile/methods.html#systempackagetool), however, with the migration of recipe and modernization of package manager support, the [conan.tools.system.package_manager](https://docs.conan.io/en/latest/reference/conanfile/tools/system/package_manager.html#conan-tools-system-package-manager) uses [config](https://docs.conan.io/en/latest/reference/conanfile/tools/system/package_manager.html#conan-tools-system-package-manager-config) to enable installing by default.

As current workaround, need to [run script before to build on Docker](https://github.com/conan-io/conan-package-tools#running-scripts-and-executing-commands-before-to-build-on-docker) to enable it, updating the global config file.

The feature consists updating it automatically, by adding a new cpt option, which will update both legacy and modern version. For instance:

```
CPT_SYSREQUIRES_MODE=[install, check, disable]
```

* `install`: equivalent to `CONAN_SYSREQUIRES_MODE=enabled` + `tools.system.package_manager:mode=install`
* `check`: equivalent to `CONAN_SYSREQUIRES_MODE=verify` + `tools.system.package_manager:mode=check`
* `disabled`: equivalent to `CONAN_SYSREQUIRES_MODE=disabled` + `tools.system.package_manager:mode=None` (It should accept `False` and `None` too)

```
CONAN_SYSREQUIRES_SUDO=[true,false]
```

* `true`, `1`, `yes`: Uses `sudo` command to execute the system package manager tool
* `false`, `0`, `no`: Do not use `sudo` command to execute the system package manager tool

```
CONAN_SYSREQUIRES_TOOL=[apt-get, yum, dnf, brew, pacman, choco, zypper, pkg, pkgutil]
```

It follows those supported tools listed on [modern system package](https://docs.conan.io/en/latest/reference/conanfile/tools/system/package_manager.html#configuration-properties-that-affect-how-system-package-managers-are-invoked)

Pull request description:
Add support global.conf
Changelog: Feature: Support global.conf

closes #607

- [ ] Refer to the issue that supports this Pull Request.
- [ ] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [ ] I've read the [Contributing guide](https://github.com/conan-io/conan-package-tools/blob/develop/.github/CONTRIBUTING.md).
- [ ] I've followed the [PEP8](https://www.python.org/dev/peps/pep-0008) style guides for Python code.


Patch:
diff --git a/README.md b/README.md
index 9a32de0..c491a3e 100644
--- a/README.md
+++ b/README.md
@@ -1205,6 +1205,7 @@ Check [Conan Build policies](https://docs.conan.io/en/latest/mastering/policies.
 - **force_selinux**: Force docker to relabel file objects on the shared volumes
 - **skip_recipe_export**: If True, the package recipe will only be exported on the first build. Default [False]
 - **update_dependencies**: Update all dependencies before building e.g conan create -u
+- **global_conf**: A list with values to be added to `global.conf` file
 
 Upload related parameters:
 
@@ -1372,6 +1373,7 @@ Check [Conan Build policies](https://docs.conan.io/en/latest/mastering/policies.
 - **CONAN_SKIP_RECIPE_EXPORT**: If defined, the package recipe will only be exported on the first build.
 - **CPT_UPDATE_DEPENDENCIES**: Update all dependencies before building e.g conan create -u
 - **CONAN_PURE_C**: Set `pure_c` by environment variable, default `True`
+- **CONAN_GLOBAL_CONF**: Add `global.conf` file with listed values e.g '*:tools.cmake.cmaketoolchain:generator=Ninja,tools.system.package_manager:mode=install'
 
 
 # Full example
diff --git a/cpt/config.py b/cpt/config.py
index 4be1ec5..e19d664 100644
--- a/cpt/config.py
+++ b/cpt/config.py
@@ -1,3 +1,7 @@
+import os.path
+from conans import tools
+from conans.model.conf import ConfDefinition
+
 
 class ConfigManager(object):
 
@@ -11,3 +15,23 @@ class ConfigManager(object):
             message += " with args \"%s\"" % args
         self.printer.print_message(message)
         self._conan_api.config_install(url, verify_ssl=True, args=args)
+
+
+class GlobalConf(object):
+    def __init__(self, conan_api, printer):
+        self._conan_api = conan_api
+        self.printer = printer
+
+    def populate(self, values):
+        global_conf = self._conan_api.app.cache.new_config_path
+        if isinstance(values, str):
+            values = values.split(",")
+        config = ConfDefinition()
+        if os.path.exists(global_conf):
+            content = tools.load(global_conf)
+            config.loads(content)
+        for value in values:
+            key = value[:value.find('=')]
+            k_value = value[value.find('=') + 1:]
+            config.update(key, k_value)
+        tools.save(global_conf, config.dumps())
diff --git a/cpt/packager.py b/cpt/packager.py
index d3ef608..bc79c2e 100644
--- a/cpt/packager.py
+++ b/cpt/packager.py
@@ -147,7 +147,8 @@ class ConanMultiPackager(object):
                  force_selinux=None,
                  skip_recipe_export=False,
                  update_dependencies=None,
-                 lockfile=None):
+                 lockfile=None,
+                 global_conf=None):
 
         conan_version = get_client_version()
 
@@ -338,6 +339,8 @@ class ConanMultiPackager(object):
 
         self.builds_in_current_page = []
 
+        self.global_conf = global_conf or os.getenv("CONAN_GLOBAL_CONF")
+
         self.test_folder = test_folder or os.getenv("CPT_TEST_FOLDER")
 
         self.config_url = config_url or os.getenv("CONAN_CONFIG_URL")
@@ -705,6 +708,7 @@ class ConanMultiPackager(object):
                                  skip_recipe_export=skip_recipe_export,
                                  update_dependencies=self.update_dependencies,
                                  profile_build_abs_path=profile_build_abs_path,
+                                 global_conf=self.global_conf,
                                  )
                 r.run()
                 self._packages_summary.append({"configuration":  build, "package" : r.results})
@@ -747,6 +751,7 @@ class ConanMultiPackager(object):
                                        update_dependencies=self.update_dependencies,
                                        profile_build_text=profile_build_text,
                                        base_profile_build_text=base_profile_build_text,
+                                       global_conf=self.global_conf,
                                        cwd=self.cwd)
 
                 r.run(pull_image=not pulled_docker_images[docker_image],
diff --git a/cpt/run_in_docker.py b/cpt/run_in_docker.py
index 2bbfa3e..a569d83 100644
--- a/cpt/run_in_docker.py
+++ b/cpt/run_in_docker.py
@@ -37,6 +37,7 @@ def run():
     build_policy = unscape_env(os.getenv("CPT_BUILD_POLICY"))
     require_overrides = unscape_env(os.getenv("CPT_REQUIRE_OVERRIDES"))
     test_folder = unscape_env(os.getenv("CPT_TEST_FOLDER"))
+    global_conf = unscape_env(os.getenv("CPT_GLOBAL_CONF"))
     reference = ConanFileReference.loads(os.getenv("CONAN_REFERENCE"))
 
     profile_text = unscape_env(os.getenv("CPT_PROFILE"))
@@ -68,7 +69,8 @@ def run():
                           skip_recipe_export=skip_recipe_export,
                           update_dependencies=update_dependencies,
                           lockfile=lockfile,
-                          profile_build_abs_path=abs_profile_build_path)
+                          profile_build_abs_path=abs_profile_build_path,
+                          global_conf=global_conf)
     runner.run()
 
 
diff --git a/cpt/runner.py b/cpt/runner.py
index 163e23e..eefb487 100644
--- a/cpt/runner.py
+++ b/cpt/runner.py
@@ -10,7 +10,7 @@ from conans.model.version import Version
 from conans.model.ref import ConanFileReference
 
 from cpt import __version__ as package_tools_version, get_client_version
-from cpt.config import ConfigManager
+from cpt.config import ConfigManager, GlobalConf
 from cpt.printer import Printer
 from cpt.profiles import load_profile, patch_default_base_profile
 from conans.client.conan_api import ProfileData
@@ -23,7 +23,7 @@ class CreateRunner(object):
                  cwd=None, printer=None, upload=False, upload_only_recipe=None,
                  test_folder=None, config_url=None, config_args=None,
                  upload_dependencies=None, conanfile=None, skip_recipe_export=False,
-                 update_dependencies=False, lockfile=None, profile_build_abs_path=None):
+                 update_dependencies=False, lockfile=None, profile_build_abs_path=None, global_conf=None):
 
         self.printer = printer or Printer()
         self._cwd = cwd or os.getcwd()
@@ -54,6 +54,7 @@ class CreateRunner(object):
         self._update_dependencies = update_dependencies
         self._results = None
         self._profile_build_abs_path = profile_build_abs_path
+        self._global_conf = global_conf
 
         patch_default_base_profile(conan_api, profile_abs_path)
         client_version = get_client_version()
@@ -86,6 +87,10 @@ class CreateRunner(object):
         if self._config_url:
             ConfigManager(self._conan_api, self.printer).install(url=self._config_url, args=self._config_args)
 
+        if self._global_conf:
+            global_conf = GlobalConf(self._conan_api, self.printer)
+            global_conf.populate(self._global_conf)
+
         context = tools.no_op()
         compiler = self.settings.get("compiler", None)
         if not self._exclude_vcvars_precommand:
@@ -213,7 +218,8 @@ class DockerCreateRunner(object):
                  lockfile=None,
                  profile_build_text=None,
                  base_profile_build_text=None,
-                 cwd=None):
+                 cwd=None,
+                 global_conf=None):
 
         self.printer = printer or Printer()
         self._upload = upload
@@ -253,6 +259,7 @@ class DockerCreateRunner(object):
         self._profile_build_text = profile_build_text
         self._base_profile_build_text = base_profile_build_text
         self._cwd = cwd or os.getcwd()
+        self._global_conf = global_conf
 
     def _pip_update_conan_command(self):
         commands = []
@@ -375,6 +382,7 @@ class DockerCreateRunner(object):
         ret["CPT_BASE_PROFILE"] = escape_env(self._base_profile_text)
         ret["CPT_BASE_PROFILE_NAME"] = escape_env(self._base_profile_name)
         ret["CPT_PROFILE_BUILD"] = escape_env(self._profile_build_text)
+        ret["CPT_GLOBAL_CONF"] = escape_env(self._global_conf)
 
         ret["CONAN_USERNAME"] = escape_env(self._reference.user or ret.get("CONAN_USERNAME"))
         ret["CONAN_TEMP_TEST_FOLDER"] = "1"  # test package folder to a temp one


Test patch:
diff --git a/cpt/test/test_client/global_conf_test.py b/cpt/test/test_client/global_conf_test.py
new file mode 100644
index 0000000..7b4c13a
--- /dev/null
+++ b/cpt/test/test_client/global_conf_test.py
@@ -0,0 +1,29 @@
+import unittest
+import textwrap
+
+from conans.client.tools import environment_append, load
+from cpt.test.utils.tools import TestClient
+
+from cpt.test.test_client.tools import get_patched_multipackager
+
+
+class GlobalConfTest(unittest.TestCase):
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        class Pkg(ConanFile):
+            pass
+        """)
+
+    def test_environment_variable(self):
+        tc = TestClient(users={"default": [("user", "password")]})
+        tc.save({"conanfile.py": self.conanfile})
+        global_conf = ["tools.system.package_manager:mode=install", "tools.system.package_manager:sudo=True"]
+
+        with environment_append({"CONAN_GLOBAL_CONF": ",".join(global_conf)}):
+            mulitpackager = get_patched_multipackager(tc, exclude_vcvars_precommand=True)
+            mulitpackager.add_common_builds(reference="lib/1.0@user/stable", shared_option_name=False)
+            mulitpackager.run()
+            assert textwrap.dedent("""tools.system.package_manager:mode=install
+            tools.system.package_manager:sudo=True
+            """.replace(" ", "")) == load(tc.cache.new_config_path)
diff --git a/cpt/test/unit/global_conf_test.py b/cpt/test/unit/global_conf_test.py
new file mode 100644
index 0000000..0ccf722
--- /dev/null
+++ b/cpt/test/unit/global_conf_test.py
@@ -0,0 +1,41 @@
+import unittest
+import textwrap
+from cpt.config import GlobalConf
+from cpt.printer import Printer
+from cpt.test.unit.packager_test import MockConanAPI
+from conans import tools
+
+
+class GlobalConfUnitTest(unittest.TestCase):
+
+    def setUp(self):
+        self.conan_api = MockConanAPI()
+        self.configuration = ["tools.system.package_manager:mode=install", "tools.system.package_manager:sudo=True"]
+
+    def test_new_global_conf(self):
+        manager = GlobalConf(self.conan_api, Printer())
+        manager.populate(self.configuration)
+        content = tools.load(self.conan_api._cache.new_config_path)
+        assert content == textwrap.dedent("""tools.system.package_manager:mode=install
+                                             tools.system.package_manager:sudo=True
+                                          """.replace(" ", ""))
+
+    def test_append_global_conf(self):
+        manager = GlobalConf(self.conan_api, Printer())
+        manager.populate(self.configuration)
+        append_conf = ["tools.system.package_manager:tool=yum"]
+        manager.populate(append_conf)
+        content = tools.load(self.conan_api._cache.new_config_path)
+        assert content == textwrap.dedent("""tools.system.package_manager:mode=install
+                                             tools.system.package_manager:sudo=True
+                                             tools.system.package_manager:tool=yum
+                                          """.replace(" ", ""))
+
+    def test_string_global_conf(self):
+        configuration = "tools.system.package_manager:mode=install,tools.system.package_manager:sudo=True"
+        manager = GlobalConf(self.conan_api, Printer())
+        manager.populate(configuration)
+        content = tools.load(self.conan_api._cache.new_config_path)
+        assert content == textwrap.dedent("""tools.system.package_manager:mode=install
+                                             tools.system.package_manager:sudo=True
+                                          """.replace(" ", ""))
diff --git a/cpt/test/unit/utils.py b/cpt/test/unit/utils.py
index 2de4131..2de7209 100644
--- a/cpt/test/unit/utils.py
+++ b/cpt/test/unit/utils.py
@@ -29,8 +29,11 @@ class MockConanCache(object):
 
     def __init__(self, *args, **kwargs):
         _base_dir = temp_folder()
-        self.default_profile_path = os.path.join(_base_dir, "default")
-        self.profiles_path = _base_dir
+        self.default_profile_path = os.path.join(_base_dir, "profiles", "default")
+        self.profiles_path = os.path.join(_base_dir, "profiles")
+        self.new_config_path = os.path.join(_base_dir, "global.conf")
+        if not os.path.exists(self.profiles_path):
+            os.mkdir(self.profiles_path)
 
 Action = namedtuple("Action", "name args kwargs")