Skip to content

Commit

Permalink
refactor: use init command from craft-application
Browse files Browse the repository at this point in the history
Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com>
  • Loading branch information
mr-cal committed Oct 23, 2024
1 parent c52d04e commit cf21bdf
Show file tree
Hide file tree
Showing 12 changed files with 177 additions and 107 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
include schema/*
include extensions/*
recursive-include snapcraft/templates *
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 1 addition & 11 deletions snapcraft/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand Down
74 changes: 22 additions & 52 deletions snapcraft/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>'
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."
)
2 changes: 1 addition & 1 deletion snapcraft/extensions/env_injector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
""",
}
Expand Down
12 changes: 10 additions & 2 deletions snapcraft/parts/yaml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
17 changes: 17 additions & 0 deletions snapcraft/templates/simple/snap/snapcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: my-snap-name # you probably want to 'snapcraft register <name>'
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
29 changes: 27 additions & 2 deletions tests/spread/general/init/task.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
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
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
Expand Down
113 changes: 78 additions & 35 deletions tests/unit/commands/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,37 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

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
Expand All @@ -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'.")
2 changes: 1 addition & 1 deletion tests/unit/extensions/test_env_injector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
""",
}
Expand Down
Loading

0 comments on commit cf21bdf

Please sign in to comment.