Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate lifecycle commands #366

Merged
merged 9 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ test-pydocstyle:
.PHONY: test-pylint
test-pylint:
pylint rockcraft
pylint tests --disable=invalid-name,missing-module-docstring,missing-function-docstring,redefined-outer-name,too-many-arguments,too-many-public-methods,no-member
pylint tests --disable=invalid-name,missing-module-docstring,missing-function-docstring,redefined-outer-name,too-many-arguments,too-many-public-methods,no-member,import-outside-toplevel

.PHONY: test-pyright
test-pyright:
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/code/convert-to-pebble-layer/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ summary: test the "How to convert a popular entrypoint to a Pebble layer" guide

execute: |
# [docs:pack]
rockcraft
rockcraft pack
# [docs:pack-end]

# [docs:skopeo]
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/code/install-slice/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ execute: |
mv rockcraft.yaml.backup rockcraft.yaml

# [docs:pack]
rockcraft
rockcraft pack
# [docs:pack-end]

# [docs:skopeo-copy]
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/code/chisel/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ execute: |
# [docs:install-rockcraft-end]

# [docs:build-rock]
rockcraft
rockcraft pack
# [docs:build-rock-end]

test -f chisel-openssl_0.0.1_amd64.rock
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/code/migrate-to-chiselled-rock/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ execute: |
# [docs:run-docker-image-end]

# [docs:build-rock]
rockcraft --verbosity debug
rockcraft pack --verbosity debug
# [docs:build-rock-end]

test -f dotnet-runtime_chiselled_amd64.rock
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/code/pyfiglet/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ execute: |
cp ../rockcraft.yaml .

# [docs:build-rock]
rockcraft
rockcraft pack
# [docs:build-rock-end]

# [docs:skopeo-copy]
Expand Down
5 changes: 5 additions & 0 deletions rockcraft/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@

"""The craft tool to create ROCKs."""

from craft_parts import Features

__version__ = "0.0.1.dev1"


Features(enable_overlay=True)
3 changes: 2 additions & 1 deletion rockcraft/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Main module."""
import sys

from .cli import run

if __name__ == "__main__":
run()
sys.exit(run())
93 changes: 0 additions & 93 deletions rockcraft/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,10 @@
from __future__ import annotations

import functools
import os
import pathlib
import signal
import sys

import craft_cli
from craft_application import Application, AppMetadata, util
from craft_application.application import _Dispatcher
from craft_application.commands import get_lifecycle_command_group
from craft_cli import CommandGroup
from overrides import override

from rockcraft import models
Expand All @@ -42,103 +36,16 @@
)


class FallbackRunError(RuntimeError):
"""Run should be handled by the legacy code."""


class Rockcraft(Application):
"""Rockcraft application definition."""

@property
@override
def command_groups(self) -> list[craft_cli.CommandGroup]:
"""Filter supported command from CraftApplication."""
all_lifecycle_commands = get_lifecycle_command_group().commands
migrated_commands = {"pack"}

return [
CommandGroup(
"Lifecycle",
[c for c in all_lifecycle_commands if c.name in migrated_commands],
)
]

@functools.cached_property
def project(self) -> models.Project:
"""Get this application's Project metadata."""
project_file = (pathlib.Path.cwd() / f"{self.app.name}.yaml").resolve()
craft_cli.emit.debug(f"Loading project file '{project_file!s}'")
return models.Project.from_yaml_file(project_file, work_dir=self._work_dir)

@override
def _get_dispatcher(self) -> _Dispatcher:
"""Overridden to raise a FallbackRunError() for unhandled commands.

Should be removed after we finish migrating all commands.
"""
craft_cli.emit.init(
mode=craft_cli.EmitterMode.BRIEF,
appname=self.app.name,
greeting=f"Starting {self.app.name}",
log_filepath=self.log_path,
streaming_brief=True,
)

dispatcher = _Dispatcher(
self.app.name,
self.command_groups,
summary=str(self.app.summary),
extra_global_args=self._global_arguments,
)

try:
craft_cli.emit.trace("pre-parsing arguments...")
# Workaround for the fact that craft_cli requires a command.
# https://github.com/canonical/craft-cli/issues/141
if "--version" in sys.argv or "-V" in sys.argv:
try:
global_args = dispatcher.pre_parse_args(["pull", *sys.argv[1:]])
except craft_cli.ArgumentParsingError:
global_args = dispatcher.pre_parse_args(sys.argv[1:])
else:
global_args = dispatcher.pre_parse_args(sys.argv[1:])

if global_args.get("version"):
craft_cli.emit.ended_ok()
print(f"{self.app.name} {self.app.version}")
sys.exit(0)
# Try loading the command to shake out possible ArgumentParsingErrors
# from commands with options that we can't handle yet (like
# --destructive-mode)
dispatcher.load_command(
{
"app": self.app,
"services": self.services,
}
)
except (craft_cli.ProvideHelpException, craft_cli.ArgumentParsingError) as err:
# Difference from base's behavior: fallback to the legacy run
# if we get an unknown command, or an invalid option, or a request
# for help.
raise FallbackRunError() from err
except KeyboardInterrupt as err:
self._emit_error(craft_cli.CraftError("Interrupted."), cause=err)
sys.exit(128 + signal.SIGINT)
except Exception as err: # pylint: disable=broad-except
self._emit_error(
craft_cli.CraftError(
f"Internal error while loading {self.app.name}: {err!r}"
)
)
if os.getenv("CRAFT_DEBUG") == "1":
raise
sys.exit(70) # EX_SOFTWARE from sysexits.h

craft_cli.emit.trace("Preparing application...")
self.configure(global_args)

return dispatcher

@override
def _configure_services(self, platform: str | None, build_for: str | None) -> None:
if build_for is None:
Expand Down
136 changes: 27 additions & 109 deletions rockcraft/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,63 +17,15 @@
"""Command-line application entry point."""

import logging
import sys
from typing import Optional

import craft_cli
from craft_cli import ArgumentParsingError, ProvideHelpException, emit
from craft_parts import PartsError
from craft_providers import ProviderError

from rockcraft import __version__, errors, plugins
from rockcraft import plugins

from . import commands
from .services import RockcraftServiceFactory
from .utils import is_managed_mode

COMMAND_GROUPS = [
craft_cli.CommandGroup(
"Lifecycle",
[
commands.CleanCommand,
commands.PullCommand,
commands.OverlayCommand,
commands.BuildCommand,
commands.StageCommand,
commands.PrimeCommand,
commands.PackCommand,
],
),
craft_cli.CommandGroup(
"Extensions",
[
commands.ExtensionsCommand,
commands.ListExtensionsCommand,
commands.ExpandExtensionsCommand,
],
),
craft_cli.CommandGroup(
"Other",
[
commands.InitCommand,
],
),
]

GLOBAL_ARGS = [
craft_cli.GlobalArgument(
"version", "flag", "-V", "--version", "Show the application version and exit"
)
]


def run() -> None:
def run() -> int:
"""Command-line interface entrypoint."""
# pylint: disable=import-outside-toplevel
# Import these here so that the script that generates the docs for the
# commands doesn't need to know *too much* of the application.
from .application import APP_METADATA, FallbackRunError, Rockcraft

# Register our own plugins
plugins.register()

Expand All @@ -82,71 +34,37 @@ def run() -> None:
logger = logging.getLogger(lib_name)
logger.setLevel(logging.DEBUG)

app = _create_app()

return app.run()


def _create_app():
# pylint: disable=import-outside-toplevel
# Import these here so that the script that generates the docs for the
# commands doesn't need to know *too much* of the application.
from .application import APP_METADATA, Rockcraft

services = RockcraftServiceFactory(
# type: ignore # type: ignore[call-arg]
app=APP_METADATA,
)

app = Rockcraft(app=APP_METADATA, services=services)

try:
app.run()
except FallbackRunError:
legacy_run()


def _emit_error(error: craft_cli.CraftError, cause: Optional[Exception] = None) -> None:
"""Emit the error in a centralized way so we can alter it consistently."""
# set the cause, if any
if cause is not None:
error.__cause__ = cause

# if running inside a managed instance, do not report the internal logpath
if is_managed_mode():
error.logpath_report = False

# finally, emit
emit.error(error)


def legacy_run() -> None:
"""Run the CLI."""
dispatcher = craft_cli.Dispatcher(
"rockcraft",
COMMAND_GROUPS,
summary="A tool to create OCI images",
extra_global_args=GLOBAL_ARGS,
default_command=commands.PackCommand,
app.add_command_group(
"Other",
[
commands.InitCommand,
],
)
app.add_command_group(
"Extensions",
[
commands.ExtensionsCommand,
commands.ListExtensionsCommand,
commands.ExpandExtensionsCommand,
],
)

try:
global_args = dispatcher.pre_parse_args(sys.argv[1:])
if global_args.get("version"):
emit.message(f"rockcraft {__version__}")
else:
dispatcher.load_command(None)
dispatcher.run()
emit.ended_ok()
except ProvideHelpException as err:
print(err, file=sys.stderr) # to stderr, as argparse normally does
emit.ended_ok()
except ArgumentParsingError as err:
print(err, file=sys.stderr) # to stderr, as argparse normally does
emit.ended_ok()
sys.exit(1)
except errors.RockcraftError as err:
_emit_error(err)
sys.exit(1)
except PartsError as err:
_emit_error(
craft_cli.CraftError(
err.brief, details=err.details, resolution=err.resolution
)
)
sys.exit(1)
except ProviderError as err:
_emit_error(craft_cli.CraftError(f"craft-providers error: {err}"))
sys.exit(1)
except Exception as err: # pylint: disable=broad-except
_emit_error(craft_cli.CraftError(f"rockcraft internal error: {err!r}"))
sys.exit(1)
return app
16 changes: 0 additions & 16 deletions rockcraft/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,9 @@
ListExtensionsCommand,
)
from .init import InitCommand
from .lifecycle import (
BuildCommand,
CleanCommand,
OverlayCommand,
PackCommand,
PrimeCommand,
PullCommand,
StageCommand,
)

__all__ = [
"InitCommand",
"CleanCommand",
"PullCommand",
"OverlayCommand",
"BuildCommand",
"StageCommand",
"PrimeCommand",
"PackCommand",
"ExpandExtensionsCommand",
"ExtensionsCommand",
"ListExtensionsCommand",
Expand Down
Loading