← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/lpcraft:add-miniconda-plugin into lpcraft:main

 

Jürgen Gmach has proposed merging ~jugmac00/lpcraft:add-miniconda-plugin into lpcraft:main.

Commit message:
Add news entry for adding the Miniconda plugin

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/lpcraft/+git/lpcraft/+merge/423699
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/lpcraft:add-miniconda-plugin into lpcraft:main.
diff --git a/NEWS.rst b/NEWS.rst
index 7a51838..de18653 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -13,6 +13,8 @@ Version history
 
 - Allow interpolation of the  ``run`` commands.
 
+- Add Miniconda plugin.
+
 0.0.14 (2022-05-18)
 ===================
 
diff --git a/lpcraft/plugin/tests/test_plugins.py b/lpcraft/plugin/tests/test_plugins.py
index 6976849..fe7d930 100644
--- a/lpcraft/plugin/tests/test_plugins.py
+++ b/lpcraft/plugin/tests/test_plugins.py
@@ -324,3 +324,162 @@ class TestPlugins(CommandBaseTestCase):
         plugin_config = fake_plugin[0].get_plugin_config()
         self.assertEqual(plugin_config.python_version, "3.8")
         self.assertIsNone(getattr(plugin_config, "series", None))
+
+    @patch("lpcraft.commands.run.get_provider")
+    @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+    def test_miniconda_plugin(
+        self, mock_get_host_architecture, mock_get_provider
+    ):
+        launcher = Mock(spec=launch)
+        provider = makeLXDProvider(lxd_launcher=launcher)
+        mock_get_provider.return_value = provider
+        execute_run = launcher.return_value.execute_run
+        execute_run.return_value = subprocess.CompletedProcess([], 0)
+        config = dedent(
+            """
+            pipeline:
+                - build
+
+            jobs:
+                build:
+                    series: focal
+                    architectures: amd64
+                    plugin: miniconda
+                    conda-channels:
+                        - conda-forge
+                    conda-packages:
+                        - mamba
+                        - pip
+                    conda-python: 3.8
+                    run: |
+                        pip install --upgrade pytest
+            """
+        )
+        Path(".launchpad.yaml").write_text(config)
+        pre_run_command = dedent(
+            """
+        if [ ! -d "$HOME/miniconda3" ]; then
+            wget -O /tmp/miniconda.sh https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
+            chmod +x /tmp/miniconda.sh
+            /tmp/miniconda.sh -b
+        fi
+        export PATH=$HOME/miniconda3/bin:$PATH
+        conda remove --all -q -y -n $CONDA_ENV
+        conda create -n $CONDA_ENV -q -y -c conda-forge -c defaults PYTHON=3.8 mamba pip
+        source activate $CONDA_ENV
+        """  # noqa:E501
+        )
+
+        run_command = dedent(
+            """
+            export PATH=$HOME/miniconda3/bin:$PATH
+            source activate $CONDA_ENV
+            pip install --upgrade pytest
+        """
+        )
+        post_run_command = (
+            "export PATH=$HOME/miniconda3/bin:$PATH; "
+            "source activate $CONDA_ENV; conda env export"
+        )
+
+        self.run_command("run")
+
+        self.assertEqual(
+            [
+                call(
+                    ["apt", "update"],
+                    cwd=PosixPath("/root/lpcraft/project"),
+                    env={"CONDA_ENV": "lpci"},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+                call(
+                    [
+                        "apt",
+                        "install",
+                        "-y",
+                        "git",
+                        "python3-dev",
+                        "python3-pip",
+                        "python3-venv",
+                        "wget",
+                    ],
+                    cwd=PosixPath("/root/lpcraft/project"),
+                    env={"CONDA_ENV": "lpci"},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+                call(
+                    [
+                        "bash",
+                        "--noprofile",
+                        "--norc",
+                        "-ec",
+                        pre_run_command,
+                    ],
+                    cwd=PosixPath("/root/lpcraft/project"),
+                    env={"CONDA_ENV": "lpci"},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+                call(
+                    [
+                        "bash",
+                        "--noprofile",
+                        "--norc",
+                        "-ec",
+                        run_command,
+                    ],
+                    cwd=PosixPath("/root/lpcraft/project"),
+                    env={"CONDA_ENV": "lpci"},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+                call(
+                    [
+                        "bash",
+                        "--noprofile",
+                        "--norc",
+                        "-ec",
+                        post_run_command,
+                    ],
+                    cwd=PosixPath("/root/lpcraft/project"),
+                    env={"CONDA_ENV": "lpci"},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+            ],
+            execute_run.call_args_list,
+        )
+
+    def test_miniconda_plugin_works_without_plugin_settings(self):
+        config = dedent(
+            """
+            pipeline:
+                - build
+
+            jobs:
+                build:
+                    series: focal
+                    architectures: amd64
+                    plugin: miniconda
+                    run: |
+                        pip install --upgrade pytest
+        """
+        )
+        config_path = Path(".launchpad.yaml")
+        config_path.write_text(config)
+        config_obj = lpcraft.config.Config.load(config_path)
+        self.assertEqual(config_obj.jobs["build"][0].plugin, "miniconda")
+        pm = get_plugin_manager(config_obj.jobs["build"][0])
+        plugins = pm.get_plugins()
+        plugin_match = [
+            _ for _ in plugins if _.__class__.__name__ == "MiniCondaPlugin"
+        ]
+        self.assertEqual(
+            [
+                "defaults",
+            ],
+            plugin_match[0].conda_channels,
+        )
+        self.assertEqual(["PYTHON=3.8", "pip"], plugin_match[0].conda_packages)
diff --git a/lpcraft/plugins/plugins.py b/lpcraft/plugins/plugins.py
index 9117da8..c114030 100644
--- a/lpcraft/plugins/plugins.py
+++ b/lpcraft/plugins/plugins.py
@@ -3,11 +3,13 @@
 
 from __future__ import annotations  # isort:skip
 
-__all__ = ["ToxPlugin", "PyProjectBuildPlugin"]
+__all__ = ["ToxPlugin", "PyProjectBuildPlugin", "MiniCondaPlugin"]
 
-from typing import TYPE_CHECKING
+import textwrap
+from typing import TYPE_CHECKING, ClassVar, List, Optional, cast
 
 import pydantic
+from pydantic import StrictStr
 
 from lpcraft.plugin import hookimpl
 from lpcraft.plugins import register
@@ -95,3 +97,119 @@ class PyProjectBuildPlugin(BasePlugin):
     def lpcraft_execute_run(self) -> str:
         # XXX jugmac00 2022-01-20: we should consider using a PPA
         return "python3 -m pip install build==0.7.0; python3 -m build"
+
+
+@register(name="miniconda")
+class MiniCondaPlugin(BasePlugin):
+    """Installs `miniconda3` and resets the environment.
+
+    Usage:
+        In `.launchpad.yaml`, create the following structure:
+
+        .. code-block:: yaml
+
+           jobs:
+               myjob:
+                  plugin: miniconda
+                  conda-packages:
+                    - mamba
+                    - numpy=1.17
+                    - scipy
+                    - pip
+                  conda-python: 3.8
+                  run: |
+                    conda install ....
+                    pip install --upgrade pytest
+                    python -m build .
+    """
+
+    class Config(BaseConfig):
+        conda_packages: Optional[List[StrictStr]]
+        conda_python: Optional[StrictStr]
+        conda_channels: Optional[List[StrictStr]]
+
+        @pydantic.validator("conda_python", pre=True)
+        def validate_conda_python(cls, v: str | float | int) -> str:
+            return str(v)
+
+    INTERPOLATES_RUN_COMMAND = True
+    DEFAULT_CONDA_PACKAGES: ClassVar[tuple[str, ...]] = ("pip",)
+    DEFAULT_CONDA_PYTHON = "3.8"
+    DEFAULT_CONDA_CHANNELS = ("defaults",)
+
+    def get_plugin_config(self) -> "MiniCondaPlugin.Config":
+        return cast(MiniCondaPlugin.Config, self.config.plugin_config)
+
+    @property
+    def conda_packages(self) -> list[str]:
+        conda_packages: set[str] = set()
+        conda_python = self.DEFAULT_CONDA_PYTHON
+        plugin_config = self.get_plugin_config()
+        if plugin_config.conda_python:
+            conda_python = plugin_config.conda_python
+        if plugin_config.conda_packages:
+            conda_packages.update(set(plugin_config.conda_packages))
+        conda_packages.add(f"PYTHON={conda_python}")
+        conda_packages.update(self.DEFAULT_CONDA_PACKAGES)
+        return sorted(conda_packages)
+
+    @property
+    def conda_channels(self) -> list[str]:
+        conda_channels: list[str] = []
+        plugin_config = self.get_plugin_config()
+        if plugin_config.conda_channels:
+            conda_channels.extend(plugin_config.conda_channels)
+        for channel in set(self.DEFAULT_CONDA_CHANNELS) - set(conda_channels):
+            conda_channels.append(channel)
+        return conda_channels
+
+    @hookimpl  # type: ignore
+    def lpcraft_set_environment(self) -> dict[str, str]:
+        # `CONDA_ENV` sets the name of the Conda virtual environment
+        return {"CONDA_ENV": "lpci"}
+
+    @hookimpl  # type: ignore
+    def lpcraft_install_packages(self) -> list[str]:
+        return [
+            "git",
+            "python3-dev",
+            "python3-pip",
+            "python3-venv",
+            "wget",
+        ]
+
+    @hookimpl  # type: ignore
+    def lpcraft_execute_before_run(self) -> str:
+        run = self.config.run_before or ""
+        conda_channels = " ".join(f"-c {_}" for _ in self.conda_channels)
+        return textwrap.dedent(
+            f"""
+        if [ ! -d "$HOME/miniconda3" ]; then
+            wget -O /tmp/miniconda.sh https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
+            chmod +x /tmp/miniconda.sh
+            /tmp/miniconda.sh -b
+        fi
+        export PATH=$HOME/miniconda3/bin:$PATH
+        conda remove --all -q -y -n $CONDA_ENV
+        conda create -n $CONDA_ENV -q -y {conda_channels} {' '.join(self.conda_packages)}
+        source activate $CONDA_ENV
+        {run}"""  # noqa:E501
+        )
+
+    @hookimpl  # type: ignore
+    def lpcraft_execute_run(self) -> str:
+        run = self.config.run or ""
+        return textwrap.dedent(
+            f"""
+        export PATH=$HOME/miniconda3/bin:$PATH
+        source activate $CONDA_ENV
+        {run}"""
+        )
+
+    @hookimpl  # type: ignore
+    def lpcraft_execute_after_run(self) -> str:
+        run = f"; {self.config.run_after}" if self.config.run_after else ""
+        return (
+            "export PATH=$HOME/miniconda3/bin:$PATH; "
+            f"source activate $CONDA_ENV; conda env export{run}"
+        )

Follow ups