Repository: conan-io/conan

Issue:
[feature] Add the possibility to choose exact package version with tools.system.package_manager
### What is your suggestion?

Hi!

I've read the [documentation](https://docs.conan.io/2/reference/tools/system/package_manager.html) for package_manager module, tried it in recipes and read the source [code](https://github.com/conan-io/conan/blob/develop2/conan/tools/system/package_manager.py) of the module.
Nowhere to be found is the ability to choose the exact version of the package that system needs to install.

For the `apt` or `apt-get` interface it is possible to provide the version in the following way:
`apt-get install package=X.Y.Z`

For `choco`:
`choco install package --version=X.Y.Z`

I don't know any Linux\Windows\MacOS package managers (including brew\macports) which don't provide this ability.
So is there any chance for it to be added to Conan?


### Have you read the CONTRIBUTING guide?

- [x] I've read the CONTRIBUTING guide
[bug] `system_requirements()`: apt problems when cross-building and using `all` packages
### Describe the bug

Environment: Linux x86_64, Debian bookworm, conan 2.17.0

**Note**: `strace` and `tzdata` are just examples for architecture-dependent and architecture-independent packages, respectively.

Conan does not correctly install packages when there is already an appropriate package for a different architecture installed.
Also, when cross-building, conan never thinks that `all` packages are installed.

I already checked the source code why it fails:
* The [check command](https://github.com/conan-io/conan/blob/7bdaec04db20cdbbb2f9f9d9d4e53aa58ef36469/conan/tools/system/package_manager.py#L223) is set to: `dpkg-query -W -f='${Status}' {package} | grep -q "ok installed"`
* `{package}` is `strace` or `strace:arm64`, for `x86_64` and `armv8` host architecture, respectively. See [code here](https://github.com/conan-io/conan/blob/7bdaec04db20cdbbb2f9f9d9d4e53aa58ef36469/conan/tools/system/package_manager.py#L70).

There are two problems:
* When the `arm64` package is already installed and when building for native (`x86_64`), conan thinks that the needed package is already installed, doesn't install it and later the build fails as it can't find the needed files for `x86_64`.
  * `dpkg-query -W -f='${Status}' strace` is executed sucessfully, however only due to the installed `arm64` package.
  * Conan thinks everything is okay, skipping installation of `amd64` variant.
* When you depend on an architecture-independent (`all`) package, conan never sees it as installed when cross-building.  
  conan checks for `tzdata:arm64`, but there is only `tzdata:all` or `tzdata`.

A possible fix seems to be to check that at least one of `<package>:<architecture>` and `<package>:all` is installed.
`dpkg-query` can handle multiple packages at once.

### How to reproduce it

`conanfile.py`:
```py
from conan import ConanFile
from conan.tools.system.package_manager import Apt

class Recipe(ConanFile):
    name = "demo"
    version = "1.0"

    settings = "arch"

    def system_requirements(self):
        Apt(self).install(["strace", "tzdata"])

    def build(self):
        self.run("file /usr/bin/strace")
```

* Execute `conan build . -s arch=armv8 -c tools.system.package_manager:mode=install` two times.
  At first time `strace:arm64` gets installed, every following execution is a no-op, but still invokes `apt`.
* Execute `conan build . -s arch=x86_64 -c tools.system.package_manager:mode=install`.
  After that execute `dpkg -l strace` and you see `strace` is not really installed (only the wrong `arm64` package).


Discussion:
Hi @CommanderCRM 

Thanks for the feedback.

So far this hasn't been a need, because the ``tools.system.package_manager`` functionality is focused on "system" packages that are very challenging to be implemented as Conan packages and be versioned with them.
The usages in ConanCenter are for example things like ``xorg`` or ``opengl``, which usually comes very defined by the Linux distribution that is running, it is not that selecting different versions is always possible.

And for other more regular system packages that could have several versions, this approach can easily lead to issues with system versions, for example if 2 different Conan packages installed different versions of the same system package, that could produce inconsistencies and problems if those 2 packages are used in the same project. And tracking the system packages versions to detect conflicts is not planned. This is why Conan users mostly create Conan packages for the kind of thing that can be easily versioned and it is not very integrated with the system to be managed externally.

Said that, I am not opposed to have this functionality, but given the usage so far, this wouldn't be a priority for the team. If some users are willing to contribute such functionality, it should cover the cases that ``_SystemPackageManagerTool`` is covering, that is: "check", "install", "update", and the different modes that ``tools.system.package_manager:mode`` provides: "check", "install", "report" and "report-installed". It could be integrated in the current package name such ``apt.install(["pkg1", "pkg2"])`` => ``apt.install(["pkg1=version1", "pkg2=version2"])``, and that can be parsed internally in case it is necessary. In any case, this would still be a thin wrapper over the system package manager command, no other functionality as version conflict or the like should be considered.





Patch:
diff --git a/conan/tools/system/package_manager.py b/conan/tools/system/package_manager.py
index 570744d5e0a..c5b32314e3f 100644
--- a/conan/tools/system/package_manager.py
+++ b/conan/tools/system/package_manager.py
@@ -11,9 +11,12 @@ class _SystemPackageManagerTool(object):
     mode_report = "report"  # Only report what would be installed, no check (can run in any system)
     mode_report_installed = "report-installed"  # report installed and missing packages
     tool_name = None
+    version_separator = ""
     install_command = ""
     update_command = ""
     check_command = ""
+    check_version_command = ""
+    full_package_name = "{name}{arch_separator}{arch_name}"
     accepted_install_codes = [0]
     accepted_update_codes = [0]
     accepted_check_codes = [0, 1]
@@ -67,15 +70,30 @@ def get_default_tool(self):
         self._conanfile.output.info("A default system package manager couldn't be found for {}, "
                                     "system packages will not be installed.".format(os_name))
 
+    def _split_package_name(self, package, host_package):
+
+        name, version = (package.split("=")[0], package.split("=")[1]) if "=" in package else (package, "")
+        arch_separator, arch_name = "", ""
+        version_separator = self.version_separator if version else ""
+
+        if self._arch in self._arch_names and cross_building(self._conanfile) and host_package:
+            arch_separator = self._arch_separator
+            arch_name = self._arch_names.get(self._arch)
+        return name, version, arch_separator, arch_name, version_separator
+
     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
+
+        name, version, arch_separator, arch_name, version_separator = self._split_package_name(package, host_package)
+
+        return self.full_package_name.format(name=name,
+                                             arch_separator=arch_separator,
+                                             arch_name=arch_name,
+                                             version_separator=version_separator,
+                                             version=version)
 
     @property
     def sudo_str(self):
@@ -169,7 +187,6 @@ def _install(self, packages, update=False, check=True, host_package=True, **kwar
             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)
-
         if self._mode == self.mode_report_installed:
             return
 
@@ -186,7 +203,6 @@ def _install(self, packages, update=False, check=True, host_package=True, **kwar
         elif packages:
             if update:
                 self.update()
-
             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,
@@ -206,21 +222,36 @@ def _update(self):
             return self._conanfile_run(command, self.accepted_update_codes)
 
     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]
+        missing = [pkg for pkg in packages if self.check_package(pkg, host_package) != 0]
         return missing
 
-    def check_package(self, package):
-        command = self.check_command.format(tool=self.tool_name,
-                                            package=package)
+    def check_package(self, package, host_package=True):
+        name, version, arch_separator, arch_name, _ = self._split_package_name(package, host_package)
+        arch_package = arch_name or self._arch_names.get(self._arch or self._conanfile.settings_build.get_safe('arch'))
+        package = self.full_package_name.format(name=name,
+                                                arch_separator=arch_separator,
+                                                arch_name=arch_name,
+                                                version="",
+                                                version_separator="")
+        command = self.check_command.format(tool=self.tool_name, package=package, arch_package=arch_package)
+        if version:
+            if self.check_version_command:
+                command = self.check_version_command.format(tool=self.tool_name, package=package, version=version, arch_package=arch_package)
+            else:
+                self._conanfile.output.warning(f"System requirements: \"{self.tool_name}\" doesn't support package versions,"
+                                               f" \"{package}\" will be installed without a specific version.")
         return self._conanfile_run(command, self.accepted_check_codes)
 
 
 class Apt(_SystemPackageManagerTool):
     # TODO: apt? apt-get?
     tool_name = "apt-get"
+    version_separator = "="
+    full_package_name = "{name}{arch_separator}{arch_name}{version_separator}{version}"
     install_command = "{sudo}{tool} install -y {recommends}{packages}"
     update_command = "{sudo}{tool} update"
-    check_command = "dpkg-query -W -f='${{Status}}' {package} | grep -q \"ok installed\""
+    check_command = "dpkg-query -W -f='${{Architecture}}' {package} | grep -qEx '({arch_package}|all)'"
+    check_version_command = "dpkg-query -W -f='${{Architecture}} ${{Version}}' {package} | grep -qEx '({arch_package}|all) {version}'"
 
     def __init__(self, conanfile, arch_names=None):
         """
@@ -265,9 +296,12 @@ def install(self, packages, update=False, check=True, recommends=False, host_pac
 
 class Yum(_SystemPackageManagerTool):
     tool_name = "yum"
+    version_separator = "-"
+    full_package_name = "{name}{version_separator}{version}{arch_separator}{arch_name}"
     install_command = "{sudo}{tool} install -y {packages}"
     update_command = "{sudo}{tool} check-update -y"
     check_command = "rpm -q {package}"
+    check_version_command = "rpm -q {package}-{version}"
     accepted_update_codes = [0, 100]
 
     def __init__(self, conanfile, arch_names=None):
@@ -293,24 +327,35 @@ def __init__(self, conanfile, arch_names=None):
 
 class Dnf(Yum):
     tool_name = "dnf"
+    version_separator = "-"
+    full_package_name = "{name}{version_separator}{version}{arch_separator}{arch_name}"
+    check_version_command = "rpm -q {package}-{version}"
 
 
 class Brew(_SystemPackageManagerTool):
     tool_name = "brew"
+    version_separator = "@"
+    full_package_name = "{name}{version_separator}{version}"
     install_command = "{sudo}{tool} install {packages}"
     update_command = "{sudo}{tool} update"
     check_command = 'test -n "$({tool} ls --versions {package})"'
+    check_version_command = 'brew list --versions {package} | grep "{version}"'
 
 
 class Pkg(_SystemPackageManagerTool):
     tool_name = "pkg"
+    version_separator = "-"
+    full_package_name = "{name}{version_separator}{version}"
     install_command = "{sudo}{tool} install -y {packages}"
     update_command = "{sudo}{tool} update"
     check_command = "{tool} info {package}"
+    check_version_command = "{tool} info {package} | grep \"Version: {version}\""
 
 
 class PkgUtil(_SystemPackageManagerTool):
     tool_name = "pkgutil"
+    version_separator = "@"
+    full_package_name = "{name}{version_separator}{version}"
     install_command = "{sudo}{tool} --install --yes {packages}"
     update_command = "{sudo}{tool} --catalog"
     check_command = 'test -n "`{tool} --list {package}`"'
@@ -318,9 +363,12 @@ class PkgUtil(_SystemPackageManagerTool):
 
 class Chocolatey(_SystemPackageManagerTool):
     tool_name = "choco"
+    version_separator = " --version "
+    full_package_name = "{name}{version_separator}{version}"
     install_command = "{tool} install --yes {packages}"
     update_command = "{tool} outdated"
     check_command = '{tool} list --exact {package} | findstr /c:"1 packages installed."'
+    check_version_command = '{tool} list --local-only {package} | findstr /i "{version}"'
 
 
 class PacMan(_SystemPackageManagerTool):
@@ -345,9 +393,12 @@ def __init__(self, conanfile, arch_names=None):
 
 class Apk(_SystemPackageManagerTool):
     tool_name = "apk"
+    version_separator = "="
+    full_package_name = "{name}{version_separator}{version}"
     install_command = "{sudo}{tool} add --no-cache {packages}"
     update_command = "{sudo}{tool} update"
     check_command = "{tool} info -e {package}"
+    check_version_command = "{tool} info {package} | grep \"{version}\""
 
     def __init__(self, conanfile, _arch_names=None):
         """
@@ -364,6 +415,9 @@ def __init__(self, conanfile, _arch_names=None):
 
 class Zypper(_SystemPackageManagerTool):
     tool_name = "zypper"
+    version_separator = "="  # < or >
+    full_package_name = "{name}{arch_separator}{arch_name}{version_separator}{version}"
     install_command = "{sudo}{tool} --non-interactive in {packages}"
     update_command = "{sudo}{tool} --non-interactive ref"
     check_command = "rpm -q {package}"
+    check_version_command = "rpm -q {package}-{version}"


Test patch:
diff --git a/test/functional/tools/system/package_manager_test.py b/test/functional/tools/system/package_manager_test.py
index fc030542e71..eb61a2449cf 100644
--- a/test/functional/tools/system/package_manager_test.py
+++ b/test/functional/tools/system/package_manager_test.py
@@ -25,8 +25,6 @@ def system_requirements(self):
                 print("missing:", not_installed)
         """)})
     client.run("create . --name=test --version=1.0 -s:b arch=armv8 -s:h arch=x86")
-    assert "dpkg-query: no packages found matching non-existing1:i386" in client.out
-    assert "dpkg-query: no packages found matching non-existing2:i386" in client.out
     assert "missing: ['non-existing1', 'non-existing2']" in client.out
 
 
@@ -53,10 +51,6 @@ def system_requirements(self):
     client.save({"conanfile.py": conanfile_py.format(installs)})
     client.run("create . --name=test --version=1.0 -c tools.system.package_manager:mode=install "
                "-c tools.system.package_manager:sudo=True", assert_error=True)
-    assert "dpkg-query: no packages found matching non-existing1" in client.out
-    assert "dpkg-query: no packages found matching non-existing2" in client.out
-    assert "dpkg-query: no packages found matching non-existing3" in client.out
-    assert "dpkg-query: no packages found matching non-existing4" in client.out
     assert "None of the installs for the package substitutes succeeded." in client.out
 
     client.run_command("sudo apt remove nano -yy")
@@ -91,8 +85,6 @@ class consumer(ConanFile):
         """)})
     client.run("create consumer.py --name=consumer --version=1.0 "
                "-s:b arch=armv8 -s:h arch=x86 --build=missing")
-    assert "dpkg-query: no packages found matching non-existing1" in client.out
-    assert "dpkg-query: no packages found matching non-existing2" in client.out
     assert "missing: ['non-existing1', 'non-existing2']" in client.out
 
 
diff --git a/test/integration/tools/system/package_manager_test.py b/test/integration/tools/system/package_manager_test.py
index 1309469b987..445bbabc1bd 100644
--- a/test/integration/tools/system/package_manager_test.py
+++ b/test/integration/tools/system/package_manager_test.py
@@ -246,6 +246,47 @@ def fake_check(*args, **kwargs):
     assert tool._conanfile.command == result
 
 
+@pytest.mark.parametrize("tool_class, arch_host, result", [
+    # Install host package and not cross-compile -> do not add host architecture
+    (Apk, 'x86_64', 'apk add --no-cache package1=0.1 package2=0.2'),
+    (Apt, 'x86_64', 'apt-get install -y --no-install-recommends package1=0.1 package2=0.2'),
+    (Yum, 'x86_64', 'yum install -y package1-0.1 package2-0.2'),
+    (Dnf, 'x86_64', 'dnf install -y package1-0.1 package2-0.2'),
+    (Brew, 'x86_64', 'brew install package1@0.1 package2@0.2'),
+    (Pkg, 'x86_64', 'pkg install -y package1-0.1 package2-0.2'),
+    (PkgUtil, 'x86_64', 'pkgutil --install --yes package1@0.1 package2@0.2'),
+    (Chocolatey, 'x86_64', 'choco install --yes package1 --version 0.1 package2 --version 0.2'),
+    (PacMan, 'x86_64', 'pacman -S --noconfirm package1 package2'),
+    (Zypper, 'x86_64', 'zypper --non-interactive in package1=0.1 package2=0.2'),
+    # Install host package and cross-compile -> add host architecture
+    (Apt, 'x86', 'apt-get install -y --no-install-recommends package1:i386=0.1 package2:i386=0.2'),
+    (Yum, 'x86', 'yum install -y package1-0.1.i?86 package2-0.2.i?86'),
+    (Dnf, 'x86', 'dnf install -y package1-0.1.i?86 package2-0.2.i?86'),
+    (Brew, 'x86', 'brew install package1@0.1 package2@0.2'),
+    (Pkg, 'x86', 'pkg install -y package1-0.1 package2-0.2'),
+    (PkgUtil, 'x86', 'pkgutil --install --yes package1@0.1 package2@0.2'),
+    (Chocolatey, 'x86', 'choco install --yes package1 --version 0.1 package2 --version 0.2'),
+    (PacMan, 'x86', 'pacman -S --noconfirm package1-lib32 package2-lib32'),
+    (Zypper, 'x86', 'zypper --non-interactive in package1=0.1 package2=0.2'),
+])
+def test_tools_install_mode_install_different_archs_with_version(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=0.1", "package2=0.2"]
+        from conan.tools.system.package_manager import _SystemPackageManagerTool
+        with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)):
+            tool.install(["package1=0.1", "package2=0.2"])
+    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'),
@@ -288,6 +329,48 @@ def fake_check(*args, **kwargs):
     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=0.1 package2=0.2'),
+    (Apt, 'x86_64', 'apt-get install -y --no-install-recommends package1=0.1 package2=0.2'),
+    (Yum, 'x86_64', 'yum install -y package1-0.1 package2-0.2'),
+    (Dnf, 'x86_64', 'dnf install -y package1-0.1 package2-0.2'),
+    (Brew, 'x86_64', 'brew install package1@0.1 package2@0.2'),
+    (Pkg, 'x86_64', 'pkg install -y package1-0.1 package2-0.2'),
+    (PkgUtil, 'x86_64', 'pkgutil --install --yes package1@0.1 package2@0.2'),
+    (Chocolatey, 'x86_64', 'choco install --yes package1 --version 0.1 package2 --version 0.2'),
+    (PacMan, 'x86_64', 'pacman -S --noconfirm package1 package2'),
+    (Zypper, 'x86_64', 'zypper --non-interactive in package1=0.1 package2=0.2'),
+    # Install build machine package and cross-compile -> do not add host architecture
+    (Apt, 'x86', 'apt-get install -y --no-install-recommends package1=0.1 package2=0.2'),
+    (Yum, 'x86', 'yum install -y package1-0.1 package2-0.2'),
+    (Dnf, 'x86', 'dnf install -y package1-0.1 package2-0.2'),
+    (Brew, 'x86', 'brew install package1@0.1 package2@0.2'),
+    (Pkg, 'x86', 'pkg install -y package1-0.1 package2-0.2'),
+    (PkgUtil, 'x86', 'pkgutil --install --yes package1@0.1 package2@0.2'),
+    (Chocolatey, 'x86', 'choco install --yes package1 --version 0.1 package2 --version 0.2'),
+    (PacMan, 'x86', 'pacman -S --noconfirm package1 package2'),
+    (Zypper, 'x86', 'zypper --non-interactive in package1=0.1 package2=0.2'),
+])
+def test_tools_install_mode_install_to_build_machine_arch_with_version(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=0.1", "package2=0.2"]
+        from conan.tools.system.package_manager import _SystemPackageManagerTool
+        with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)):
+            tool.install(["package1=0.1", "package2=0.2"], 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
@@ -315,9 +398,36 @@ def fake_check(*args, **kwargs):
     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
+    (Apt, 'apt-get install -y --no-install-recommends package1=0.1 package2=0.2'),
+    (Yum, 'yum install -y package1-0.1 package2-0.2'),
+    (Dnf, 'dnf install -y package1-0.1 package2-0.2'),
+    (PacMan, 'pacman -S --noconfirm package1 package2'),
+])
+def test_tools_install_archless_with_version(tool_class, result):
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"arch": "x86"})
+    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, arch_names={})
+
+        def fake_check(*args, **kwargs):
+            return ["package1=0.1", "package2=0.2"]
+        from conan.tools.system.package_manager import _SystemPackageManagerTool
+        with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)):
+            tool.install(["package1=0.1", "package2=0.2"])
+
+    assert tool._conanfile.command == result
+
+
 @pytest.mark.parametrize("tool_class, result", [
     (Apk, 'apk info -e package'),
-    (Apt, 'dpkg-query -W -f=\'${Status}\' package | grep -q "ok installed"'),
+    (Apt, 'dpkg-query -W -f=\'${Architecture}\' package | grep -qEx \'(amd64|all)\''),
     (Yum, 'rpm -q package'),
     (Dnf, 'rpm -q package'),
     (Brew, 'test -n "$(brew ls --versions package)"'),
@@ -337,3 +447,27 @@ def test_tools_check(tool_class, result):
         tool.check(["package"])
 
     assert tool._conanfile.command == result
+
+
+@pytest.mark.parametrize("tool_class, result", [
+    (Apk, 'apk info package | grep "0.1"'),
+    (Apt, 'dpkg-query -W -f=\'${Architecture} ${Version}\' package | grep -qEx \'(amd64|all) 0.1\''),
+    (Yum, 'rpm -q package-0.1'),
+    (Dnf, 'rpm -q package-0.1'),
+    (Brew, 'brew list --versions package | grep "0.1"'),
+    (Pkg, 'pkg info package | grep "Version: 0.1"'),
+    (PkgUtil, 'test -n "`pkgutil --list package`"'),
+    (Chocolatey, 'choco list --local-only package | findstr /i "0.1"'),
+    (PacMan, 'pacman -Qi package'),
+    (Zypper, 'rpm -q package-0.1'),
+])
+def test_tools_check_with_version(tool_class, result):
+    conanfile = ConanFileMock()
+    conanfile.settings = Settings()
+    conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name)
+    with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock:
+        context_mock.return_value = "host"
+        tool = tool_class(conanfile)
+        tool.check(["package=0.1"])
+
+    assert tool._conanfile.command == result
\ No newline at end of file