diff --git a/snapcraft/__init__.py b/snapcraft/__init__.py
index 6c9a0a516e..a3da6b7da8 100644
--- a/snapcraft/__init__.py
+++ b/snapcraft/__init__.py
@@ -20,6 +20,9 @@
import pkg_resources
+# For legacy compatibility
+import snapcraft.sources # noqa: F401
+
def _get_version():
if os.environ.get("SNAP_NAME") == "snapcraft":
diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py
index 5b776dd252..b3c7a57c52 100644
--- a/snapcraft/parts/lifecycle.py
+++ b/snapcraft/parts/lifecycle.py
@@ -234,6 +234,9 @@ def _run_lifecycle_and_pack(
step_name,
shell=getattr(parsed_args, "shell", False),
shell_after=getattr(parsed_args, "shell_after", False),
+ # Repriming needs to happen to take into account any changes to
+ # the actual target directory.
+ rerun_step=command_name == "try",
)
# Extract metadata and generate snap.yaml
diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py
index be2d95072b..24be7f1baf 100644
--- a/snapcraft/parts/parts.py
+++ b/snapcraft/parts/parts.py
@@ -141,10 +141,14 @@ def run(
*,
shell: bool = False,
shell_after: bool = False,
+ rerun_step: bool = False,
) -> None:
"""Run the parts lifecycle.
:param target_step: The final step to execute.
+ :param shell: Enter a shell instead of running step_name.
+ :param shell_after: Enter a shell after running step_name.
+ :param rerun_step: Force running step_name.
:raises PartsLifecycleError: On error during lifecycle.
:raises RuntimeError: On unexpected error.
@@ -171,6 +175,16 @@ def run(
with self._lcm.action_executor() as aex:
for action in actions:
+ # Workaround until canonical/craft-parts#540 is fixed
+ if action.step == target_step and rerun_step:
+ action = craft_parts.Action(
+ part_name=action.part_name,
+ step=action.step,
+ action_type=ActionType.RERUN,
+ reason="forced rerun",
+ project_vars=action.project_vars,
+ properties=action.properties,
+ )
message = _action_message(action)
emit.progress(f"Executing parts lifecycle: {message}")
with emit.open_stream("Executing action") as stream:
diff --git a/snapcraft/projects.py b/snapcraft/projects.py
index 1c822efb32..f207436935 100644
--- a/snapcraft/projects.py
+++ b/snapcraft/projects.py
@@ -187,7 +187,9 @@ def _validate_list_stream(cls, listen_stream):
f"{listen_stream!r} is not an integer between 1 and 65535 (inclusive)."
)
elif isinstance(listen_stream, str):
- if not re.match(r"^[A-Za-z0-9/._#:$-]*$", listen_stream):
+ if not listen_stream.startswith("@snap.") and not re.match(
+ r"^[A-Za-z0-9/._#:$-]*$", listen_stream
+ ):
raise ValueError(
f"{listen_stream!r} is not a valid socket path (e.g. /tmp/mysocket.sock)."
)
diff --git a/snapcraft/sources.py b/snapcraft/sources.py
new file mode 100644
index 0000000000..455aa612a4
--- /dev/null
+++ b/snapcraft/sources.py
@@ -0,0 +1,26 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 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 .
+
+"""Legacy support for local plugins."""
+
+import sys as _sys
+
+if _sys.platform == "linux":
+ from snapcraft_legacy.sources import get
+
+ __all__ = [
+ "get",
+ ]
diff --git a/tests/spread/core22/set-version-twice/modified-snapcraft.yaml b/tests/spread/core22/set-version-twice/modified-snapcraft.yaml
new file mode 100644
index 0000000000..c002a7f939
--- /dev/null
+++ b/tests/spread/core22/set-version-twice/modified-snapcraft.yaml
@@ -0,0 +1,24 @@
+name: test-set-version-twice
+base: core22
+version: '0.1'
+summary: Test fix for set version and out of order step execution
+description: |
+ As described in https://bugs.launchpad.net/snapcraft/+bug/1831135/comments/10,
+ a bug in craft-parts caused unexpected double setting of project variables
+ such as `version`. Make sure this scenario builds correctly.
+adopt-info: part1
+
+grade: devel
+confinement: devmode
+
+parts:
+ part1:
+ plugin: nil
+ override-pull: |
+ craftctl default
+ craftctl set version=xx
+ echo
+
+ part2:
+ plugin: nil
+ after: [part1]
diff --git a/tests/spread/core22/set-version-twice/original-snapcraft.yaml b/tests/spread/core22/set-version-twice/original-snapcraft.yaml
new file mode 100644
index 0000000000..48af83e031
--- /dev/null
+++ b/tests/spread/core22/set-version-twice/original-snapcraft.yaml
@@ -0,0 +1,23 @@
+name: test-set-version-twice
+base: core22
+version: '0.1'
+summary: Test fix for set version and out of order step execution
+description: |
+ As described in https://bugs.launchpad.net/snapcraft/+bug/1831135/comments/10,
+ a bug in craft-parts caused unexpected double setting of project variables
+ such as `version`. Make sure this scenario builds correctly.
+adopt-info: part1
+
+grade: devel
+confinement: devmode
+
+parts:
+ part1:
+ plugin: nil
+ override-pull: |
+ craftctl default
+ craftctl set version=xx
+
+ part2:
+ plugin: nil
+ after: [part1]
diff --git a/tests/spread/core22/set-version-twice/task.yaml b/tests/spread/core22/set-version-twice/task.yaml
new file mode 100644
index 0000000000..bf756d072b
--- /dev/null
+++ b/tests/spread/core22/set-version-twice/task.yaml
@@ -0,0 +1,29 @@
+summary: Test fix for version setting corner case
+
+prepare: |
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+
+restore: |
+ snapcraft clean
+ rm -Rf subdir ./*.snap
+ rm -f snap/*.yaml
+
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+
+execute: |
+ mkdir -p snap
+ cp original-snapcraft.yaml snap/snapcraft.yaml
+
+ snapcraft prime
+
+ cp modified-snapcraft.yaml snap/snapcraft.yaml
+
+ snapcraft build part2
+ snapcraft prime
+
+ cp original-snapcraft.yaml snap/snapcraft.yaml
+
+ snapcraft build part2
+ snapcraft prime
diff --git a/tests/spread/core22/try/task.yaml b/tests/spread/core22/try/task.yaml
index c2e9bb2418..5dff360e1c 100644
--- a/tests/spread/core22/try/task.yaml
+++ b/tests/spread/core22/try/task.yaml
@@ -7,6 +7,9 @@ execute: |
chmod a+w prime
unset SNAPCRAFT_BUILD_ENVIRONMENT
+ # Prime first to regression test snapcore/snapcraft#4219
+ snapcraft prime --use-lxd
+ # Followed by the actual try
snapcraft try --use-lxd
find prime/meta/snap.yaml
@@ -14,4 +17,4 @@ execute: |
snap try prime
hello-try | MATCH "Hello, world"
- snap remove hello-try
\ No newline at end of file
+ snap remove hello-try
diff --git a/tests/spread/plugins/v1/x-local/snaps/source-get/snap/plugins/x_local_plugin.py b/tests/spread/plugins/v1/x-local/snaps/source-get/snap/plugins/x_local_plugin.py
new file mode 100644
index 0000000000..1da6ecbeb0
--- /dev/null
+++ b/tests/spread/plugins/v1/x-local/snaps/source-get/snap/plugins/x_local_plugin.py
@@ -0,0 +1,43 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 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 snapcraft
+from snapcraft.plugins.v1 import PluginV1
+
+
+class LocalPlugin(PluginV1):
+ @classmethod
+ def schema(cls):
+ schema = super().schema()
+
+ schema["properties"]["foo"] = {"type": "string"}
+
+ return schema
+
+ @classmethod
+ def get_pull_properties(cls):
+ return ["foo", "stage-packages"]
+
+ @classmethod
+ def get_build_properties(cls):
+ return ["foo", "stage-packages"]
+
+ def pull(self):
+ super().pull()
+ print(snapcraft.sources.get)
+
+ def build(self):
+ return self.run(["touch", "build-stamp"], self.installdir)
diff --git a/tests/spread/plugins/v1/x-local/snaps/source-get/snap/snapcraft.yaml b/tests/spread/plugins/v1/x-local/snaps/source-get/snap/snapcraft.yaml
new file mode 100644
index 0000000000..66ea5d7b9c
--- /dev/null
+++ b/tests/spread/plugins/v1/x-local/snaps/source-get/snap/snapcraft.yaml
@@ -0,0 +1,12 @@
+name: test-local-plugins
+base: core18
+version: "0.1"
+summary: local plugin using snapcraft.sources.get
+description: Tests if local plugins load and can build
+confinement: strict
+grade: devel
+
+parts:
+ x-local-plugin:
+ plugin: x-local-plugin
+ source: .
diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py
index a03079339d..c643149013 100644
--- a/tests/unit/parts/test_lifecycle.py
+++ b/tests/unit/parts/test_lifecycle.py
@@ -299,13 +299,45 @@ def test_lifecycle_run_command_step(
parsed_args=parsed_args,
)
- call_args = {"shell": False, "shell_after": False}
+ call_args = {"shell": False, "shell_after": False, "rerun_step": False}
if debug_shell:
call_args[debug_shell] = True
assert run_mock.mock_calls == [call(step, **call_args)]
+def test_lifecycle_run_try_command(snapcraft_yaml, project_vars, new_dir, mocker):
+ project = Project.unmarshal(snapcraft_yaml(base="core22"))
+ run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run")
+ mocker.patch("snapcraft.meta.snap_yaml.write")
+ mocker.patch("snapcraft.pack.pack_snap")
+
+ parsed_args = argparse.Namespace(
+ debug=False,
+ destructive_mode=True,
+ enable_manifest=False,
+ shell=False,
+ shell_after=False,
+ use_lxd=False,
+ ua_token=None,
+ parts=[],
+ )
+
+ parts_lifecycle._run_command(
+ "try",
+ project=project,
+ parse_info={},
+ assets_dir=Path(),
+ start_time=datetime.now(),
+ parallel_build_count=8,
+ parsed_args=parsed_args,
+ )
+
+ assert run_mock.mock_calls == [
+ call("prime", shell=False, shell_after=False, rerun_step=True)
+ ]
+
+
@pytest.mark.parametrize("managed_mode", [True, False])
@pytest.mark.parametrize("build_env", [None, "host", "multipass", "lxd", "other"])
@pytest.mark.parametrize("cmd", ["pack", "snap"])
@@ -356,7 +388,9 @@ def test_lifecycle_run_local_destructive_mode(
)
assert run_in_provider_mock.mock_calls == []
- assert run_mock.mock_calls == [call("prime", shell=False, shell_after=False)]
+ assert run_mock.mock_calls == [
+ call("prime", shell=False, shell_after=False, rerun_step=False)
+ ]
assert pack_mock.mock_calls[:1] == [
call(
new_dir / "home/prime" if managed_mode else new_dir / "prime",
@@ -419,7 +453,9 @@ def test_lifecycle_run_local_managed_mode(
)
assert run_in_provider_mock.mock_calls == []
- assert run_mock.mock_calls == [call("prime", shell=False, shell_after=False)]
+ assert run_mock.mock_calls == [
+ call("prime", shell=False, shell_after=False, rerun_step=False)
+ ]
assert pack_mock.mock_calls[:1] == [
call(
new_dir / "home/prime",
@@ -482,7 +518,9 @@ def test_lifecycle_run_local_build_env(
)
assert run_in_provider_mock.mock_calls == []
- assert run_mock.mock_calls == [call("prime", shell=False, shell_after=False)]
+ assert run_mock.mock_calls == [
+ call("prime", shell=False, shell_after=False, rerun_step=False)
+ ]
assert pack_mock.mock_calls[:1] == [
call(
new_dir / "home/prime" if managed_mode else new_dir / "prime",
@@ -656,7 +694,9 @@ def test_lifecycle_pack_metadata_error(cmd, snapcraft_yaml, new_dir, mocker):
assert str(raised.value) == (
"error setting grade: unexpected value; permitted: 'stable', 'devel'"
)
- assert run_mock.mock_calls == [call("prime", shell=False, shell_after=False)]
+ assert run_mock.mock_calls == [
+ call("prime", shell=False, shell_after=False, rerun_step=False)
+ ]
assert pack_mock.mock_calls == []
diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py
index 48c5ccbb03..627400836f 100644
--- a/tests/unit/test_projects.py
+++ b/tests/unit/test_projects.py
@@ -1084,7 +1084,9 @@ def test_app_command_chain(self, command_chain, app_yaml_data):
assert project.apps is not None
assert project.apps["app1"].command_chain == command_chain
- @pytest.mark.parametrize("listen_stream", [1, 100, 65535, "/tmp/mysocket.sock"])
+ @pytest.mark.parametrize(
+ "listen_stream", [1, 100, 65535, "/tmp/mysocket.sock", "@snap.foo"]
+ )
def test_app_sockets_valid_listen_stream(self, listen_stream, socket_yaml_data):
data = socket_yaml_data(listen_stream=listen_stream)
@@ -1094,13 +1096,25 @@ def test_app_sockets_valid_listen_stream(self, listen_stream, socket_yaml_data):
assert project.apps["app1"].sockets["socket1"].listen_stream == listen_stream
@pytest.mark.parametrize("listen_stream", [-1, 0, 65536])
- def test_app_sockets_invalid_listen_stream(self, listen_stream, socket_yaml_data):
+ def test_app_sockets_invalid_int_listen_stream(
+ self, listen_stream, socket_yaml_data
+ ):
data = socket_yaml_data(listen_stream=listen_stream)
error = f".*{listen_stream} is not an integer between 1 and 65535"
with pytest.raises(errors.ProjectValidationError, match=error):
Project.unmarshal(data)
+ @pytest.mark.parametrize("listen_stream", ["@foo"])
+ def test_app_sockets_invalid_socket_listen_stream(
+ self, listen_stream, socket_yaml_data
+ ):
+ data = socket_yaml_data(listen_stream=listen_stream)
+
+ error = f".*{listen_stream!r} is not a valid socket path.*"
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ Project.unmarshal(data)
+
def test_app_sockets_missing_listen_stream(self, socket_yaml_data):
data = socket_yaml_data()