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/requirements-devel.txt b/requirements-devel.txt index a11632f78f..67fee633e5 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.7 +craft-application==4.4.0 craft-archives==2.0.1 -craft-cli==2.7.0 +craft-cli==2.10.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 024510c35c..b5494fa73e 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.7 +craft-application==4.4.0 craft-archives==2.0.1 -craft-cli==2.7.0 +craft-cli==2.10.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 db561aecc0..364fdc3f76 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.7 +craft-application==4.4.0 craft-archives==2.0.1 -craft-cli==2.7.0 +craft-cli==2.10.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 3fc201f7fd..45a840ce25 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.7,<5.0.0", + "craft-application~=4.4", "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", @@ -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/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 e968d6ee70..0000000000 --- a/snapcraft/commands/init.py +++ /dev/null @@ -1,89 +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.""" - -from pathlib import Path -from textwrap import dedent - -from craft_application.commands import AppCommand -from craft_cli import emit -from overrides import overrides - -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 - - parts: - my-part: - # See 'snapcraft plugins' - plugin: nil - """ -) - - -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() - raise errors.SnapcraftError( - "could not initialize 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: - 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}.") - - snapcraft_yaml_path.parent.mkdir(exist_ok=True) - snapcraft_yaml_path.write_text(_TEMPLATE_YAML, encoding="utf-8") - - emit.message(f"Created {str(snapcraft_yaml_path)!r}.") - 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/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/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/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..de736debc1 --- /dev/null +++ b/snapcraft/services/init.py @@ -0,0 +1,83 @@ +# -*- 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) + # 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" + ) + + 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.j2 b/snapcraft/templates/simple/snap/snapcraft.yaml.j2 new file mode 100644 index 0000000000..5b69f37f7f --- /dev/null +++ b/snapcraft/templates/simple/snap/snapcraft.yaml.j2 @@ -0,0 +1,17 @@ +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 +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..e86e905028 100644 --- a/tests/spread/general/init/task.yaml +++ b/tests/spread/general/init/task.yaml @@ -1,16 +1,60 @@ summary: Run snapcraft init +systems: [ubuntu-22*] + +environment: + PROFILE: null + 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 - # initialize a new snapcraft project - snapcraft init + args=("init") + + if [[ -n "$PROFILE" ]]; then + args+=("--profile" "$PROFILE") + fi + + if [[ -n "$NAME" ]]; then + args+=("--name" "$NAME") + fi + + if [[ -n "$PROJECT_DIR" ]]; then + args+=("$PROJECT_DIR") + fi + + snapcraft "${args[@]}" + + if [[ -n "$PROJECT_DIR" ]]; then + cd "$PROJECT_DIR" + fi + + if [[ -n "$NAME" ]]; then + expected_name="$NAME" + else + expected_name="$(basename "$PWD")" + fi - # the base should be core24 + 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/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 diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py index 2398ee3915..a5b760dc61 100644 --- a/tests/unit/commands/test_init.py +++ b/tests/unit/commands/test_init.py @@ -14,28 +14,60 @@ # 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): +from snapcraft.parts.yaml_utils import apply_yaml, process_yaml + + +@pytest.fixture +def valid_new_dir(tmp_path, monkeypatch): + """Change to a new temporary directory whose name is a valid snap name.""" + new_dir = tmp_path / "test-snap-name-dir" + new_dir.mkdir() + monkeypatch.chdir(new_dir) + return new_dir + + +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 project_dir: + cmd.append(project_dir) + 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.""" - snapcraft_yaml = Path("snap/snapcraft.yaml") - - cli.run() + if name: + expected_name = name + elif project_dir: + expected_name = project_dir + else: + 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() + + app.run() assert snapcraft_yaml.exists() # unmarshal the snapcraft.yaml to verify its contents @@ -43,7 +75,7 @@ def test_init_default(emitter, new_dir): 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", @@ -61,46 +93,10 @@ 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.", - ), - ] - ) - - -def test_init_snap_dir_exists(emitter, new_dir): - """'snapcraft init' should work even if the 'snap/' directory already exists.""" - snapcraft_yaml = Path("snap/snapcraft.yaml") - Path("snap").mkdir() - - cli.run() - - assert snapcraft_yaml.exists() - emitter.assert_message("Created 'snap/snapcraft.yaml'.") - - -@pytest.mark.parametrize( - "snapcraft_yaml", [project.project_file for project in _SNAP_PROJECT_FILES] -) -def test_init_exists(capsys, emitter, new_dir, snapcraft_yaml): - """Raise an error if a snapcraft.yaml file already exists.""" - snapcraft_yaml.parent.mkdir(parents=True, exist_ok=True) - snapcraft_yaml.touch() - - cli.run() - - out, err = capsys.readouterr() - assert not out - assert ( - "could not initialize a new snapcraft project because " - f"{str(snapcraft_yaml)!r} already exists" - ) in err emitter.assert_progress("Checking for 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." + ) + emitter.assert_message("Successfully initialised project.") 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/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() + ) 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)