From 93fbc8fe99729b6d0f9a8efa802501f4dc4b7bb6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Apr 2026 20:10:52 +0000
Subject: [PATCH 1/2] Initial plan
From 8b768814c4ab95f12976c0ac0710901577bc67ea Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Apr 2026 20:18:13 +0000
Subject: [PATCH 2/2] Extract _resolve_snap to shared helper in
imagecraft/plugins/_utils.py
Agent-Logs-Url: https://github.com/canonical/imagecraft/sessions/4615f0f5-ebf0-4d3b-86ea-03f5a729dd29
Co-authored-by: lengau <4305943+lengau@users.noreply.github.com>
---
imagecraft/plugins/_utils.py | 32 +++++++++++++++++++
imagecraft/plugins/snap_preseed_plugin.py | 11 ++-----
imagecraft/plugins/uc_prepare_plugin.py | 11 ++-----
tests/unit/plugins/test_utils.py | 38 +++++++++++++++++++++++
4 files changed, 76 insertions(+), 16 deletions(-)
create mode 100644 imagecraft/plugins/_utils.py
create mode 100644 tests/unit/plugins/test_utils.py
diff --git a/imagecraft/plugins/_utils.py b/imagecraft/plugins/_utils.py
new file mode 100644
index 00000000..4ad23cdf
--- /dev/null
+++ b/imagecraft/plugins/_utils.py
@@ -0,0 +1,32 @@
+# This file is part of imagecraft.
+#
+# Copyright 2026 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3, as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see .
+
+"""Shared plugin utilities."""
+
+
+def resolve_snap(snap: str) -> str:
+ """Resolve a snap reference to the format expected by snap prepare-image.
+
+ If the snap reference contains a channel (e.g. ``name/track/risk``), it is
+ converted to the ``name=track/risk`` form that ``snap prepare-image``
+ expects. Plain snap names and local ``.snap`` file paths are returned
+ unchanged.
+ """
+ snap = snap.strip()
+ if "/" in snap and not snap.endswith(".snap"):
+ name, channel = snap.split("/", 1)
+ snap = f"{name}={channel}"
+ return snap
diff --git a/imagecraft/plugins/snap_preseed_plugin.py b/imagecraft/plugins/snap_preseed_plugin.py
index 19b67b32..ede1715d 100644
--- a/imagecraft/plugins/snap_preseed_plugin.py
+++ b/imagecraft/plugins/snap_preseed_plugin.py
@@ -21,6 +21,8 @@
from craft_parts.plugins import Plugin, PluginProperties
from typing_extensions import override
+from ._utils import resolve_snap
+
class SnapPreseedPluginProperties(PluginProperties, frozen=True):
"""Properties for the 'snap-preseed' plugin."""
@@ -77,17 +79,10 @@ def get_build_commands(self) -> list[str]:
)
cmd.extend(
- f"--snap={self._resolve_snap(snap)}" for snap in options.snap_preseed_snaps
+ f"--snap={resolve_snap(snap)}" for snap in options.snap_preseed_snaps
)
cmd.append(
f'"{options.snap_preseed_model_assert}" {self._part_info.part_install_dir}'
)
return [" ".join(cmd)]
-
- def _resolve_snap(self, snap: str) -> str:
- snap = snap.strip()
- if "/" in snap and not snap.endswith(".snap"):
- name, channel = snap.split("/", 1)
- snap = f"{name}={channel}"
- return snap
diff --git a/imagecraft/plugins/uc_prepare_plugin.py b/imagecraft/plugins/uc_prepare_plugin.py
index 4cd15022..71adf7d4 100644
--- a/imagecraft/plugins/uc_prepare_plugin.py
+++ b/imagecraft/plugins/uc_prepare_plugin.py
@@ -22,6 +22,8 @@
from pydantic import model_validator
from typing_extensions import Self, override
+from ._utils import resolve_snap
+
class UcPreparePluginProperties(PluginProperties, frozen=True):
"""Properties for the uc-prepare plugin."""
@@ -118,7 +120,7 @@ def get_build_commands(self) -> list[str]:
)
cmd.extend(
- f"--snap={self._resolve_snap(snap)}" for snap in options.uc_prepare_snaps
+ f"--snap={resolve_snap(snap)}" for snap in options.uc_prepare_snaps
)
cmd.append(
@@ -126,10 +128,3 @@ def get_build_commands(self) -> list[str]:
)
return [" ".join(cmd)]
-
- def _resolve_snap(self, snap: str) -> str:
- snap = snap.strip()
- if "/" in snap and not snap.endswith(".snap"):
- name, channel = snap.split("/", 1)
- snap = f"{name}={channel}"
- return snap
diff --git a/tests/unit/plugins/test_utils.py b/tests/unit/plugins/test_utils.py
new file mode 100644
index 00000000..75709c3d
--- /dev/null
+++ b/tests/unit/plugins/test_utils.py
@@ -0,0 +1,38 @@
+# This file is part of imagecraft.
+#
+# Copyright 2026 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import pytest
+from imagecraft.plugins._utils import resolve_snap
+
+
+@pytest.mark.parametrize(
+ ("snap", "expected"),
+ [
+ # Plain snap name – unchanged
+ ("core24", "core24"),
+ # Leading/trailing whitespace is stripped
+ (" core24 ", "core24"),
+ # Local .snap file path with a slash – unchanged (not a channel)
+ ("./my-snap_1.0_amd64.snap", "./my-snap_1.0_amd64.snap"),
+ # snap name with channel – converted to name=channel
+ ("hello-world/latest/stable", "hello-world=latest/stable"),
+ ("core24/stable", "core24=stable"),
+ # snap name with channel and leading whitespace
+ (" hello-world/latest/stable ", "hello-world=latest/stable"),
+ ],
+)
+def test_resolve_snap(snap, expected):
+ assert resolve_snap(snap) == expected