Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 65 additions & 11 deletions conan/tools/system/package_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand All @@ -293,34 +327,48 @@ 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}`"'


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):
Expand All @@ -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):
"""
Expand All @@ -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}"
8 changes: 0 additions & 8 deletions test/functional/tools/system/package_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed these lines because dpkg -l doesn't print anything if it doesn't find any package with the search pattern

assert "dpkg-query: no packages found matching non-existing2:i386" in client.out
assert "missing: ['non-existing1', 'non-existing2']" in client.out


Expand All @@ -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")
Expand Down Expand Up @@ -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


Expand Down
136 changes: 135 additions & 1 deletion test/integration/tools/system/package_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected] [email protected]'),
(Pkg, 'x86_64', 'pkg install -y package1-0.1 package2-0.2'),
(PkgUtil, 'x86_64', 'pkgutil --install --yes [email protected] [email protected]'),
(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 [email protected] [email protected]'),
(Pkg, 'x86', 'pkg install -y package1-0.1 package2-0.2'),
(PkgUtil, 'x86', 'pkgutil --install --yes [email protected] [email protected]'),
(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'),
Expand Down Expand Up @@ -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 [email protected] [email protected]'),
(Pkg, 'x86_64', 'pkg install -y package1-0.1 package2-0.2'),
(PkgUtil, 'x86_64', 'pkgutil --install --yes [email protected] [email protected]'),
(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 [email protected] [email protected]'),
(Pkg, 'x86', 'pkg install -y package1-0.1 package2-0.2'),
(PkgUtil, 'x86', 'pkgutil --install --yes [email protected] [email protected]'),
(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
Expand Down Expand Up @@ -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)"'),
Expand All @@ -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
Loading