Skip to content

Commit

Permalink
feat(craft-application): initial support (#4560)
Browse files Browse the repository at this point in the history
Signed-off-by: Callahan Kovacs <[email protected]>
Co-authored-by: Callahan <[email protected]>
Co-authored-by: Alex Lowe <[email protected]>
  • Loading branch information
3 people authored Feb 7, 2024
1 parent 11c1a6f commit 9855085
Show file tree
Hide file tree
Showing 40 changed files with 887 additions and 197 deletions.
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

0 comments on commit 9855085

Please sign in to comment.