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