Repository: conan-io/conan

Issue:
[feature] Support Alpine Linux system package manager
### What is your suggestion?

Hi,

The system manager support in conan is really handy and already covers a large list of distributions/tools.
It would be very nice though if it could also support [`apk`](https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper) for the Alpine Linux distribution.

### Have you read the CONTRIBUTING guide?

- [X] I've read the CONTRIBUTING guide


Discussion:
Hi @Sahnvour 

Thanks for your suggestion.

Just to be sure, this is to add the ``Apk`` package manager to the Conan tool, but that doesn't mean that package recipes in ConanCenter will necessarily add its usage (Alpine not tested in ConanCenter CI). So it is for your own private recipes, isn't it?

We don't know much about ``apk``, would you like to contribute the feature yourself? It should be relatively straightforward, copying one of the existing ones.
Hi @memsharded,

> So it is for your own private recipes, isn't it?

Yes, I am building my projects on Alpine in CI because it's a musl-based distribution that improves coverage, and I also think it is quite used in the docker space because of its small default size. As such, I'd be glad to let conan install system dependencies by itself instead of manually listing them myself.

I understand ConanCenter would probably not have prebuilt binaries but it's ok.
I don't know much about `apk` myself, however if you can give me some pointers on where to start in conan I will have a look at it (no promises).
> I don't know much about apk myself, however if you can give me some pointers on where to start in conan I will have a look at it (no promises).

Sure, no commitment here at all, just if you like to do it. Some pointers:

- The entry point, and possibly the only file necessary to change is this one: https://github.com/conan-io/conan/blob/release/2.0/conan/tools/system/package_manager.py
- Copy and paste the ``Apt`` or the ``Yum`` class, and then start customizing for the ``apk`` case.
- If you clone and pip-install as editable the source project (https://github.com/conan-io/conan#setup), you can run ``conan`` commands using your clone changes
- Write a small recipe ``conanfile.py`` with a ``system_requirements()`` method that uses your new ``Apk`` class
- Try things.
- Finally, some integration tests would be enough, in this file: https://github.com/conan-io/conan/blob/release/2.0/conans/test/integration/tools/system/package_manager_test.py
Hey @memsharded ! Thanks for the pointers!
One issue I realized while poking into that is the fact that Alpine is a no-multilib distro. Hence, the concept of adding an arch separator and an explicit architecture to the package name is not applicable there.

So far I'm not sure how much of a problem this is. Let's see.

Resolving patch:
diff --git a/conan/tools/system/package_manager.py b/conan/tools/system/package_manager.py
--- a/conan/tools/system/package_manager.py
+++ b/conan/tools/system/package_manager.py
@@ -40,6 +40,7 @@ def get_default_tool(self):
         elif os_name == "Windows" and self._conanfile.conf.get("tools.microsoft.bash:subsystem") == "msys2":
             os_name = "msys2"
         manager_mapping = {"apt-get": ["Linux", "ubuntu", "debian", "raspbian"],
+                           "apk": ["alpine"],
                            "yum": ["pidora", "scientific", "xenserver", "amazon", "oracle", "amzn",
                                    "almalinux", "rocky"],
                            "dnf": ["fedora", "rhel", "centos", "mageia"],
@@ -332,6 +333,25 @@ def __init__(self, conanfile, arch_names=None):
         self._arch_separator = "-"
 
 
+class Apk(_SystemPackageManagerTool):
+    tool_name = "apk"
+    install_command = "{sudo}{tool} add --no-cache {packages}"
+    update_command = "{sudo}{tool} update"
+    check_command = "{tool} info -e {package}"
+
+    def __init__(self, conanfile, _arch_names=None):
+        """
+        Constructor method.
+        Note that *Apk* does not support architecture names since Alpine Linux does not support
+        multiarch. Therefore, the ``arch_names`` argument is ignored.
+
+        :param conanfile: the current recipe object. Always use ``self``.
+        """
+        super(Apk, self).__init__(conanfile)
+        self._arch_names = {}
+        self._arch_separator = ""
+
+
 class Zypper(_SystemPackageManagerTool):
     tool_name = "zypper"
     install_command = "{sudo}{tool} --non-interactive in {packages}"


Test patch:
diff --git a/conans/test/integration/tools/system/package_manager_test.py b/conans/test/integration/tools/system/package_manager_test.py
--- a/conans/test/integration/tools/system/package_manager_test.py
+++ b/conans/test/integration/tools/system/package_manager_test.py
@@ -5,8 +5,8 @@
 import pytest
 from unittest.mock import PropertyMock
 
-from conan.tools.system.package_manager import Apt, Dnf, Yum, Brew, Pkg, PkgUtil, Chocolatey, Zypper, \
-    PacMan, _SystemPackageManagerTool
+from conan.tools.system.package_manager import Apt, Apk, Dnf, Yum, Brew, Pkg, PkgUtil, Chocolatey, \
+    Zypper, PacMan, _SystemPackageManagerTool
 from conans.errors import ConanException
 from conans.model.conf import Conf
 from conans.model.settings import Settings
@@ -58,6 +58,7 @@ def test_msys2():
     ("opensuse-leap", "zypper"),
     ("opensuse-next_version", "zypper"),
     ("freebsd", "pkg"),
+    ("alpine", "apk"),
 ])
 @pytest.mark.skipif(platform.system() != "Linux", reason="Only linux")
 def test_package_manager_distro(distro, tool):
@@ -107,7 +108,7 @@ def test_apt_install_recommends(recommends, recommends_str):
 
 
 @pytest.mark.parametrize("tool_class",
-                         [Apt, Yum, Dnf, Brew, Pkg, PkgUtil, Chocolatey, PacMan, Zypper])
+                         [Apk, Apt, Yum, Dnf, Brew, Pkg, PkgUtil, Chocolatey, PacMan, Zypper])
 def test_tools_install_mode_check(tool_class):
     conanfile = ConanFileMock()
     conanfile.conf = Conf()
@@ -132,6 +133,7 @@ def fake_check(*args, **kwargs):
 
 @pytest.mark.parametrize("tool_class, result",
 [
+    (Apk, "apk update"),
     (Apt, "apt-get update"),
     (Yum, "yum check-update -y"),
     (Dnf, "dnf check-update -y"),
@@ -198,6 +200,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
+    (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'),
@@ -221,7 +224,7 @@ def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=Fa
 def test_tools_install_mode_install_different_archs(tool_class, arch_host, result):
     conanfile = ConanFileMock()
     conanfile.conf = Conf()
-    conanfile.settings = MockSettings({"arch": f"{arch_host}"})
+    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")
@@ -267,6 +270,7 @@ def fake_check(*args, **kwargs):
 
 
 @pytest.mark.parametrize("tool_class, result", [
+    (Apk, 'apk info -e package'),
     (Apt, 'dpkg-query -W -f=\'${Status}\' package | grep -q "ok installed"'),
     (Yum, 'rpm -q package'),
     (Dnf, 'rpm -q package'),