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()