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

feat(craft-application): initial support #4560

Merged
merged 7 commits into from
Feb 7, 2024
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
1 change: 1 addition & 0 deletions requirements-devel.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ click==8.1.7
codespell==2.2.6
colorama==0.4.6
coverage==7.4.0
craft-application==1.2.1
craft-archives==1.1.3
craft-cli==2.5.1
craft-grammar==1.1.2
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ cffi==1.16.0
chardet==5.2.0
charset-normalizer==3.3.2
click==8.1.7
craft-application==1.2.1
craft-archives==1.1.3
craft-cli==2.5.1
craft-grammar==1.1.2
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def recursive_data_files(directory, install_directory):
"attrs",
"catkin-pkg; sys_platform == 'linux'",
"click",
"craft-application",
"craft-archives",
"craft-cli",
"craft-grammar",
Expand Down Expand Up @@ -154,7 +155,7 @@ def recursive_data_files(directory, install_directory):
entry_points=dict(
console_scripts=[
"snapcraft_legacy = snapcraft_legacy.cli.__main__:run",
"snapcraft = snapcraft.cli:run",
"snapcraft = snapcraft.application:main",
]
),
data_files=(
Expand Down
4 changes: 2 additions & 2 deletions snapcraft/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@

import sys

from snapcraft import cli
from snapcraft import application

sys.exit(cli.run())
sys.exit(application.main())
244 changes: 244 additions & 0 deletions snapcraft/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2023 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 <http://www.gnu.org/licenses/>.

"""Main Snapcraft Application."""

from __future__ import annotations

import os
import signal
import sys

import craft_cli
from craft_application import Application, AppMetadata, util
from craft_cli import emit
from overrides import override

from snapcraft import cli, errors, models, services
from snapcraft.commands import unimplemented

APP_METADATA = AppMetadata(
name="snapcraft",
summary="Package, distribute, and update snaps for Linux and IoT",
ProjectClass=models.Project,
source_ignore_patterns=["*.snap"],
)


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

@override
def _configure_services(self, platform: str | None, build_for: str | None) -> None:
if build_for is None:
build_for = util.get_host_architecture()

self.services.set_kwargs("package", platform=platform, build_for=build_for)
super()._configure_services(platform, build_for)

@property
def command_groups(self):
"""Short-circuit the standard command groups for now."""
# TODO: Remove this once we've got lifecycle commands and version migrated.
return self._command_groups

def run(self) -> int:
"""Fall back to the old snapcraft entrypoint."""
self._get_dispatcher()
raise errors.ClassicFallback()

@override
def _get_dispatcher(self) -> craft_cli.Dispatcher:
"""Configure this application. Should be called by the run method.

Side-effect: This method may exit the process.

:returns: A ready-to-run Dispatcher object
"""
# Set the logging level to DEBUG for all craft-libraries. This is OK even if
# the specific application doesn't use a specific library, the call does not
# import the package.
util.setup_loggers(*self._cli_loggers)

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 = craft_cli.Dispatcher(
self.app.name,
self.command_groups,
summary=str(self.app.summary),
extra_global_args=self._global_arguments,
# TODO: craft-application should allow setting the default command without
# overriding `_get_dispatcher()`
default_command=unimplemented.Pack,
)

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)
except craft_cli.ProvideHelpException as err:
print(err, file=sys.stderr) # to stderr, as argparse normally does
craft_cli.emit.ended_ok()
sys.exit(0)
except craft_cli.ArgumentParsingError as err:
print(err, file=sys.stderr) # to stderr, as argparse normally does
craft_cli.emit.ended_ok()
sys.exit(64) # Command line usage error from sysexits.h
except KeyboardInterrupt as err:
self._emit_error(craft_cli.CraftError("Interrupted."), cause=err)
sys.exit(128 + signal.SIGINT)
# pylint: disable-next=broad-exception-caught
except Exception as err: # noqa: BLE001
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


def main() -> int:
"""Run craft-application based snapcraft with classic fallback."""
util.setup_loggers(
"craft_parts", "craft_providers", "craft_store", "snapcraft.remote"
)

snapcraft_services = services.SnapcraftServiceFactory(app=APP_METADATA)

app = Snapcraft(app=APP_METADATA, services=snapcraft_services)

app.add_command_group(
"Lifecycle",
[
unimplemented.Clean,
unimplemented.Pull,
unimplemented.Build,
unimplemented.Stage,
unimplemented.Prime,
unimplemented.Pack,
unimplemented.RemoteBuild,
unimplemented.Snap, # Hidden (legacy compatibility)
unimplemented.Plugins,
unimplemented.ListPlugins,
unimplemented.Try,
],
)
app.add_command_group(
"Extensions",
[
unimplemented.ListExtensions,
unimplemented.Extensions,
unimplemented.ExpandExtensions,
],
)
app.add_command_group(
"Store Account",
[
unimplemented.Login,
unimplemented.ExportLogin,
unimplemented.Logout,
unimplemented.Whoami,
],
)
app.add_command_group(
"Store Snap Names",
[
unimplemented.Register,
unimplemented.Names,
unimplemented.ListRegistered,
unimplemented.List,
unimplemented.Metrics,
unimplemented.UploadMetadata,
],
)
app.add_command_group(
"Store Snap Release Management",
[
unimplemented.Release,
unimplemented.Close,
unimplemented.Status,
unimplemented.Upload,
unimplemented.Push,
unimplemented.Promote,
unimplemented.ListRevisions,
unimplemented.Revisions,
],
)
app.add_command_group(
"Store Snap Tracks",
[
unimplemented.ListTracks,
unimplemented.Tracks,
unimplemented.SetDefaultTrack,
],
)
app.add_command_group(
"Store Key Management",
[
unimplemented.CreateKey,
unimplemented.RegisterKey,
unimplemented.SignBuild,
unimplemented.ListKeys,
],
)
app.add_command_group(
"Store Validation Sets",
[
unimplemented.EditValidationSets,
unimplemented.ListValidationSets,
unimplemented.Validate,
unimplemented.Gated,
],
)
app.add_command_group(
"Other",
[
unimplemented.Version,
unimplemented.Lint,
unimplemented.Init,
],
)

try:
return app.run()
except errors.ClassicFallback:
emit.debug("Falling back from craft-application to snapcraft.")
return cli.run()
13 changes: 0 additions & 13 deletions snapcraft/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,6 @@ def get_dispatcher() -> craft_cli.Dispatcher:
_ORIGINAL_LIB_NAME_LOG_LEVEL[lib_name] = logger.level
logger.setLevel(logging.DEBUG)

if utils.is_managed_mode():
log_filepath = utils.get_managed_environment_log_path()
else:
log_filepath = None

emit.init(
mode=get_verbosity(),
appname="snapcraft",
greeting=f"Starting Snapcraft {__version__}",
log_filepath=log_filepath,
streaming_brief=True,
)

return craft_cli.Dispatcher(
"snapcraft",
COMMAND_GROUPS,
Expand Down
5 changes: 2 additions & 3 deletions snapcraft/commands/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@
from craft_parts.plugins import get_registered_plugins
from overrides import overrides

from snapcraft import errors
from snapcraft import errors, models
from snapcraft.parts.yaml_utils import (
apply_yaml,
extract_parse_info,
get_snap_project,
process_yaml,
)
from snapcraft.projects import Project
from snapcraft.utils import get_host_architecture

if TYPE_CHECKING:
Expand Down Expand Up @@ -80,7 +79,7 @@ def run(self, parsed_args):
# determine the base
extract_parse_info(yaml_data_for_arch)

project = Project.unmarshal(yaml_data_for_arch)
project = models.Project.unmarshal(yaml_data_for_arch)
base = project.get_effective_base()
message = (
f"Displaying plugins available to the current base {base!r} project"
Expand Down
5 changes: 2 additions & 3 deletions snapcraft/commands/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@
from overrides import overrides
from pydantic import BaseModel

from snapcraft import extensions
from snapcraft import extensions, models
from snapcraft.parts.yaml_utils import (
apply_yaml,
extract_parse_info,
get_snap_project,
process_yaml,
)
from snapcraft.projects import Project
from snapcraft.utils import get_host_architecture
from snapcraft_legacy.internal.project_loader import (
find_extension,
Expand Down Expand Up @@ -135,5 +134,5 @@ def run(self, parsed_args):
# not part of the Project model
extract_parse_info(yaml_data_for_arch)

Project.unmarshal(yaml_data_for_arch)
models.Project.unmarshal(yaml_data_for_arch)
emit.message(yaml.safe_dump(yaml_data_for_arch, indent=4, sort_keys=False))
10 changes: 5 additions & 5 deletions snapcraft/commands/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from craft_providers.util import snap_cmd
from overrides import overrides

from snapcraft import errors, linters, projects, providers
from snapcraft import errors, linters, models, providers
from snapcraft.meta import snap_yaml
from snapcraft.parts.yaml_utils import apply_yaml, extract_parse_info, process_yaml
from snapcraft.utils import (
Expand Down Expand Up @@ -257,7 +257,7 @@ def _unsquash_snap(self, snap_file: Path) -> Iterator[Path]:

yield Path(temp_dir)

def _load_project(self, snapcraft_yaml_file: Path) -> Optional[projects.Project]:
def _load_project(self, snapcraft_yaml_file: Path) -> Optional[models.Project]:
"""Load a snapcraft Project from a snapcraft.yaml, if present.

The snapcraft.yaml exist for snaps built with the `--enable-manifest` parameter.
Expand All @@ -284,7 +284,7 @@ def _load_project(self, snapcraft_yaml_file: Path) -> Optional[projects.Project]
yaml_data_for_arch = apply_yaml(yaml_data, arch, arch)
# discard parse-info - it is not needed
extract_parse_info(yaml_data_for_arch)
project = projects.Project.unmarshal(yaml_data_for_arch)
project = models.Project.unmarshal(yaml_data_for_arch)
return project

def _install_snap(
Expand Down Expand Up @@ -343,14 +343,14 @@ def _install_snap(

return Path("/snap") / snap_metadata.name / "current"

def _load_lint_filters(self, project: Optional[projects.Project]) -> projects.Lint:
def _load_lint_filters(self, project: Optional[models.Project]) -> models.Lint:
"""Load lint filters from a Project and disable the classic linter.

:param project: Project from the snap file, if present.

:returns: Lint config with classic linter disabled.
"""
lint_config = projects.Lint(ignore=["classic"])
lint_config = models.Lint(ignore=["classic"])

if project:
if project.lint:
Expand Down
Loading
Loading