From c52d04e696ed03b6584c2d06e8e1654db956863c Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Wed, 23 Oct 2024 07:37:29 -0500 Subject: [PATCH 1/6] build(deps): bump craft-application, craft-cli Signed-off-by: Callahan Kovacs --- requirements-devel.txt | 6 +++--- requirements-docs.txt | 6 +++--- requirements.txt | 6 +++--- setup.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index b34cf5f2ea..a25bfd3a2f 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -24,9 +24,9 @@ click==8.1.7 codespell==2.3.0 colorama==0.4.6 coverage==7.6.1 -craft-application==4.2.6 +git+https://github.com/canonical/craft-application@work/CRAFT-3543-use-app-commands#egg=craft-application craft-archives==2.0.0 -craft-cli==2.7.0 +craft-cli==2.9.0 craft-grammar==2.0.1 craft-parts==2.1.2 craft-platforms==0.4.0 @@ -89,7 +89,7 @@ pbr==6.0.0 pexpect==4.9.0 plaster==1.1.2 plaster-pastedeploy==1.0.1 -platformdirs==4.2.2 +platformdirs==4.3.6 pluggy==1.5.0 polib==1.2.0 progressbar==2.5 diff --git a/requirements-docs.txt b/requirements-docs.txt index 3c0d37570a..e6e90a3638 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -19,9 +19,9 @@ chardet==5.2.0 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 -craft-application==4.2.6 +git+https://github.com/canonical/craft-application@work/CRAFT-3543-use-app-commands#egg=craft-application craft-archives==2.0.0 -craft-cli==2.7.0 +craft-cli==2.9.0 craft-grammar==2.0.1 craft-parts==2.1.2 craft-platforms==0.4.0 @@ -69,7 +69,7 @@ natsort==8.4.0 oauthlib==3.2.2 overrides==7.7.0 packaging==24.1 -platformdirs==4.2.2 +platformdirs==4.3.6 polib==1.2.0 progressbar==2.5 protobuf==5.27.3 diff --git a/requirements.txt b/requirements.txt index f8f5854405..37aacbc663 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,9 +7,9 @@ cffi==1.17.1 chardet==5.2.0 charset-normalizer==3.3.2 click==8.1.7 -craft-application==4.2.6 +git+https://github.com/canonical/craft-application@work/CRAFT-3543-use-app-commands#egg=craft-application craft-archives==2.0.0 -craft-cli==2.7.0 +craft-cli==2.9.0 craft-grammar==2.0.1 craft-parts==2.1.2 craft-platforms==0.4.0 @@ -37,7 +37,7 @@ mypy-extensions==1.0.0 oauthlib==3.2.2 overrides==7.7.0 packaging==24.1 -platformdirs==4.2.2 +platformdirs==4.3.6 progressbar==2.5 protobuf==5.27.3 psutil==6.0.0 diff --git a/setup.py b/setup.py index 275446acd9..7264a6be23 100755 --- a/setup.py +++ b/setup.py @@ -98,9 +98,9 @@ def recursive_data_files(directory, install_directory): "attrs", "catkin-pkg; sys_platform == 'linux'", "click", - "craft-application>=4.2.6,<5.0.0", + "craft-application @ git+https://github.com/canonical/craft-application@work/CRAFT-3543-use-app-commands#egg=craft-application", "craft-archives~=2.0", - "craft-cli~=2.6", + "craft-cli~=2.9", "craft-grammar>=2.0.1,<3.0.0", "craft-parts>=2.1.2,<3.0.0", "craft-platforms~=0.4", From 57d432599b314cd9acb150f75017dc15e295e61d Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Wed, 23 Oct 2024 09:41:57 -0500 Subject: [PATCH 2/6] refactor: use init command from craft-application Signed-off-by: Callahan Kovacs --- MANIFEST.in | 1 + setup.py | 4 + snapcraft/application.py | 12 +- snapcraft/commands/init.py | 74 ++++-------- snapcraft/extensions/env_injector.py | 2 +- snapcraft/parts/yaml_utils.py | 12 +- .../templates/simple/snap/snapcraft.yaml | 17 +++ tests/spread/general/init/task.yaml | 30 ++++- tests/unit/commands/test_init.py | 113 ++++++++++++------ tests/unit/extensions/test_env_injector.py | 2 +- tests/unit/parts/test_yaml_utils.py | 13 ++ tools/docs/gen_cli_docs.py | 4 +- 12 files changed, 178 insertions(+), 106 deletions(-) create mode 100644 snapcraft/templates/simple/snap/snapcraft.yaml diff --git a/MANIFEST.in b/MANIFEST.in index f24832561c..b09a917f23 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include schema/* include extensions/* +recursive-include snapcraft/templates * diff --git a/setup.py b/setup.py index 7264a6be23..ab3fc38e3d 100755 --- a/setup.py +++ b/setup.py @@ -174,6 +174,10 @@ def recursive_data_files(directory, install_directory): + recursive_data_files("keyrings", "share/snapcraft") + recursive_data_files("extensions", "share/snapcraft") ), + include_package_data=True, + package_data={ + "snapcraft": ["templates/*"], + }, python_requires=">=3.10", install_requires=install_requires, extras_require=extras_requires, diff --git a/snapcraft/application.py b/snapcraft/application.py index 0b0f96281c..51e1037727 100644 --- a/snapcraft/application.py +++ b/snapcraft/application.py @@ -150,16 +150,6 @@ def _configure_services(self, provider_name: str | None) -> None: super()._configure_services(provider_name) - @property - def command_groups(self): - """Replace craft-application's LifecycleCommand group.""" - _command_groups = super().command_groups - for index, command_group in enumerate(_command_groups): - if command_group.name == "Lifecycle": - _command_groups[index] = cli.CORE24_LIFECYCLE_COMMAND_GROUP - - return _command_groups - @override def _resolve_project_path(self, project_dir: pathlib.Path | None) -> pathlib.Path: """Overridden to handle the two possible locations for snapcraft.yaml.""" @@ -467,7 +457,7 @@ def create_app() -> Snapcraft: services=snapcraft_services, ) - for group in cli.COMMAND_GROUPS: + for group in [cli.CORE24_LIFECYCLE_COMMAND_GROUP, *cli.COMMAND_GROUPS]: app.add_command_group(group.name, group.commands) return app diff --git a/snapcraft/commands/init.py b/snapcraft/commands/init.py index e968d6ee70..89dac23186 100644 --- a/snapcraft/commands/init.py +++ b/snapcraft/commands/init.py @@ -16,74 +16,44 @@ """Snapcraft init command.""" -from pathlib import Path -from textwrap import dedent +import argparse -from craft_application.commands import AppCommand -from craft_cli import emit -from overrides import overrides +import craft_application.commands +import craft_cli +from typing_extensions import override from snapcraft import errors from snapcraft.parts.yaml_utils import get_snap_project -_TEMPLATE_YAML = dedent( - """\ - name: my-snap-name # you probably want to 'snapcraft register ' - base: core24 # the base snap is the execution environment for this snap - version: '0.1' # just for humans, typically '1.2+git' or '1.3.2' - summary: Single-line elevator pitch for your amazing snap # 79 char long summary - description: | - This is my-snap's description. You have a paragraph or two to tell the - most important story about your snap. Keep it under 100 words though, - we live in tweetspace and your description wants to look good in the snap - store. - grade: devel # must be 'stable' to release into candidate/stable channels - confinement: devmode # use 'strict' once you have the right plugs and slots +class InitCommand(craft_application.commands.InitCommand): + """Snapcraft init command.""" - parts: - my-part: - # See 'snapcraft plugins' - plugin: nil - """ -) + @override + def run(self, parsed_args: argparse.Namespace) -> None: + """Pack a directory or run the lifecycle and pack all artifacts.""" + project_dir = self._get_project_dir(parsed_args) + if parsed_args.name: + craft_cli.emit.progress( + "Ignoring '--name' parameter because it is not supported yet.", + permanent=True, + ) -class InitCommand(AppCommand): - """Initialize a snapcraft project.""" - - name = "init" - help_msg = "Initialize a snapcraft project." - overview = "Initialize a snapcraft project in the current directory." - - @overrides - def run(self, parsed_args): - """Initialize a snapcraft project in the current directory. - - :raises SnapcraftError: If a snapcraft.yaml already exists. - """ - emit.progress("Checking for an existing 'snapcraft.yaml'.") - - # if a project is found, then raise an error try: - project = get_snap_project() + craft_cli.emit.progress("Checking for an existing 'snapcraft.yaml'.") + project = get_snap_project(project_dir) raise errors.SnapcraftError( - "could not initialize a new snapcraft project because " + "could not initialise a new snapcraft project because " f"{str(project.project_file)!r} already exists" ) - # the `ProjectMissing` error means a new project can be initialized + # the `ProjectMissing` error means a new project can be initialized except errors.ProjectMissing: - emit.progress("Could not find an existing 'snapcraft.yaml'.") - - snapcraft_yaml_path = Path("snap/snapcraft.yaml") - - emit.progress(f"Creating {str(snapcraft_yaml_path)!r}.") + craft_cli.emit.progress("Could not find an existing 'snapcraft.yaml'.") - snapcraft_yaml_path.parent.mkdir(exist_ok=True) - snapcraft_yaml_path.write_text(_TEMPLATE_YAML, encoding="utf-8") + super().run(parsed_args) - emit.message(f"Created {str(snapcraft_yaml_path)!r}.") - emit.message( + craft_cli.emit.message( "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " "information about the snapcraft.yaml format." ) diff --git a/snapcraft/extensions/env_injector.py b/snapcraft/extensions/env_injector.py index 8c24222241..7e99e53d9c 100644 --- a/snapcraft/extensions/env_injector.py +++ b/snapcraft/extensions/env_injector.py @@ -106,7 +106,7 @@ def get_parts_snippet(self) -> Dict[str, Any]: cargo build --target {toolchain} --release mkdir -p $SNAPCRAFT_PART_INSTALL/bin/command-chain - + cp target/{toolchain}/release/env-exporter $SNAPCRAFT_PART_INSTALL/bin/command-chain """, } diff --git a/snapcraft/parts/yaml_utils.py b/snapcraft/parts/yaml_utils.py index 0480a916f8..f91bb76d81 100644 --- a/snapcraft/parts/yaml_utils.py +++ b/snapcraft/parts/yaml_utils.py @@ -218,13 +218,21 @@ def apply_yaml( return yaml_data -def get_snap_project() -> _SnapProject: +def get_snap_project(project_dir: Path | None = None) -> _SnapProject: """Find the snapcraft.yaml to load. + :param project_dir: The directory to search for the project yaml file. If not + provided, the current working directory is used. + :raises SnapcraftError: if the project yaml file cannot be found. """ for snap_project in _SNAP_PROJECT_FILES: - if snap_project.project_file.exists(): + if project_dir: + snap_project_path = project_dir / snap_project.project_file + else: + snap_project_path = snap_project.project_file + + if snap_project_path.exists(): return snap_project raise errors.ProjectMissing() diff --git a/snapcraft/templates/simple/snap/snapcraft.yaml b/snapcraft/templates/simple/snap/snapcraft.yaml new file mode 100644 index 0000000000..4ef5b5f2d6 --- /dev/null +++ b/snapcraft/templates/simple/snap/snapcraft.yaml @@ -0,0 +1,17 @@ +name: my-snap-name # you probably want to 'snapcraft register ' +base: core24 # the base snap is the execution environment for this snap +version: '0.1' # just for humans, typically '1.2+git' or '1.3.2' +summary: Single-line elevator pitch for your amazing snap # 79 char long summary +description: | + This is my-snap's description. You have a paragraph or two to tell the + most important story about your snap. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the snap + store. + +grade: devel # must be 'stable' to release into candidate/stable channels +confinement: devmode # use 'strict' once you have the right plugs and slots + +parts: + my-part: + # See 'snapcraft plugins' + plugin: nil diff --git a/tests/spread/general/init/task.yaml b/tests/spread/general/init/task.yaml index d66d8b328d..8c69c91b6e 100644 --- a/tests/spread/general/init/task.yaml +++ b/tests/spread/general/init/task.yaml @@ -1,14 +1,40 @@ summary: Run snapcraft init +environment: + PROFILE/default_profile: null + PROFILE/simple_profile: simple + PROFILE: null + PROJECT_DIR/default_dir: null + PROJECT_DIR/project_dir: test-project-dir + PROJECT_DIR: null + restore: | + if [[ -n "$PROJECT_DIR" ]]; then + cd "$PROJECT_DIR" + fi + rm -f ./*.snap + rm -rf ./snap execute: | # unset SNAPCRAFT_BUILD_ENVIRONMENT=host unset SNAPCRAFT_BUILD_ENVIRONMENT - # initialize a new snapcraft project - snapcraft init + args=("init") + + if [[ -n "$PROFILE" ]]; then + args+=("--profile" "$PROFILE") + fi + + if [[ -n "$PROJECT_DIR" ]]; then + args+=("$PROJECT_DIR") + fi + + snapcraft "${args[@]}" + + if [[ -n "$PROJECT_DIR" ]]; then + cd "$PROJECT_DIR" + fi # the base should be core24 grep "^base: core24" snap/snapcraft.yaml diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py index 2398ee3915..d0c6c2e75c 100644 --- a/tests/unit/commands/test_init.py +++ b/tests/unit/commands/test_init.py @@ -14,28 +14,37 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import pathlib import sys from pathlib import Path from textwrap import dedent -from unittest.mock import call import pytest -from snapcraft import cli +from snapcraft import application from snapcraft.models.project import Project from snapcraft.parts.yaml_utils import _SNAP_PROJECT_FILES, apply_yaml, process_yaml -@pytest.fixture(autouse=True) -def mock_argv(mocker): - return mocker.patch.object(sys, "argv", ["snapcraft", "init"]) - - -def test_init_default(emitter, new_dir): +@pytest.mark.parametrize("profile", [None, "simple"]) +@pytest.mark.parametrize("name", [None, "test-snap-name"]) +@pytest.mark.parametrize("project_dir", [None, "test-project-dir"]) +def test_init_default(profile, name, project_dir, emitter, new_dir, mocker): """Test the 'snapcraft init' command.""" - snapcraft_yaml = Path("snap/snapcraft.yaml") - - cli.run() + cmd = ["snapcraft", "init"] + if profile: + cmd.extend(["--profile", profile]) + if name: + cmd.extend(["--name", name]) + if project_dir: + cmd.append(project_dir) + snapcraft_yaml = pathlib.Path(project_dir) / "snap/snapcraft.yaml" + else: + snapcraft_yaml = Path("snap/snapcraft.yaml") + mocker.patch.object(sys, "argv", cmd) + app = application.create_app() + + app.run() assert snapcraft_yaml.exists() # unmarshal the snapcraft.yaml to verify its contents @@ -61,46 +70,80 @@ def test_init_default(emitter, new_dir): "platforms": {"amd64": {"build-on": "amd64", "build-for": "amd64"}}, } ) - emitter.assert_interactions( - [ - call("progress", "Checking for an existing 'snapcraft.yaml'."), - call("progress", "Could not find an existing 'snapcraft.yaml'."), - call("progress", "Creating 'snap/snapcraft.yaml'."), - call("message", "Created 'snap/snapcraft.yaml'."), - call( - "message", - "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " - "information about the snapcraft.yaml format.", - ), - ] + if name: + emitter.assert_progress( + "Ignoring '--name' parameter because it is not supported yet.", + permanent=True, + ) + emitter.assert_progress("Checking for an existing 'snapcraft.yaml'.") + emitter.assert_progress("Could not find an existing 'snapcraft.yaml'.") + emitter.assert_message("Successfully initialised project.") + emitter.assert_message( + "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " + "information about the snapcraft.yaml format." ) -def test_init_snap_dir_exists(emitter, new_dir): +@pytest.mark.parametrize("profile", [None, "simple"]) +@pytest.mark.parametrize("name", [None, "test-snap-name"]) +@pytest.mark.parametrize("project_dir", [None, "test-project-dir"]) +def test_init_snap_dir_exists(profile, name, project_dir, emitter, new_dir, mocker): """'snapcraft init' should work even if the 'snap/' directory already exists.""" - snapcraft_yaml = Path("snap/snapcraft.yaml") - Path("snap").mkdir() - - cli.run() + cmd = ["snapcraft", "init"] + if profile: + cmd.extend(["--profile", profile]) + if name: + cmd.extend(["--name", name]) + if project_dir: + cmd.append(project_dir) + snapcraft_yaml = pathlib.Path(project_dir) / "snap/snapcraft.yaml" + else: + snapcraft_yaml = Path("snap/snapcraft.yaml") + snapcraft_yaml.parent.mkdir(parents=True) + mocker.patch.object(sys, "argv", cmd) + app = application.create_app() + + app.run() assert snapcraft_yaml.exists() - emitter.assert_message("Created 'snap/snapcraft.yaml'.") + emitter.assert_message("Successfully initialised project.") + emitter.assert_message( + "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " + "information about the snapcraft.yaml format." + ) @pytest.mark.parametrize( "snapcraft_yaml", [project.project_file for project in _SNAP_PROJECT_FILES] ) -def test_init_exists(capsys, emitter, new_dir, snapcraft_yaml): +@pytest.mark.parametrize("profile", [None, "simple"]) +@pytest.mark.parametrize("name", [None, "test-snap-name"]) +@pytest.mark.parametrize("project_dir", [None, "test-project-dir"]) +def test_init_exists( + profile, name, project_dir, capsys, emitter, new_dir, snapcraft_yaml, mocker +): """Raise an error if a snapcraft.yaml file already exists.""" - snapcraft_yaml.parent.mkdir(parents=True, exist_ok=True) - snapcraft_yaml.touch() - - cli.run() + cmd = ["snapcraft", "init"] + if profile: + cmd.extend(["--profile", profile]) + if name: + cmd.extend(["--name", name]) + if project_dir: + cmd.append(project_dir) + snapcraft_yaml_path = pathlib.Path(project_dir) / snapcraft_yaml + else: + snapcraft_yaml_path = snapcraft_yaml + mocker.patch.object(sys, "argv", cmd) + snapcraft_yaml_path.parent.mkdir(parents=True, exist_ok=True) + snapcraft_yaml_path.touch() + app = application.create_app() + + app.run() out, err = capsys.readouterr() assert not out assert ( - "could not initialize a new snapcraft project because " + "could not initialise a new snapcraft project because " f"{str(snapcraft_yaml)!r} already exists" ) in err emitter.assert_progress("Checking for an existing 'snapcraft.yaml'.") diff --git a/tests/unit/extensions/test_env_injector.py b/tests/unit/extensions/test_env_injector.py index 0797b64f1d..b94691deda 100644 --- a/tests/unit/extensions/test_env_injector.py +++ b/tests/unit/extensions/test_env_injector.py @@ -99,7 +99,7 @@ def test_get_parts_snippet(self, envinjector_extension, unsupported_arch): cargo build --target {toolchain} --release mkdir -p $SNAPCRAFT_PART_INSTALL/bin/command-chain - + cp target/{toolchain}/release/env-exporter $SNAPCRAFT_PART_INSTALL/bin/command-chain """, } diff --git a/tests/unit/parts/test_yaml_utils.py b/tests/unit/parts/test_yaml_utils.py index fea87d3bce..23340fe92b 100644 --- a/tests/unit/parts/test_yaml_utils.py +++ b/tests/unit/parts/test_yaml_utils.py @@ -16,6 +16,7 @@ import io +import pathlib from textwrap import dedent import pytest @@ -248,3 +249,15 @@ def test_get_base_from_yaml(mocker): project_type="test-type", ) assert effective_base == "test-effective-base" + + +@pytest.mark.parametrize("project", yaml_utils._SNAP_PROJECT_FILES) +@pytest.mark.parametrize("project_dir", [None, "test-project-dir"]) +def test_get_snap_project(project, project_dir, new_dir): + project_dir = pathlib.Path(project_dir) if project_dir else new_dir + (project_dir / project.project_file).parent.mkdir(parents=True, exist_ok=True) + (project_dir / project.project_file).touch() + + actual_project = yaml_utils.get_snap_project(project_dir) + + assert actual_project == project diff --git a/tools/docs/gen_cli_docs.py b/tools/docs/gen_cli_docs.py index 84a7e9c5a0..429be288f1 100755 --- a/tools/docs/gen_cli_docs.py +++ b/tools/docs/gen_cli_docs.py @@ -82,6 +82,7 @@ def main(docs_dir): # Create a dispatcher like Snapcraft does to get access to the same options. app = application.create_app() + app._setup_logging() command_groups = app.command_groups # Create a dispatcher like Snapcraft does to get access to the same options. @@ -102,8 +103,7 @@ def main(docs_dir): g = group_path.open("w") for cmd_class in sorted(group.commands, key=lambda c: c.name): - # craft-application.AppCommand require 'app' and 'services' in the config - cmd = cmd_class(config={"app": {}, "services": {}}) + cmd = cmd_class(app.app_config) p = _CustomArgumentParser(help_builder) cmd.fill_parser(p) From e49653c634345491ffece9fe435798d7d1215fde Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Thu, 7 Nov 2024 07:06:02 -0600 Subject: [PATCH 3/6] chore: overide InitService, render name, fix spread tests Signed-off-by: Callahan Kovacs --- snapcraft/cli.py | 1 - snapcraft/commands/__init__.py | 2 - snapcraft/commands/init.py | 59 -------- snapcraft/models/project.py | 6 +- snapcraft/services/__init__.py | 2 + snapcraft/services/init.py | 82 +++++++++++ snapcraft/services/service_factory.py | 3 + .../{snapcraft.yaml => snapcraft.yaml.j2} | 2 +- tests/spread/general/init/task.yaml | 30 +++- tests/unit/commands/test_init.py | 122 ++++++----------- tests/unit/services/test_init.py | 129 ++++++++++++++++++ 11 files changed, 284 insertions(+), 154 deletions(-) delete mode 100644 snapcraft/commands/init.py create mode 100644 snapcraft/services/init.py rename snapcraft/templates/simple/snap/{snapcraft.yaml => snapcraft.yaml.j2} (90%) create mode 100644 tests/unit/services/test_init.py diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 98d890ceeb..361ad8ed03 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -151,7 +151,6 @@ "Other", [ commands.LintCommand, - commands.InitCommand, ], ), ] diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index b1a8cdc5eb..f3ee75220f 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -28,7 +28,6 @@ ExtensionsCommand, ListExtensionsCommand, ) -from .init import InitCommand from .legacy import ( StoreLegacyCreateKeyCommand, StoreLegacyGatedCommand, @@ -67,7 +66,6 @@ __all__ = [ "ExpandExtensionsCommand", "ExtensionsCommand", - "InitCommand", "LintCommand", "ListExtensionsCommand", "ListPluginsCommand", diff --git a/snapcraft/commands/init.py b/snapcraft/commands/init.py deleted file mode 100644 index 89dac23186..0000000000 --- a/snapcraft/commands/init.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2023-2024 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 . - -"""Snapcraft init command.""" - -import argparse - -import craft_application.commands -import craft_cli -from typing_extensions import override - -from snapcraft import errors -from snapcraft.parts.yaml_utils import get_snap_project - - -class InitCommand(craft_application.commands.InitCommand): - """Snapcraft init command.""" - - @override - def run(self, parsed_args: argparse.Namespace) -> None: - """Pack a directory or run the lifecycle and pack all artifacts.""" - project_dir = self._get_project_dir(parsed_args) - - if parsed_args.name: - craft_cli.emit.progress( - "Ignoring '--name' parameter because it is not supported yet.", - permanent=True, - ) - - try: - craft_cli.emit.progress("Checking for an existing 'snapcraft.yaml'.") - project = get_snap_project(project_dir) - raise errors.SnapcraftError( - "could not initialise a new snapcraft project because " - f"{str(project.project_file)!r} already exists" - ) - # the `ProjectMissing` error means a new project can be initialized - except errors.ProjectMissing: - craft_cli.emit.progress("Could not find an existing 'snapcraft.yaml'.") - - super().run(parsed_args) - - craft_cli.emit.message( - "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " - "information about the snapcraft.yaml format." - ) diff --git a/snapcraft/models/project.py b/snapcraft/models/project.py index 0b2b0bee59..31a363313d 100644 --- a/snapcraft/models/project.py +++ b/snapcraft/models/project.py @@ -224,7 +224,7 @@ def _validate_version_name(version: str, model_name: str) -> None: ) -def _validate_name(*, name: str, field_name: str) -> str: +def validate_name(*, name: str, field_name: str) -> str: """Validate a name. :param name: The name to validate. @@ -261,7 +261,7 @@ def _validate_component(name: str) -> str: raise ValueError( "component names cannot start with the reserved prefix 'snap-'" ) - return _validate_name(name=name, field_name="component") + return validate_name(name=name, field_name="component") def _get_partitions_from_components( @@ -729,7 +729,7 @@ def _validate_mandatory_base(self): @pydantic.field_validator("name") @classmethod def _validate_snap_name(cls, name): - return _validate_name(name=name, field_name="snap") + return validate_name(name=name, field_name="snap") @pydantic.field_validator("components") @classmethod diff --git a/snapcraft/services/__init__.py b/snapcraft/services/__init__.py index 746d7dbafb..055e32006a 100644 --- a/snapcraft/services/__init__.py +++ b/snapcraft/services/__init__.py @@ -17,6 +17,7 @@ """Snapcraft services.""" from snapcraft.services.assertions import Assertion +from snapcraft.services.init import Init from snapcraft.services.lifecycle import Lifecycle from snapcraft.services.package import Package from snapcraft.services.provider import Provider @@ -26,6 +27,7 @@ __all__ = [ "Assertion", + "Init", "Lifecycle", "Package", "Provider", diff --git a/snapcraft/services/init.py b/snapcraft/services/init.py new file mode 100644 index 0000000000..92c079c162 --- /dev/null +++ b/snapcraft/services/init.py @@ -0,0 +1,82 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2024 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 . + +"""Service for initializing a project.""" + +import pathlib + +import craft_cli +from craft_application import services +from typing_extensions import override + +from snapcraft import errors +from snapcraft.models.project import validate_name +from snapcraft.parts.yaml_utils import get_snap_project + + +class Init(services.InitService): + """Service class for initializing a project.""" + + @override + def initialise_project( + self, + *, + project_dir: pathlib.Path, + project_name: str, + template_dir: pathlib.Path, + ) -> None: + try: + validate_name(name=project_name, field_name="snap") + if len(project_name) > 40: + raise ValueError("snap names must be 40 characters or less") + except ValueError as err: + raise errors.SnapcraftError( + message=f"Invalid snap name {project_name!r}: {str(err)}.", + resolution="Provide a valid name with '--name' or rename the project directory.", + ) from err + + super().initialise_project( + project_dir=project_dir, + project_name=project_name, + template_dir=template_dir, + ) + craft_cli.emit.message( + "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " + "information about the snapcraft.yaml format." + ) + + @override + def check_for_existing_files( + self, + *, + project_dir: pathlib.Path, + template_dir: pathlib.Path, + ) -> None: + try: + craft_cli.emit.progress("Checking for an existing 'snapcraft.yaml'.") + project = get_snap_project(project_dir) + raise errors.SnapcraftError( + "Could not initialise a new snapcraft project because " + f"{str(project.project_file)!r} already exists" + ) + # the `ProjectMissing` error means a new project can be initialised + except errors.ProjectMissing: + craft_cli.emit.progress("Could not find an existing 'snapcraft.yaml'.") + + super().check_for_existing_files( + project_dir=project_dir, + template_dir=template_dir, + ) diff --git a/snapcraft/services/service_factory.py b/snapcraft/services/service_factory.py index bbb9f0630f..0d51c74168 100644 --- a/snapcraft/services/service_factory.py +++ b/snapcraft/services/service_factory.py @@ -33,6 +33,9 @@ class SnapcraftServiceFactory(ServiceFactory): project: models.Project | None = None # type: ignore[reportIncompatibleVariableOverride] # These are overrides of default ServiceFactory services + InitClass: type[services.Init] = ( # type: ignore[reportIncompatibleVariableOverride] + services.Init + ) LifecycleClass: type[services.Lifecycle] = ( # type: ignore[reportIncompatibleVariableOverride] services.Lifecycle ) diff --git a/snapcraft/templates/simple/snap/snapcraft.yaml b/snapcraft/templates/simple/snap/snapcraft.yaml.j2 similarity index 90% rename from snapcraft/templates/simple/snap/snapcraft.yaml rename to snapcraft/templates/simple/snap/snapcraft.yaml.j2 index 4ef5b5f2d6..5b69f37f7f 100644 --- a/snapcraft/templates/simple/snap/snapcraft.yaml +++ b/snapcraft/templates/simple/snap/snapcraft.yaml.j2 @@ -1,4 +1,4 @@ -name: my-snap-name # you probably want to 'snapcraft register ' +name: {{ name }} # you probably want to 'snapcraft register ' base: core24 # the base snap is the execution environment for this snap version: '0.1' # just for humans, typically '1.2+git' or '1.3.2' summary: Single-line elevator pitch for your amazing snap # 79 char long summary diff --git a/tests/spread/general/init/task.yaml b/tests/spread/general/init/task.yaml index 8c69c91b6e..e86e905028 100644 --- a/tests/spread/general/init/task.yaml +++ b/tests/spread/general/init/task.yaml @@ -1,23 +1,31 @@ summary: Run snapcraft init +systems: [ubuntu-22*] + environment: - PROFILE/default_profile: null - PROFILE/simple_profile: simple PROFILE: null - PROJECT_DIR/default_dir: null - PROJECT_DIR/project_dir: test-project-dir PROJECT_DIR: null + NAME: null + PROFILE/default_profile: null + PROFILE/simple_profile: simple + NAME/with_name: test-snap-name + PROJECT_DIR/with_project_dir: test-project-dir + NAME/with_project_dir_and_name: test-snap-name + PROJECT_DIR/with_project_dir_and_name: test-project-dir restore: | + unset SNAPCRAFT_BUILD_ENVIRONMENT + if [[ -n "$PROJECT_DIR" ]]; then cd "$PROJECT_DIR" fi + snapcraft clean + rm -f ./*.snap rm -rf ./snap execute: | - # unset SNAPCRAFT_BUILD_ENVIRONMENT=host unset SNAPCRAFT_BUILD_ENVIRONMENT args=("init") @@ -26,6 +34,10 @@ execute: | args+=("--profile" "$PROFILE") fi + if [[ -n "$NAME" ]]; then + args+=("--name" "$NAME") + fi + if [[ -n "$PROJECT_DIR" ]]; then args+=("$PROJECT_DIR") fi @@ -36,7 +48,13 @@ execute: | cd "$PROJECT_DIR" fi - # the base should be core24 + if [[ -n "$NAME" ]]; then + expected_name="$NAME" + else + expected_name="$(basename "$PWD")" + fi + + grep "^name: ${expected_name}" snap/snapcraft.yaml grep "^base: core24" snap/snapcraft.yaml # 'snapcraft init' should create a usable snapcraft.yaml file diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py index d0c6c2e75c..eac92759ec 100644 --- a/tests/unit/commands/test_init.py +++ b/tests/unit/commands/test_init.py @@ -14,33 +14,61 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import pathlib import sys -from pathlib import Path from textwrap import dedent import pytest from snapcraft import application from snapcraft.models.project import Project -from snapcraft.parts.yaml_utils import _SNAP_PROJECT_FILES, apply_yaml, process_yaml +from snapcraft.parts.yaml_utils import apply_yaml, process_yaml -@pytest.mark.parametrize("profile", [None, "simple"]) -@pytest.mark.parametrize("name", [None, "test-snap-name"]) -@pytest.mark.parametrize("project_dir", [None, "test-project-dir"]) -def test_init_default(profile, name, project_dir, emitter, new_dir, mocker): - """Test the 'snapcraft init' command.""" +@pytest.fixture +def valid_new_dir(tmp_path): + """Change to a new temporary directory whose name is a valid snap name.""" + cwd = os.getcwd() + new_dir = tmp_path / "test-snap-name-dir" + new_dir.mkdir() + os.chdir(new_dir) + + yield new_dir + + os.chdir(cwd) + + +def _create_command( + *, + profile: str | None = None, + project_dir: str | None = None, + name: str | None = None, +): + """Build a snapcraft init command.""" cmd = ["snapcraft", "init"] if profile: cmd.extend(["--profile", profile]) - if name: - cmd.extend(["--name", name]) if project_dir: cmd.append(project_dir) - snapcraft_yaml = pathlib.Path(project_dir) / "snap/snapcraft.yaml" + if name: + cmd.extend(["--name", name]) + return cmd + + +@pytest.mark.parametrize("profile", [None, "simple"]) +@pytest.mark.parametrize("name", [None, "test-snap-name"]) +@pytest.mark.parametrize("project_dir", [None, "test-project-dir"]) +def test_init_default(profile, name, project_dir, emitter, valid_new_dir, mocker): + """Test the 'snapcraft init' command.""" + if name: + expected_name = name + elif project_dir: + expected_name = project_dir else: - snapcraft_yaml = Path("snap/snapcraft.yaml") + expected_name = str(valid_new_dir.name) + snapcraft_yaml = pathlib.Path(project_dir or valid_new_dir) / "snap/snapcraft.yaml" + cmd = _create_command(profile=profile, project_dir=project_dir, name=name) mocker.patch.object(sys, "argv", cmd) app = application.create_app() @@ -52,7 +80,7 @@ def test_init_default(profile, name, project_dir, emitter, new_dir, mocker): project = Project.unmarshal(data) assert project == Project.unmarshal( { - "name": "my-snap-name", + "name": expected_name, "base": "core24", "version": "0.1", "summary": "Single-line elevator pitch for your amazing snap", @@ -70,80 +98,10 @@ def test_init_default(profile, name, project_dir, emitter, new_dir, mocker): "platforms": {"amd64": {"build-on": "amd64", "build-for": "amd64"}}, } ) - if name: - emitter.assert_progress( - "Ignoring '--name' parameter because it is not supported yet.", - permanent=True, - ) emitter.assert_progress("Checking for an existing 'snapcraft.yaml'.") emitter.assert_progress("Could not find an existing 'snapcraft.yaml'.") - emitter.assert_message("Successfully initialised project.") emitter.assert_message( "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " "information about the snapcraft.yaml format." ) - - -@pytest.mark.parametrize("profile", [None, "simple"]) -@pytest.mark.parametrize("name", [None, "test-snap-name"]) -@pytest.mark.parametrize("project_dir", [None, "test-project-dir"]) -def test_init_snap_dir_exists(profile, name, project_dir, emitter, new_dir, mocker): - """'snapcraft init' should work even if the 'snap/' directory already exists.""" - cmd = ["snapcraft", "init"] - if profile: - cmd.extend(["--profile", profile]) - if name: - cmd.extend(["--name", name]) - if project_dir: - cmd.append(project_dir) - snapcraft_yaml = pathlib.Path(project_dir) / "snap/snapcraft.yaml" - else: - snapcraft_yaml = Path("snap/snapcraft.yaml") - snapcraft_yaml.parent.mkdir(parents=True) - mocker.patch.object(sys, "argv", cmd) - app = application.create_app() - - app.run() - - assert snapcraft_yaml.exists() emitter.assert_message("Successfully initialised project.") - emitter.assert_message( - "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " - "information about the snapcraft.yaml format." - ) - - -@pytest.mark.parametrize( - "snapcraft_yaml", [project.project_file for project in _SNAP_PROJECT_FILES] -) -@pytest.mark.parametrize("profile", [None, "simple"]) -@pytest.mark.parametrize("name", [None, "test-snap-name"]) -@pytest.mark.parametrize("project_dir", [None, "test-project-dir"]) -def test_init_exists( - profile, name, project_dir, capsys, emitter, new_dir, snapcraft_yaml, mocker -): - """Raise an error if a snapcraft.yaml file already exists.""" - cmd = ["snapcraft", "init"] - if profile: - cmd.extend(["--profile", profile]) - if name: - cmd.extend(["--name", name]) - if project_dir: - cmd.append(project_dir) - snapcraft_yaml_path = pathlib.Path(project_dir) / snapcraft_yaml - else: - snapcraft_yaml_path = snapcraft_yaml - mocker.patch.object(sys, "argv", cmd) - snapcraft_yaml_path.parent.mkdir(parents=True, exist_ok=True) - snapcraft_yaml_path.touch() - app = application.create_app() - - app.run() - - out, err = capsys.readouterr() - assert not out - assert ( - "could not initialise a new snapcraft project because " - f"{str(snapcraft_yaml)!r} already exists" - ) in err - emitter.assert_progress("Checking for an existing 'snapcraft.yaml'.") diff --git a/tests/unit/services/test_init.py b/tests/unit/services/test_init.py new file mode 100644 index 0000000000..f073d2effd --- /dev/null +++ b/tests/unit/services/test_init.py @@ -0,0 +1,129 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2024 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +"""Tests for the init service.""" + +import importlib.resources +import pathlib + +import pytest + +from snapcraft import errors +from snapcraft.parts.yaml_utils import _SNAP_PROJECT_FILES + + +@pytest.fixture() +def init_service(default_factory): + from snapcraft.application import APP_METADATA + from snapcraft.services import Init + + service = Init(app=APP_METADATA, services=default_factory) + + return service + + +def template_dir(): + with importlib.resources.path("snapcraft", "templates") as _template_dir: + return _template_dir / "simple" + + +@pytest.mark.parametrize( + "name", + [ + "name", + "name-with-dashes", + "name0123", + "0123name", + "a234567890123456789012345678901234567890", + ], +) +def test_init_valid_name(name, init_service, new_dir, emitter): + """Initialise a project with a valid snap name.""" + init_service.initialise_project( + project_dir=new_dir, project_name=name, template_dir=template_dir() + ) + + assert (new_dir / "snap/snapcraft.yaml").exists() + emitter.assert_message( + "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " + "information about the snapcraft.yaml format." + ) + + +@pytest.mark.parametrize( + "name,error", + [ + ("name_with_underscores", "snap names can only use"), + ("name-with-UPPERCASE", "snap names can only use"), + ("name with spaces", "snap names can only use"), + ("-name-starts-with-hyphen", "snap names cannot start with a hyphen"), + ("name-ends-with-hyphen-", "snap names cannot end with a hyphen"), + ("name-has--two-hyphens", "snap names cannot have two hyphens in a row"), + ("123456", "snap names can only use"), + ( + "a2345678901234567890123456789012345678901", + "snap names must be 40 characters or less", + ), + ], +) +def test_init_invalid_name(name, error, init_service, new_dir): + """Error on invalid names.""" + expected_error = f"Invalid snap name {name!r}: {error}." + + with pytest.raises(errors.SnapcraftError, match=expected_error): + init_service.initialise_project( + project_dir=new_dir, + project_name=name, + template_dir=template_dir(), + ) + + +def test_init_snap_dir_exists(init_service, new_dir, emitter): + """Initialise a project even if the 'snap/' directory already exists.""" + snapcraft_yaml = new_dir / "snap/snapcraft.yaml" + snapcraft_yaml.parent.mkdir(parents=True) + + init_service.check_for_existing_files( + project_dir=new_dir, template_dir=template_dir() + ) + init_service.initialise_project( + project_dir=new_dir, project_name="test-snap-name", template_dir=template_dir() + ) + + assert snapcraft_yaml.exists() + emitter.assert_message( + "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " + "information about the snapcraft.yaml format." + ) + + +@pytest.mark.parametrize( + "project_file", [project.project_file for project in _SNAP_PROJECT_FILES] +) +def test_init_exists(init_service, new_dir, project_file): + """Raise an error if a snapcraft.yaml file already exists.""" + snapcraft_yaml = pathlib.Path(new_dir) / project_file + snapcraft_yaml.parent.mkdir(parents=True, exist_ok=True) + snapcraft_yaml.touch() + expected = ( + "Could not initialise a new snapcraft project because " + f"{str(project_file)!r} already exists" + ) + + with pytest.raises(errors.SnapcraftError, match=expected): + init_service.check_for_existing_files( + project_dir=new_dir, template_dir=template_dir() + ) From 890b313f06568df7d502120f05568acc525a5570 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Thu, 7 Nov 2024 10:51:42 -0600 Subject: [PATCH 4/6] tests: fix snap name Signed-off-by: Callahan Kovacs --- tests/spread/general/version-git/task.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/spread/general/version-git/task.yaml b/tests/spread/general/version-git/task.yaml index 65655016f1..538541d415 100644 --- a/tests/spread/general/version-git/task.yaml +++ b/tests/spread/general/version-git/task.yaml @@ -31,10 +31,10 @@ execute: | # First with lxd snapcraft --use-lxd ls -l - test -f my-snap-name_5.5-dirty_amd64.snap + test -f test-snap_5.5-dirty_amd64.snap rm ./*.snap # Then on host. snapcraft --destructive-mode ls -l - test -f my-snap-name_5.5-dirty_amd64.snap + test -f test-snap_5.5-dirty_amd64.snap From 5c15d08fa7ada7a30c3d4ca5b464a36701666e52 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Fri, 8 Nov 2024 07:30:39 -0600 Subject: [PATCH 5/6] chore: feedback from PR Signed-off-by: Callahan Kovacs --- snapcraft/services/init.py | 7 ++++--- tests/unit/commands/test_init.py | 13 ++++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/snapcraft/services/init.py b/snapcraft/services/init.py index 92c079c162..de736debc1 100644 --- a/snapcraft/services/init.py +++ b/snapcraft/services/init.py @@ -68,13 +68,14 @@ def check_for_existing_files( try: craft_cli.emit.progress("Checking for an existing 'snapcraft.yaml'.") project = get_snap_project(project_dir) + # the `ProjectMissing` error means a new project can be initialised + except errors.ProjectMissing: + craft_cli.emit.debug("Could not find an existing 'snapcraft.yaml'.") + else: raise errors.SnapcraftError( "Could not initialise a new snapcraft project because " f"{str(project.project_file)!r} already exists" ) - # the `ProjectMissing` error means a new project can be initialised - except errors.ProjectMissing: - craft_cli.emit.progress("Could not find an existing 'snapcraft.yaml'.") super().check_for_existing_files( project_dir=project_dir, diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py index eac92759ec..a5b760dc61 100644 --- a/tests/unit/commands/test_init.py +++ b/tests/unit/commands/test_init.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os import pathlib import sys from textwrap import dedent @@ -27,16 +26,12 @@ @pytest.fixture -def valid_new_dir(tmp_path): +def valid_new_dir(tmp_path, monkeypatch): """Change to a new temporary directory whose name is a valid snap name.""" - cwd = os.getcwd() new_dir = tmp_path / "test-snap-name-dir" new_dir.mkdir() - os.chdir(new_dir) - - yield new_dir - - os.chdir(cwd) + monkeypatch.chdir(new_dir) + return new_dir def _create_command( @@ -99,7 +94,7 @@ def test_init_default(profile, name, project_dir, emitter, valid_new_dir, mocker } ) emitter.assert_progress("Checking for an existing 'snapcraft.yaml'.") - emitter.assert_progress("Could not find an existing 'snapcraft.yaml'.") + emitter.assert_debug("Could not find an existing 'snapcraft.yaml'.") emitter.assert_message( "Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more " "information about the snapcraft.yaml format." From c17e0625e0055e33aeff7a3c7416e6f330098adc Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Fri, 8 Nov 2024 15:37:38 -0600 Subject: [PATCH 6/6] build(deps): bump craft-application Signed-off-by: Callahan Kovacs --- requirements-devel.txt | 2 +- requirements-docs.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index d9ebb29e33..67fee633e5 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -24,7 +24,7 @@ click==8.1.7 codespell==2.3.0 colorama==0.4.6 coverage==7.6.1 -git+https://github.com/canonical/craft-application@main#egg=craft-application +craft-application==4.4.0 craft-archives==2.0.1 craft-cli==2.10.0 craft-grammar==2.0.1 diff --git a/requirements-docs.txt b/requirements-docs.txt index 0d3ccd5192..b5494fa73e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -19,7 +19,7 @@ chardet==5.2.0 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 -git+https://github.com/canonical/craft-application@main#egg=craft-application +craft-application==4.4.0 craft-archives==2.0.1 craft-cli==2.10.0 craft-grammar==2.0.1 diff --git a/requirements.txt b/requirements.txt index b6530010d2..364fdc3f76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ cffi==1.17.1 chardet==5.2.0 charset-normalizer==3.3.2 click==8.1.7 -git+https://github.com/canonical/craft-application@main#egg=craft-application +craft-application==4.4.0 craft-archives==2.0.1 craft-cli==2.10.0 craft-grammar==2.0.1 diff --git a/setup.py b/setup.py index 85f9825b5f..45a840ce25 100755 --- a/setup.py +++ b/setup.py @@ -98,7 +98,7 @@ def recursive_data_files(directory, install_directory): "attrs", "catkin-pkg; sys_platform == 'linux'", "click", - "craft-application @ git+https://github.com/canonical/craft-application@main#egg=craft-application", + "craft-application~=4.4", "craft-archives~=2.0", "craft-cli~=2.9", "craft-grammar>=2.0.1,<3.0.0",