Repository: conan-io/conan

Issue:
[feature request] SystemPackageTool platform support
To help us debug your issue please explain:

As supported by [MSBuild](https://docs.conan.io/en/latest/reference/build_helpers/visual_studio.html#build), I would like to add the same platforms parameter for `SystemPackageTool`, where it will solve the package platform to be installed.

```python
def system_requirements(self):
        if tools.os_info.with_apt:
            packages = [
                "libgtk-3-dev",
                "libatk1.0-dev",
                "libglib2.0-dev",
                "libpango1.0-dev",
                "libgdk-pixbuf2.0-dev",
                "libcairo2-dev",
                "libpango1.0-dev",
                "libcairo2-dev"
            ]
            if tools.cross_building(self.settings):
                target_arch = {"x86": "i386", "armv7": "arm", "armv7hf": "armhf"}
                conan_arch = str(self.settings.arch)
                packages = ["%s:%s" % (package, target_arch[conan_arch]) for package in packages]
            installer = SystemPackageTool()
            installer.install(" ".join(packages))
```

As you can see, I need to append the package name to solve the target platform. I think it could be solve automatically:

```python
def system_requirements(self):
        if tools.os_info.with_apt:
            packages = [
                "libgtk-3-dev",
                "libatk1.0-dev",
                "libglib2.0-dev",
                "libpango1.0-dev",
                "libgdk-pixbuf2.0-dev",
                "libcairo2-dev",
                "libpango1.0-dev",
                "libcairo2-dev"
            ]
            distro_archs = None
            if tools.cross_building(self.settings):
                distro_archs = {"x86": "i386", "armv7": "arm", "armv7hf": "armhf"}
            installer = SystemPackageTool()
            installer.install(" ".join(packages), platforms=distro_archs)
```

Regards!

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




Discussion:
You would need to pass to the `installer.install`:
 * the target platform, or
 * the `distro_archs` dictionary and the arch, right?

Or, does it make sense to have the `distro_archs` inside the `SystemPackageTool` and pass only the `arch`? Of course, that `distro_archs` should be an optional argument to override it.

Does it work for all the system packager tools (appending the arch to the package name like `package:arch`)? Does it work for _all_ the versions?

...you know that system packages are something we don't feel comfortable with, that's the only reason for so many questions 😸 

I know, `SystemPackageTools` is evil, but is what we have for now ...

> does it make sense to have the distro_archs inside the SystemPackageTool and pass only the arch?

I think yes, since is the same pattern for all packages that we have used with `SystemPackageTools`.

> Of course, that distro_archs should be an optional argument to override it.

Right! If not specified, the behavior should be the same as now.

> Does it work for all the system packager tools (appending the arch to the package name like package:arch)? 

No ... APT uses the suffix **:<arch>**, YUM uses **.<arch>**, pacman uses the prefix **lib-<arch>**. No idea about Chocolatey

> Does it work for all the versions?

Yes, they are compatible.

I think your question *Does it work for all the system packager tools* make me think again.

We can implement this feature, but we will need to check which package manager we are using. This is not different from what we have done for our packages, most of the time we just copy that pattern from one recipe to another.

So the `SystemPackageTool` wrapper should select the `arch` and then delegate on each one (apt, yum, pacman,...) the composition of the full name for the package (suffix with `:`, `.`, prefix `lib-`,...). Given this neccessity, it makes sense to build the name inside, as we cannot know from the outside which one will be the package manager we are using (ok, we have the `OSInfo.with/is_XXX functions` but it is an implementation detail).

Probably the cleanest implementation would be to pass the `settings` object to the `SystemPackageTools` and implement all the behavior inside (even the _cross compiling_ detection), from outside we should be able to provide the dictionary `arch->platform` for those cases where the hardcoded one is not valid.

> Probably the cleanest implementation would be to pass the settings object to the SystemPackageTools and implement all the behavior inside 

Yes, it sounds better, since we can detect cross-building situation.

>  from outside we should be able to provide the dictionary arch->platform for those cases where the hardcoded one is not valid.

Right, as we have done for MSBuild helper.

Do you think this feature makes sense?

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
@@ -2,7 +2,7 @@
 import sys
 
 from conans.client.runner import ConanRunner
-from conans.client.tools.oss import OSInfo
+from conans.client.tools.oss import OSInfo, cross_building, get_cross_building_settings
 from conans.client.tools.files import which
 from conans.errors import ConanException
 from conans.util.env_reader import get_env
@@ -11,8 +11,8 @@
 
 class SystemPackageTool(object):
 
-    def __init__(self, runner=None, os_info=None, tool=None, recommends=False, output=None):
-
+    def __init__(self, runner=None, os_info=None, tool=None, recommends=False, output=None, conanfile=None):
+        output = output if output else conanfile.output if conanfile else None
         self._output = default_output(output, 'conans.client.tools.system_pm.SystemPackageTool')
         os_info = os_info or OSInfo()
         self._is_up_to_date = False
@@ -20,6 +20,7 @@ def __init__(self, runner=None, os_info=None, tool=None, recommends=False, outpu
         self._tool._sudo_str = self._get_sudo_str()
         self._tool._runner = runner or ConanRunner(output=self._output)
         self._tool._recommends = recommends
+        self._conanfile = conanfile
 
     @staticmethod
     def _get_sudo_str():
@@ -86,11 +87,17 @@ def update(self):
         self._is_up_to_date = True
         self._tool.update()
 
-    def install(self, packages, update=True, force=False):
+    def install(self, packages, update=True, force=False, arch_names=None):
+        """ Get the system package tool install command.
+
+        :param packages: String with all package to be installed e.g. "libusb-dev libfoobar-dev"
+        :param update: Run update command before to install
+        :param force: Force installing all packages
+        :param arch_names: Package suffix/prefix name used by installer tool e.g. {"x86_64": "amd64"}
+        :return: None
         """
-            Get the system package tool install command.
-        '"""
         packages = [packages] if isinstance(packages, str) else list(packages)
+        packages = self._get_package_names(packages, arch_names)
 
         mode = self._get_sysrequire_mode()
 
@@ -114,6 +121,24 @@ def install(self, packages, update=True, force=False):
             self.update()
         self._install_any(packages)
 
+    def _get_package_names(self, packages, arch_names):
+        """ Parse package names according it architecture
+
+        :param packages: list with all package to be installed e.g. ["libusb-dev libfoobar-dev"]
+        :param arch_names: Package suffix/prefix name used by installer tool
+        :return: list with all parsed names e.g. ["libusb-dev:armhf libfoobar-dev:armhf"]
+        """
+        if self._conanfile and self._conanfile.settings and cross_building(self._conanfile.settings):
+            _, build_arch, _, host_arch = get_cross_building_settings(self._conanfile.settings)
+            arch = host_arch or build_arch
+            parsed_packages = []
+            for package in packages:
+                for package_name in package.split(" "):
+                    parsed_packages.append(self._tool.get_package_name(package_name, arch,
+                                                                       arch_names))
+            return parsed_packages
+        return packages
+
     def _installed(self, packages):
         if not packages:
             return True
@@ -139,6 +164,16 @@ class BaseTool(object):
     def __init__(self, output=None):
         self._output = default_output(output, 'conans.client.tools.system_pm.BaseTool')
 
+    def get_package_name(self, package, arch, arch_names):
+        """ Retrieve package name to installed according the target arch.
+
+        :param package: Regular package name e.g libusb-dev
+        :param arch: Host arch from Conanfile.settings
+        :param arch_names: Dictionary with suffix/prefix names e.g {"x86_64": "amd64"}
+        :return: Package name for Tool e.g. libusb-dev:i386
+        """
+        return package
+
 
 class NullTool(BaseTool):
     def add_repository(self, repository, repo_key=None):
@@ -175,6 +210,20 @@ def installed(self, package_name):
         exit_code = self._runner("dpkg-query -W -f='${Status}' %s | grep -q \"ok installed\"" % package_name, None)
         return exit_code == 0
 
+    def get_package_name(self, package, arch, arch_names):
+        if arch_names is None:
+            arch_names = {"x86_64": "amd64",
+                         "x86": "i386",
+                         "ppc32": "powerpc",
+                         "ppc64le": "ppc64el",
+                         "armv7": "arm",
+                         "armv7hf": "armhf",
+                         "armv8": "arm64",
+                         "s390x": "s390x"}
+        if arch in arch_names:
+            return "%s:%s" % (package, arch_names[arch])
+        return package
+
 
 class YumTool(BaseTool):
     def add_repository(self, repository, repo_key=None):
@@ -192,6 +241,20 @@ def installed(self, package_name):
         exit_code = self._runner("rpm -q %s" % package_name, None)
         return exit_code == 0
 
+    def get_package_name(self, package, arch, arch_names):
+        if arch_names is None:
+            arch_names = {"x86_64": "x86_64",
+                         "x86": "i?86",
+                         "ppc32": "powerpc",
+                         "ppc64le": "ppc64le",
+                         "armv7": "armv7",
+                         "armv7hf": "armv7hl",
+                         "armv8": "aarch64",
+                         "s390x": "s390x"}
+        if arch in arch_names:
+            return "%s.%s" % (package, arch_names[arch])
+        return package
+
 
 class BrewTool(BaseTool):
     def add_repository(self, repository, repo_key=None):
@@ -271,6 +334,13 @@ def installed(self, package_name):
         exit_code = self._runner("pacman -Qi %s" % package_name, None)
         return exit_code == 0
 
+    def get_package_name(self, package, arch, arch_names):
+        if arch_names is None:
+            arch_names = {"x86": "lib32"}
+        if arch in arch_names:
+            return "%s-%s" % (arch_names[arch], package)
+        return package
+
 
 class ZypperTool(BaseTool):
     def add_repository(self, repository, repo_key=None):
@@ -287,6 +357,13 @@ def installed(self, package_name):
         exit_code = self._runner("rpm -q %s" % package_name, None)
         return exit_code == 0
 
+    def get_package_name(self, package, arch, arch_names):
+        if arch_names is None:
+            arch_names = {"x86": "i586"}
+        if arch in arch_names:
+            return "%s.%s" % (arch_names[arch], package)
+        return package
+
 
 def _run(runner, command, output, accepted_returns=None):
     accepted_returns = accepted_returns or [0, ]


Test patch:
diff --git a/conans/test/unittests/client/tools/system_pm_test.py b/conans/test/unittests/client/tools/system_pm_test.py
--- a/conans/test/unittests/client/tools/system_pm_test.py
+++ b/conans/test/unittests/client/tools/system_pm_test.py
@@ -14,6 +14,7 @@
 from conans.errors import ConanException
 from conans.test.unittests.util.tools_test import RunnerMock
 from conans.test.utils.tools import TestBufferConanOutput
+from conans.test.utils.conanfile import MockSettings, MockConanfile
 
 
 class SystemPackageToolTest(unittest.TestCase):
@@ -166,6 +167,16 @@ def system_package_tool_test(self):
             spt.install("a_package", force=True)
             self.assertEqual(runner.command_called, "sudo -A yum install -y a_package")
 
+            settings = MockSettings({"arch": "x86", "arch_build": "x86_64", "os": "Linux",
+                                     "os_build": "Linux"})
+            conanfile = MockConanfile(settings)
+            spt = SystemPackageTool(runner=runner, os_info=os_info, output=self.out,
+                                    conanfile=conanfile)
+            spt.install("a_package", force=False)
+            self.assertEqual(runner.command_called, "rpm -q a_package.i?86")
+            spt.install("a_package", force=True)
+            self.assertEqual(runner.command_called, "sudo -A yum install -y a_package.i?86")
+
             os_info.linux_distro = "debian"
             spt = SystemPackageTool(runner=runner, os_info=os_info, output=self.out)
             with self.assertRaises(ConanException):
@@ -235,6 +246,32 @@ def system_package_tool_test(self):
             spt.update()
             self.assertEqual(runner.command_called, "apt-get update")
 
+            for arch, distro_arch in {"x86_64": "", "x86": ":i386", "ppc32": ":powerpc",
+                                      "ppc64le": ":ppc64el", "armv7": ":arm", "armv7hf": ":armhf",
+                                      "armv8": ":arm64", "s390x": ":s390x"}.items():
+                settings = MockSettings({"arch": arch,
+                                         "arch_build": "x86_64",
+                                         "os": "Linux",
+                                         "os_build": "Linux"})
+                conanfile = MockConanfile(settings)
+                spt = SystemPackageTool(runner=runner, os_info=os_info, output=self.out,
+                                        conanfile=conanfile)
+                spt.install("a_package", force=True)
+                self.assertEqual(runner.command_called,
+                            "apt-get install -y --no-install-recommends a_package%s" % distro_arch)
+
+            for arch, distro_arch in {"x86_64": "", "x86": ":all"}.items():
+                settings = MockSettings({"arch": arch,
+                                         "arch_build": "x86_64",
+                                         "os": "Linux",
+                                         "os_build": "Linux"})
+                conanfile = MockConanfile(settings)
+                spt = SystemPackageTool(runner=runner, os_info=os_info, output=self.out,
+                                        conanfile=conanfile)
+                spt.install("a_package", force=True, arch_names={"x86": "all"})
+                self.assertEqual(runner.command_called,
+                            "apt-get install -y --no-install-recommends a_package%s" % distro_arch)
+
             os_info.is_macos = True
             os_info.is_linux = False
             os_info.is_windows = False