Repository: conan-io/conan
Pull request: Feature/Add an option to install() method in package_manager
Changelog: Feature: Add `host_tool` to `install()` method in `package_manager` to indicate whether the package is a host tool or a library.
Docs: https://github.com/conan-io/docs/pull/3401


According to that the we will install a package for the needed arch (host arch or destination arch which can be different in case of cross building)

This option enables the user controls whether the installed package is for the host or needed for the destination arch, this is useful specifically when cross building and we want to avoid misunderstanding about what we want to install. 

Closes https://github.com/conan-io/conan/issues/14567



- [x] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [x] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one.


Problem statement:
[bug] Install packages compatible with incorrect architecture
### Environment details

* Operating System+version: all
* Compiler+version: all
* Conan version: 2.0.9 (latest)
* Python version: all


### Steps to reproduce

1. Make sure settings.arch in your profile differs your host architecture, for example:
 If your host architecture is x86 and in your profile is defined:
```
[settings]
arch=armv7
....
[conf]
tools.system.package_manager:mode=install
tools.system.package_manager:sudo=True
```

2. In the conan file you should use `_SystemPackageManagerTool` in some way, for example:
```
def system_requirements(self):
        apt = Apt(self)
        apt.install(["tree"], update=True, check=True)
```
For installing requirements for building the conan package.

3. Then execute `conan build -pr <your_profile> ....`

4. If on your system the command `tree` is not found (in linux you can uninstall it with the command `apt-get remove tree` to try this), conan will try to install the package for arm and not for your host architecture (x86), means it will try to install `tree:arm`  instead of just `tree`.

### Logs

conan executes something like that `apt-get install tree:arm` on x86 architecture and we get an error that the architecture is suitable for our device.

### Have you read the CONTRIBUTING guide?
- [x]  I've read the CONTRIBUTING guide

Hints from issue discussion:
Hi @LielHarel 

Thanks for your report.

It is not completely clear that the "build" package is always the one to be installed. I understood that some distros allow the ``:arch`` precisely to be able to install some packages that can be used in cross-compilation scenarios, even if they don't match the current machine "build" architecture. This is the case when the system requirements are libraries to link.

On the other hand, I understand that some **tools** executables, intended to be run in the "build" context (the ``tree`` example), which is a different use case than the "host" cross-build scenario of the libraries.

So I think this is not exactly a bug that is solved by changing the context for all install, but I think that what we are missing here is a qualifier in the ``install`` methods that specify whether a system-require is a tool ``build=True`` for example or it is a library (maybe the default, ``build=False``?). Based on that qualifier, the internal will decide to append the ``:arch`` host architecture or not.

If you would like to contribute such a feature, it will be very appreciated of course :) Thanks very much!
I would love to add the feature that enables the user to indicate if the installed package is a host tool or a library :) 
Open a PR for this https://github.com/conan-io/conan/pull/14752 :)

Commit message: Merge branch 'release/2.0' into feature/add-options-when-install-pkgs

Patch to review:
diff --git a/conan/tools/system/package_manager.py b/conan/tools/system/package_manager.py
index 87cb6b87174..c1d404dc378 100644
--- a/conan/tools/system/package_manager.py
+++ b/conan/tools/system/package_manager.py
@@ -62,9 +62,12 @@ def get_default_tool(self):
                 if d in os_name:
                     return tool
 
-    def get_package_name(self, package):
-        # TODO: should we only add the arch if cross-building?
-        if self._arch in self._arch_names and cross_building(self._conanfile):
+    def get_package_name(self, package, host_package=True):        
+        # Only if the package is for building, for example a library,
+        # we should add the host arch when cross building.
+        # If the package is a tool that should be installed on the current build 
+        # machine we should not add the arch.
+        if self._arch in self._arch_names and cross_building(self._conanfile) and host_package:
             return "{}{}{}".format(package, self._arch_separator,
                                    self._arch_names.get(self._arch))
         return package
@@ -149,7 +152,7 @@ def _install_substitutes(self, *packages_substitutes, update=False, check=True,
             self._conanfile.output.warning(str(error))
         raise ConanException("None of the installs for the package substitutes succeeded.")
 
-    def _install(self, packages, update=False, check=True, **kwargs):
+    def _install(self, packages, update=False, check=True, host_package=True, **kwargs):
         pkgs = self._conanfile.system_requires.setdefault(self._active_tool, {})
         install_pkgs = pkgs.setdefault("install", [])
         install_pkgs.extend(p for p in packages if p not in install_pkgs)
@@ -157,7 +160,7 @@ def _install(self, packages, update=False, check=True, **kwargs):
             return
 
         if check or self._mode in (self.mode_check, self.mode_report_installed):
-            packages = self.check(packages)
+            packages = self.check(packages, host_package=host_package)
             missing_pkgs = pkgs.setdefault("missing", [])
             missing_pkgs.extend(p for p in packages if p not in missing_pkgs)
 
@@ -178,7 +181,7 @@ def _install(self, packages, update=False, check=True, **kwargs):
             if update:
                 self.update()
 
-            packages_arch = [self.get_package_name(package) for package in packages]
+            packages_arch = [self.get_package_name(package, host_package=host_package) for package in packages]
             if packages_arch:
                 command = self.install_command.format(sudo=self.sudo_str,
                                                       tool=self.tool_name,
@@ -196,8 +199,8 @@ def _update(self):
             command = self.update_command.format(sudo=self.sudo_str, tool=self.tool_name)
             return self._conanfile_run(command, self.accepted_update_codes)
 
-    def _check(self, packages):
-        missing = [pkg for pkg in packages if self.check_package(self.get_package_name(pkg)) != 0]
+    def _check(self, packages, host_package=True):
+        missing = [pkg for pkg in packages if self.check_package(self.get_package_name(pkg, host_package=host_package)) != 0]
         return missing
 
     def check_package(self, package):
@@ -234,7 +237,7 @@ def __init__(self, conanfile, arch_names=None):
 
         self._arch_separator = ":"
 
-    def install(self, packages, update=False, check=True, recommends=False):
+    def install(self, packages, update=False, check=True, host_package=True, recommends=False):
         """
         Will try to install the list of packages passed as a parameter. Its
         behaviour is affected by the value of ``tools.system.package_manager:mode``
@@ -243,13 +246,15 @@ def install(self, packages, update=False, check=True, recommends=False):
         :param packages: try to install the list of packages passed as a parameter.
         :param update: try to update the package manager database before checking and installing.
         :param check: check if the packages are already installed before installing them.
+        :param host_package: install the packages for the host machine architecture (the machine
+               that will run the software), it has an effect when cross building.
         :param recommends: if the parameter ``recommends`` is ``False`` it will add the
                ``'--no-install-recommends'`` argument to the *apt-get* command call.
         :return: the return code of the executed apt command.
         """
         recommends_str = '' if recommends else '--no-install-recommends '
         return super(Apt, self).install(packages, update=update, check=check,
-                                        recommends=recommends_str)
+                                        host_package=host_package, recommends=recommends_str)
 
 
 class Yum(_SystemPackageManagerTool):
diff --git a/conans/test/functional/toolchains/scons/test_sconsdeps.py b/conans/test/functional/toolchains/scons/test_sconsdeps.py
index 4d23a48af9e..e86fb847f6b 100644
--- a/conans/test/functional/toolchains/scons/test_sconsdeps.py
+++ b/conans/test/functional/toolchains/scons/test_sconsdeps.py
@@ -8,7 +8,7 @@
 
 @pytest.mark.skipif(platform.system() != "Linux", reason="SCons functional tests"
                                                          "only for Linux")
-@pytest.mark.tool("scons")
+@pytest.mark.tool_scons
 def test_sconsdeps():
     client = TestClient(path_with_spaces=False)
 
diff --git a/conans/test/integration/tools/system/package_manager_test.py b/conans/test/integration/tools/system/package_manager_test.py
index bf20c43d88c..f5c4cead21e 100644
--- a/conans/test/integration/tools/system/package_manager_test.py
+++ b/conans/test/integration/tools/system/package_manager_test.py
@@ -193,7 +193,7 @@ def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=Fa
 
 
 @pytest.mark.parametrize("tool_class, arch_host, result", [
-    # not cross-compile -> do not add host architecture
+    # Install host package and not cross-compile -> do not add host architecture
     (Apk, 'x86_64', 'apk add --no-cache package1 package2'),
     (Apt, 'x86_64', 'apt-get install -y --no-install-recommends package1 package2'),
     (Yum, 'x86_64', 'yum install -y package1 package2'),
@@ -204,7 +204,7 @@ def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=Fa
     (Chocolatey, 'x86_64', 'choco install --yes package1 package2'),
     (PacMan, 'x86_64', 'pacman -S --noconfirm package1 package2'),
     (Zypper, 'x86_64', 'zypper --non-interactive in package1 package2'),
-    # cross-compile -> add host architecture https://github.com/conan-io/conan/issues/12320
+    # Install host package and cross-compile -> add host architecture
     (Apt, 'x86', 'apt-get install -y --no-install-recommends package1:i386 package2:i386'),
     (Yum, 'x86', 'yum install -y package1.i?86 package2.i?86'),
     (Dnf, 'x86', 'dnf install -y package1.i?86 package2.i?86'),
@@ -229,11 +229,51 @@ def fake_check(*args, **kwargs):
             return ["package1", "package2"]
         from conan.tools.system.package_manager import _SystemPackageManagerTool
         with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)):
-            tool.install(["package1", "package2"])
+            tool.install(["package1", "package2"], host_package=True)
 
     assert tool._conanfile.command == result
 
+@pytest.mark.parametrize("tool_class, arch_host, result", [
+    # Install build machine package and not cross-compile -> do not add host architecture
+    (Apk, 'x86_64', 'apk add --no-cache package1 package2'),
+    (Apt, 'x86_64', 'apt-get install -y --no-install-recommends package1 package2'),
+    (Yum, 'x86_64', 'yum install -y package1 package2'),
+    (Dnf, 'x86_64', 'dnf install -y package1 package2'),
+    (Brew, 'x86_64', 'brew install package1 package2'),
+    (Pkg, 'x86_64', 'pkg install -y package1 package2'),
+    (PkgUtil, 'x86_64', 'pkgutil --install --yes package1 package2'),
+    (Chocolatey, 'x86_64', 'choco install --yes package1 package2'),
+    (PacMan, 'x86_64', 'pacman -S --noconfirm package1 package2'),
+    (Zypper, 'x86_64', 'zypper --non-interactive in package1 package2'),
+    # Install build machine package and cross-compile -> do not add host architecture
+    (Apt, 'x86', 'apt-get install -y --no-install-recommends package1 package2'),
+    (Yum, 'x86', 'yum install -y package1 package2'),
+    (Dnf, 'x86', 'dnf install -y package1 package2'),
+    (Brew, 'x86', 'brew install package1 package2'),
+    (Pkg, 'x86', 'pkg install -y package1 package2'),
+    (PkgUtil, 'x86', 'pkgutil --install --yes package1 package2'),
+    (Chocolatey, 'x86', 'choco install --yes package1 package2'),
+    (PacMan, 'x86', 'pacman -S --noconfirm package1 package2'),
+    (Zypper, 'x86', 'zypper --non-interactive in package1 package2'),
+])
+def test_tools_install_mode_install_to_build_machine_arch(tool_class, arch_host, result):
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"arch": arch_host})
+    conanfile.settings_build = MockSettings({"arch": "x86_64"})
+    conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name)
+    conanfile.conf.define("tools.system.package_manager:mode", "install")
+    with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock:
+        context_mock.return_value = "host"
+        tool = tool_class(conanfile)
+
+        def fake_check(*args, **kwargs):
+            return ["package1", "package2"]
+        from conan.tools.system.package_manager import _SystemPackageManagerTool
+        with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)):
+            tool.install(["package1", "package2"], host_package=False)
 
+    assert tool._conanfile.command == result
+    
 @pytest.mark.parametrize("tool_class, result", [
     # cross-compile but arch_names=None -> do not add host architecture
     # https://github.com/conan-io/conan/issues/12320 because the package is archless
@@ -282,4 +322,4 @@ def test_tools_check(tool_class, result):
         tool = tool_class(conanfile)
         tool.check(["package"])
 
-    assert tool._conanfile.command == result
+    assert tool._conanfile.command == result
\ No newline at end of file


Code review comments:
diff_hunk: @@ -229,11 +229,51 @@ def fake_check(*args, **kwargs):
             return ["package1", "package2"]
         from conan.tools.system.package_manager import _SystemPackageManagerTool
         with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)):
-            tool.install(["package1", "package2"])
+            tool.install(["package1", "package2"], host_package=True)
path: conans/test/integration/tools/system/package_manager_test.py
text: @user1:
```suggestion
            tool.install(["package1", "package2"])
```
Default not necessary

diff_hunk: @@ -234,7 +237,7 @@ def __init__(self, conanfile, arch_names=None):
 
         self._arch_separator = ":"
 
-    def install(self, packages, update=False, check=True, recommends=False):
+    def install(self, packages, update=False, check=True, host_package=True, recommends=False):
path: conan/tools/system/package_manager.py
text: @user1:
```suggestion
    def install(self, packages, update=False, check=True, recommends=False, host_package=True):
```
The likelihood to break existing code is lower if new arguments are added at the end.

diff_hunk: @@ -8,7 +8,7 @@
 
 @pytest.mark.skipif(platform.system() != "Linux", reason="SCons functional tests"
                                                          "only for Linux")
-@pytest.mark.tool("scons")
+@pytest.mark.tool_scons
path: conans/test/functional/toolchains/scons/test_sconsdeps.py
text: @user2:
```suggestion
@user1.mark.tool("scons")
```