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