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)