Skip to content

Commit

Permalink
fix: allow packing a directory (#4794)
Browse files Browse the repository at this point in the history
This fixes a regression in snapcraft 8.2 where `snapcraft pack
<directory>` stopped working.

Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com>
  • Loading branch information
mr-cal authored May 8, 2024
1 parent faeee1b commit 48483c4
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 7 deletions.
2 changes: 1 addition & 1 deletion snapcraft/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def create_app() -> Snapcraft:
craft_app_commands.lifecycle.BuildCommand,
craft_app_commands.lifecycle.StageCommand,
craft_app_commands.lifecycle.PrimeCommand,
craft_app_commands.lifecycle.PackCommand,
commands.PackCommand,
commands.SnapCommand, # Hidden (legacy compatibility)
commands.RemoteBuildCommand,
unimplemented.Plugins,
Expand Down
3 changes: 2 additions & 1 deletion snapcraft/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
StoreLegacyUploadMetadataCommand,
StoreLegacyValidateCommand,
)
from .lifecycle import SnapCommand
from .lifecycle import PackCommand, SnapCommand
from .manage import StoreCloseCommand, StoreReleaseCommand
from .names import (
StoreLegacyListCommand,
Expand All @@ -60,6 +60,7 @@
"ExpandExtensions",
"ListExtensions",
"RemoteBuildCommand",
"PackCommand",
"SnapCommand",
"StoreCloseCommand",
"StoreEditValidationSetsCommand",
Expand Down
95 changes: 93 additions & 2 deletions snapcraft/commands/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,107 @@
"""Snapcraft lifecycle commands."""

import argparse
import pathlib
import textwrap
from typing import Any

import craft_application.commands
from craft_cli import emit
from typing_extensions import override

# pylint: disable=too-many-ancestors
import snapcraft.pack


class SnapCommand(craft_application.commands.lifecycle.PackCommand):
class PackCommand(craft_application.commands.LifecycleCommand):
"""Snapcraft pack command."""

name = "pack"
help_msg = "Create the final artifact"
overview = textwrap.dedent(
"""
Process parts and create a snap file containing the project payload
with the provided metadata. If a directory is specified, pack its
contents instead.
"""
)

@override
def _fill_parser(self, parser: argparse.ArgumentParser) -> None:
"""Add arguments specific to the pack command."""
super()._fill_parser(parser)

parser.add_argument(
"directory",
metavar="directory",
type=str,
nargs="?",
default=None,
help="Directory to pack",
)
parser.add_argument(
"--output",
"-o",
type=pathlib.Path,
default=pathlib.Path(),
help="Output directory for created packages",
)

@override
def _run(
self,
parsed_args: argparse.Namespace,
step_name: str | None = None,
**kwargs: Any,
) -> None:
"""Pack a directory or run the lifecycle and pack all artifacts."""
if parsed_args.directory:
emit.progress("Packing...")
snap_filename = snapcraft.pack.pack_snap(
parsed_args.directory, output=str(parsed_args.output)
)
emit.message(f"Packed {snap_filename}")
else:
super()._run(parsed_args, step_name="prime")
self._services.package.update_project()
self._services.package.write_metadata(self._services.lifecycle.prime_dir)

emit.progress("Packing...")
packages = self._services.package.pack(
self._services.lifecycle.prime_dir, parsed_args.output
)

if not packages:
emit.progress("No packages created.", permanent=True)
elif len(packages) == 1:
emit.progress(f"Packed {packages[0].name}", permanent=True)
else:
package_names = ", ".join(pkg.name for pkg in packages)
emit.progress(f"Packed: {package_names}", permanent=True)

@override
def needs_project(self, parsed_args: argparse.Namespace) -> bool:
"""Project is not required to pack a directory."""
if parsed_args.directory:
emit.debug("Not loading project because a directory was provided.")
return False

emit.debug("Loading project because a directory was not provided.")
return True

@override
def run_managed(self, parsed_args: argparse.Namespace) -> bool:
"""Return whether the command should run in managed mode or not.
Packing a directory always runs locally.
"""
if parsed_args.directory:
emit.debug("Not running managed mode because a directory was provided.")
return False

return super().run_managed(parsed_args)


class SnapCommand(PackCommand):
"""Deprecated legacy command to pack the final snap payload."""

name = "snap"
Expand Down
8 changes: 8 additions & 0 deletions tests/spread/core22/packing/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ environment:
CMD/pack: pack
CMD/pack_output: pack -o output.snap
CMD/pack_output_subdir: pack --output subdir/output.snap
CMD/pack_directory: pack prime
CMD/pack_directory_output: pack prime --output output.snap
CMD/pack_directory_output_subdir: pack prime --output subdir/output.snap
CMD/snap: snap
CMD/snap_output: snap --output output.snap
CMD/snap_output_subdir: snap -o subdir/output.snap
Expand All @@ -25,6 +28,11 @@ restore: |
restore_yaml "snap/snapcraft.yaml"
execute: |
# create a local directory to pack
if [[ "$SPREAD_VARIANT" =~ "pack_directory" ]]; then
snapcraft prime --destructive-mode
fi
# shellcheck disable=SC2086
snapcraft $CMD --destructive-mode
Expand Down
16 changes: 15 additions & 1 deletion tests/spread/core24/packing/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ environment:
CMD/pack: pack
CMD/pack_output: pack -o output.snap
CMD/pack_output_subdir: pack --output subdir/output.snap
CMD/pack_directory: pack prime
CMD/pack_directory_output: pack prime --output output.snap
CMD/pack_directory_output_subdir: pack prime --output subdir/output.snap
CMD/snap: snap
CMD/snap_output: snap --output output.snap
CMD/snap_output_subdir: snap -o subdir/output.snap
Expand All @@ -17,14 +20,25 @@ prepare: |
# set_base "$SNAP_DIR/snap/snapcraft.yaml"
restore: |
snapcraft clean
rm -Rf subdir ./*.snap
# 'pack_directory' tests are run in destructive-mode
if [[ "$SPREAD_VARIANT" =~ "pack_directory" ]]; then
snapcraft clean --destructive-mode
else
snapcraft clean
fi
#shellcheck source=tests/spread/tools/snapcraft-yaml.sh
. "$TOOLS_DIR/snapcraft-yaml.sh"
restore_yaml "snap/snapcraft.yaml"
execute: |
# create a local directory to pack
if [[ "$SPREAD_VARIANT" =~ "pack_directory" ]]; then
snapcraft prime --destructive-mode
fi
# shellcheck disable=SC2086
snapcraft $CMD
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/commands/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def test_core22_pack_command_with_directory(mocker):
def test_snap_command_fallback(tmp_path, emitter, mocker, fake_services):
"""Test that the snap command is falling back to the pack command."""
parsed_args = argparse.Namespace(parts=[], output=tmp_path)
mock_pack = mocker.patch("craft_application.commands.lifecycle.PackCommand._run")
mock_pack = mocker.patch("snapcraft.commands.lifecycle.PackCommand._run")
cmd = lifecycle.SnapCommand({"app": APP_METADATA, "services": fake_services})
cmd.run(parsed_args=parsed_args)
mock_pack.assert_called_once()
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
import pytest
import yaml
from craft_application import util
from craft_application.commands.lifecycle import PackCommand
from craft_parts.packages import snaps
from craft_providers import bases

from snapcraft import application, services
from snapcraft.commands import PackCommand
from snapcraft.errors import ClassicFallback, SnapcraftError
from snapcraft.models.project import Architecture
from snapcraft.parts.yaml_utils import CURRENT_BASES, ESM_BASES, LEGACY_BASES
Expand Down

0 comments on commit 48483c4

Please sign in to comment.