← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/lpcraft:declare-snap-dependencies-for-jobs into lpcraft:main

 

Jürgen Gmach has proposed merging ~jugmac00/lpcraft:declare-snap-dependencies-for-jobs into lpcraft:main.

Commit message:
Declare snap dependencies for jobs

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/lpcraft/+git/lpcraft/+merge/412478
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/lpcraft:declare-snap-dependencies-for-jobs into lpcraft:main.
diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py
index 02e844a..79637a1 100644
--- a/lpcraft/commands/run.py
+++ b/lpcraft/commands/run.py
@@ -5,6 +5,7 @@ from argparse import Namespace
 from pathlib import Path
 
 from craft_cli import emit
+from craft_providers.actions.snap_installer import install_from_store
 
 from lpcraft import env
 from lpcraft.config import Config
@@ -35,7 +36,7 @@ def run(args: Namespace) -> int:
                     f"does not set 'run'"
                 )
 
-            cmd = ["bash", "--noprofile", "--norc", "-ec", job.run]
+            run_cmd = ["bash", "--noprofile", "--norc", "-ec", job.run]
 
             emit.progress(
                 f"Launching environment for {job.series}/{host_architecture}"
@@ -46,10 +47,22 @@ def run(args: Namespace) -> int:
                 series=job.series,
                 architecture=host_architecture,
             ) as instance:
+                if job.snaps:
+                    emit.progress("Installing snaps")
+                    for snap in job.snaps:
+                        emit.progress(f"Running `snap install` {snap}")
+                        install_from_store(
+                            executor=instance,
+                            snap_name=snap,
+                            channel="stable",
+                            classic=True,
+                        )
+
+                run_cmd = ["bash", "--noprofile", "--norc", "-ec", job.run]
                 emit.progress("Running the job")
-                with emit.open_stream(f"Running {cmd}") as stream:
+                with emit.open_stream(f"Running {run_cmd}") as stream:
                     proc = instance.execute_run(
-                        cmd,
+                        run_cmd,
                         cwd=env.get_managed_environment_project_path(),
                         env=job.environment,
                         stdout=stream,
diff --git a/lpcraft/commands/tests/test_run.py b/lpcraft/commands/tests/test_run.py
index 5b8b6bc..49d9679 100644
--- a/lpcraft/commands/tests/test_run.py
+++ b/lpcraft/commands/tests/test_run.py
@@ -418,3 +418,90 @@ class TestRun(CommandBaseTestCase):
             ],
             execute_run.call_args_list,
         )
+
+    @patch("lpcraft.commands.run.get_provider")
+    @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+    def test_install_snaps(
+        self, mock_get_host_architecture, mock_get_provider
+    ):
+        launcher = Mock(spec=launch)
+        provider = self.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:
+                - test
+
+            jobs:
+                test:
+                    series: focal
+                    architectures: amd64
+                    run: tox
+                    snaps: [chromium, firefox]
+            """
+        )
+        Path(".launchpad.yaml").write_text(config)
+
+        result = self.run_command("run")
+
+        self.assertEqual(0, result.exit_code)
+        self.assertEqual(
+            [
+                call(
+                    [
+                        "snap",
+                        "download",
+                        "chromium",
+                        "--channel=stable",
+                        "--basename=chromium",
+                        "--target-directory=/tmp",
+                    ],
+                    check=True,
+                    capture_output=True,
+                ),
+                call(
+                    [
+                        "snap",
+                        "install",
+                        "/tmp/chromium.snap",
+                        "--classic",
+                        "--dangerous",
+                    ],
+                    check=True,
+                    capture_output=True,
+                ),
+                call(
+                    [
+                        "snap",
+                        "download",
+                        "firefox",
+                        "--channel=stable",
+                        "--basename=firefox",
+                        "--target-directory=/tmp",
+                    ],
+                    check=True,
+                    capture_output=True,
+                ),
+                call(
+                    [
+                        "snap",
+                        "install",
+                        "/tmp/firefox.snap",
+                        "--classic",
+                        "--dangerous",
+                    ],
+                    check=True,
+                    capture_output=True,
+                ),
+                call(
+                    ["bash", "--noprofile", "--norc", "-ec", "tox"],
+                    cwd=Path("/root/project"),
+                    env=None,
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+            ],
+            execute_run.call_args_list,
+        )
diff --git a/lpcraft/config.py b/lpcraft/config.py
index e4011f3..92c868e 100644
--- a/lpcraft/config.py
+++ b/lpcraft/config.py
@@ -27,6 +27,7 @@ class Job(ModelConfigDefaults):
     architectures: List[StrictStr]
     run: Optional[StrictStr]
     environment: Optional[Dict[str, Optional[str]]]
+    snaps: Optional[List[StrictStr]]
 
     @pydantic.validator("architectures", pre=True)
     def validate_architectures(
@@ -36,6 +37,14 @@ class Job(ModelConfigDefaults):
             v = [v]
         return v
 
+    @pydantic.validator("snaps", pre=True)
+    def validate_snaps(
+        cls, v: Optional[Union[StrictStr, List[StrictStr]]]
+    ) -> Optional[List[StrictStr]]:
+        if isinstance(v, str):
+            v = [v]
+        return v
+
 
 def _expand_job_values(
     values: Dict[StrictStr, Any]
diff --git a/lpcraft/tests/test_config.py b/lpcraft/tests/test_config.py
index 16649ad..536bcee 100644
--- a/lpcraft/tests/test_config.py
+++ b/lpcraft/tests/test_config.py
@@ -150,3 +150,58 @@ class TestConfig(TestCase):
         self.assertEqual(
             {"ACTIVE": "1", "SKIP": "0"}, config.jobs["test"][0].environment
         )
+
+    def test_load_single_snap(self):
+        # A single snap can be written as a string, and is automatically
+        # wrapped in a list.
+        path = self.create_config(
+            dedent(
+                """
+                pipeline:
+                    - test
+
+                jobs:
+                    test:
+                        series: focal
+                        architectures: amd64
+                        snaps: chromium
+                """
+            )
+        )
+        config = Config.load(path)
+        self.assertEqual(["chromium"], config.jobs["test"][0].snaps)
+
+    def test_load_multiple_snaps(self):
+        path = self.create_config(
+            dedent(
+                """
+                pipeline:
+                    - test
+
+                jobs:
+                    test:
+                        series: focal
+                        architectures: amd64
+                        snaps: [chromium, firefox]
+                """
+            )
+        )
+        config = Config.load(path)
+        self.assertEqual(["chromium", "firefox"], config.jobs["test"][0].snaps)
+
+    def test_load_config_without_snaps(self):
+        path = self.create_config(
+            dedent(
+                """
+                pipeline:
+                    - test
+
+                jobs:
+                    test:
+                        series: focal
+                        architectures: amd64
+                """
+            )
+        )
+        config = Config.load(path)
+        self.assertEqual(None, config.jobs["test"][0].snaps)