Repository: conan-io/conan

Issue:
SystemPackageTool is missing an add_repository() method
Conan looks like it **almost** entirely handle all of our particular development environment setup, with one exception... it doesn't yet have a method for installing new repositories for system packages.

If `SystemPackageTool` had an `add_repository(repositories, repo_key=None, post_update=True)` method, then my hack-crap install script could be greatly improved. For example, with `AptTool` it would issue an `apt-add-repository <repo>` command for each repository in the list, if that repository was not already found in the system repository lists. Different styles of apt repositories could be supported, as in:
```python
if os_info.linux_distro == "ubuntu" and os_info.version < 16.04:
    installer.add_repository("ppa:jonathonf/gcc")
installer.install("g++-5")
installer.add_repository("deb http://opensource.wandisco.com/ubuntu trusty svn19", 
    repo_key="http://opensource.wandisco.com/wandisco-debian.gpg")
installer.install("subversion", "subversion-tools"])
```


Discussion:
> Conan looks like it almost entirely handle all of our particular development environment setup, with one exception... 

Lets try to do this then :). I think it shouldn't be difficult, but 1.7 is already packed, so will try for next 1.8.

Resolving patch:
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
@@ -1,4 +1,5 @@
 import os
+from six import string_types
 from conans.client.runner import ConanRunner
 from conans.client.tools.oss import OSInfo
 from conans.errors import ConanException
@@ -54,6 +55,11 @@ def _create_tool(os_info):
         else:
             return NullTool()
 
+    def add_repository(self, repository, repo_key=None, update=True):
+        self._tool.add_repository(repository, repo_key=repo_key)
+        if update:
+            self.update()
+
     def update(self):
         """
             Get the system package tool update command
@@ -115,6 +121,9 @@ def _install_any(self, packages):
 
 
 class NullTool(object):
+    def add_repository(self, repository, repo_key=None):
+        pass
+
     def update(self):
         pass
 
@@ -127,6 +136,11 @@ def installed(self, package_name):
 
 
 class AptTool(object):
+    def add_repository(self, repository, repo_key=None):
+        _run(self._runner, "%sapt-add-repository %s" % (self._sudo_str, repository))
+        if repo_key:
+            _run(self._runner, "wget -qO - %s | %sapt-key add -" % (repo_key, self._sudo_str))
+
     def update(self):
         _run(self._runner, "%sapt-get update" % self._sudo_str)
 
@@ -140,6 +154,9 @@ def installed(self, package_name):
 
 
 class YumTool(object):
+    def add_repository(self, repository, repo_key=None):
+        raise ConanException("YumTool::add_repository not implemented")
+
     def update(self):
         _run(self._runner, "%syum update" % self._sudo_str, accepted_returns=[0, 100])
 
@@ -152,6 +169,9 @@ def installed(self, package_name):
 
 
 class BrewTool(object):
+    def add_repository(self, repository, repo_key=None):
+        raise ConanException("BrewTool::add_repository not implemented")
+
     def update(self):
         _run(self._runner, "brew update")
 
@@ -164,6 +184,9 @@ def installed(self, package_name):
 
 
 class PkgTool(object):
+    def add_repository(self, repository, repo_key=None):
+        raise ConanException("PkgTool::add_repository not implemented")
+
     def update(self):
         _run(self._runner, "%spkg update" % self._sudo_str)
 
@@ -176,6 +199,9 @@ def installed(self, package_name):
 
 
 class PkgUtilTool(object):
+    def add_repository(self, repository, repo_key=None):
+        raise ConanException("PkgUtilTool::add_repository not implemented")
+
     def update(self):
         _run(self._runner, "%spkgutil --catalog" % self._sudo_str)
 
@@ -188,6 +214,9 @@ def installed(self, package_name):
 
 
 class ChocolateyTool(object):
+    def add_repository(self, repository, repo_key=None):
+        raise ConanException("ChocolateyTool::add_repository not implemented")
+
     def update(self):
         _run(self._runner, "choco outdated")
 
@@ -201,6 +230,9 @@ def installed(self, package_name):
 
 
 class PacManTool(object):
+    def add_repository(self, repository, repo_key=None):
+        raise ConanException("PacManTool::add_repository not implemented")
+
     def update(self):
         _run(self._runner, "%spacman -Syyu --noconfirm" % self._sudo_str)
 
@@ -213,6 +245,9 @@ def installed(self, package_name):
 
 
 class ZypperTool(object):
+    def add_repository(self, repository, repo_key=None):
+        raise ConanException("ZypperTool::add_repository not implemented")
+
     def update(self):
         _run(self._runner, "%szypper --non-interactive ref" % self._sudo_str)
 


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
@@ -59,6 +59,60 @@ def verify_update_test(self):
             self.assertIn('Not updating system_requirements. CONAN_SYSREQUIRES_MODE=verify',
                           tools.system_pm._global_output)
 
+    def add_repositories_exception_cases_test(self):
+        os_info = OSInfo()
+        os_info.is_macos = False
+        os_info.is_linux = True
+        os_info.is_windows = False
+        os_info.linux_distro = "fedora"  # Will instantiate YumTool
+
+        with self.assertRaisesRegexp(ConanException, "add_repository not implemented"):
+            spt = SystemPackageTool(os_info=os_info)
+            spt.add_repository(repository="deb http://repo/url/ saucy universe multiverse",
+                               repo_key=None)
+
+    def add_repository_test(self):
+        class RunnerOrderedMock:
+            commands = []  # Command + return value
+
+            def __call__(runner_self, command, output, win_bash=False, subsystem=None):
+                if not len(runner_self.commands):
+                    self.fail("Commands list exhausted, but runner called with '%s'" % command)
+                expected, ret = runner_self.commands.pop(0)
+                self.assertEqual(expected, command)
+                return ret
+
+        def _run_add_repository_test(repository, gpg_key, sudo, update):
+            sudo_cmd = "sudo " if sudo else ""
+            runner = RunnerOrderedMock()
+            runner.commands.append(("{}apt-add-repository {}".format(sudo_cmd, repository), 0))
+            if gpg_key:
+                runner.commands.append(
+                    ("wget -qO - {} | {}apt-key add -".format(gpg_key, sudo_cmd), 0))
+            if update:
+                runner.commands.append(("{}apt-get update".format(sudo_cmd), 0))
+
+            with tools.environment_append({"CONAN_SYSREQUIRES_SUDO": str(sudo)}):
+                os_info = OSInfo()
+                os_info.is_macos = False
+                os_info.is_linux = True
+                os_info.is_windows = False
+                os_info.linux_distro = "debian"
+                spt = SystemPackageTool(runner=runner, os_info=os_info)
+
+                spt.add_repository(repository=repository, repo_key=gpg_key, update=update)
+                self.assertEqual(len(runner.commands), 0)
+
+        # Run several test cases
+        repository = "deb http://repo/url/ saucy universe multiverse"
+        gpg_key = 'http://one/key.gpg'
+        _run_add_repository_test(repository, gpg_key, sudo=True, update=True)
+        _run_add_repository_test(repository, gpg_key, sudo=True, update=False)
+        _run_add_repository_test(repository, gpg_key, sudo=False, update=True)
+        _run_add_repository_test(repository, gpg_key, sudo=False, update=False)
+        _run_add_repository_test(repository, gpg_key=None, sudo=True, update=True)
+        _run_add_repository_test(repository, gpg_key=None, sudo=False, update=False)
+
     def system_package_tool_test(self):
 
         with tools.environment_append({"CONAN_SYSREQUIRES_SUDO": "True"}):