Skip to content
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
7 changes: 7 additions & 0 deletions craft_application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,13 @@ def register_plugins(self, plugins: dict[str, PluginType]) -> None:
"""Register plugins for this application."""
if not plugins:
return

warnings.warn(
"Registering plugins through the Application is deprecated. Override "
"the Lifecycle service's get_plugin_group instead.",
DeprecationWarning,
stacklevel=0,
)
from craft_parts.plugins import register # noqa: PLC0415

craft_cli.emit.trace("Registering plugins...")
Expand Down
47 changes: 47 additions & 0 deletions craft_application/services/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@
callbacks,
)
from craft_parts.errors import CallbackRegistrationError
from craft_parts.plugins.plugins import set_plugin_group
from typing_extensions import override

from craft_application import errors, util
from craft_application.errors import EmptyBuildPlanError
from craft_application.services import base
from craft_application.util import repositories

if TYPE_CHECKING: # pragma: no cover
from craft_parts.plugins import Plugin

from craft_application.application import AppMetadata
from craft_application.services import ServiceFactory

Expand Down Expand Up @@ -143,10 +147,53 @@ def __init__(
@override
def setup(self) -> None:
"""Initialize the LifecycleManager with previously-set arguments."""
# By default we can get the regular build. However, if there is no possible
# build on the current machine, we get any build we can possibly do.
# If the exhaustive build plan is still empty, error out.
try:
build = self._get_build()
except EmptyBuildPlanError:
build_plan = self._services.get("build_plan").create_build_plan(
platforms=None,
build_for=None,
build_on=None,
)
if not build_plan:
raise EmptyBuildPlanError
build = build_plan[0]

plugin_group = self.get_plugin_group(build)
if plugin_group is not None:
emit.debug(
"A plugin group has been specified for the current build. "
"This overrides any previous plugin configurations.\n"
f"Build: {self._get_build()}\n"
f"Plugins: {plugin_group}"
)
set_plugin_group(plugin_group)
self._lcm = self._init_lifecycle_manager()
callbacks.register_post_step(self.post_prime, step_list=[Step.PRIME])
callbacks.register_configure_overlay(repositories.enable_overlay_eol)

@staticmethod
def get_plugin_group(
build_info: craft_platforms.BuildInfo, # noqa: ARG004 (used by overrides)
) -> dict[str, type[Plugin]] | None:
"""Get the plugin group for a given build.

If this returns a non-``None`` value, the LifecycleService sets the plugin
group to that group when running the given build. If ``None`` is returned, the
plugin groups feature is not used and an application must manually handle its
plugin groups.

The default implementation simply returns ``None``, as this is designed for an
application to override in order to get the relevant plugin groups.

:param build_info: The BuildInfo for the build.
:returns: A dictionary that is an appropriate plugin group or ``None``.
"""
return None

def _get_build(self) -> craft_platforms.BuildInfo:
"""Get the build for this run."""
plan = self._services.get("build_plan").plan()
Expand Down
13 changes: 10 additions & 3 deletions docs/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@ Changelog

For a complete list of commits, check out the `1.2.3`_ release on GitHub.

6.1.0 (YYYY-MM-DD)
6.1.0 (unreleased)
------------------

Services
========

- The Lifecycle service now has a ``get_plugin_group()`` method.

Application
===========

- If a lifecycle command is run with ``--destructive-mode``, but without root, a warning
will be emitted about potentially unexpected behavior.
- Direct registration of plugins in the Application is deprecated, as it's mutually
incompatible with the registration of plugin groups.

For a complete list of commits, check out the `6.1.0`_ release on GitHub.

Expand Down Expand Up @@ -513,8 +520,8 @@ Services
- Setting the arguments for a service using the service factory's ``set_kwargs`` is
deprecated. Use ``update_kwargs`` instead.

Testing
=======
Testing with pytest
===================

- Add a :doc:`pytest-plugin` with a fixture that enables production mode for the
application if a test requires it.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/services/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ Services
:maxdepth: 1

app
lifecycle
project
21 changes: 21 additions & 0 deletions docs/reference/services/lifecycle.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.. meta::
:description: API reference for the LifecycleService. In a craft application, the LifecycleService handles interactions with Craft Parts.

.. py:currentmodule:: craft_application.services.lifecycle

.. _reference-LifecycleService:

``LifecycleService``
====================

The ``LifecycleService`` handles interactions with :external+craft-parts:doc:`index`.


API documentation
-----------------

.. autoclass:: LifecycleService
:member-order: bysource
:members:
:private-members: _get_build,_validate_build_plan,_get_build_for,_init_lifecycle_manager
:undoc-members:
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dependencies = [
"craft-archives>=2.2.0",
"craft-cli>=3.2.0",
"craft-grammar>=2.3.0",
"craft-parts>=2.26.0",
"craft-parts>=2.28.0",
"craft-platforms>=0.6.0",
"craft-providers>=3.2.0",
"Jinja2>=3.1.6,<4.0.0",
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/services/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@

import os
import textwrap
from unittest import mock

import craft_cli
import craft_parts
import pytest
import pytest_check
from craft_application.services.lifecycle import LifecycleService
from craft_parts.plugins import plugins
from craft_parts.plugins.nil_plugin import NilPlugin


@pytest.fixture(
Expand All @@ -47,6 +51,33 @@ def parts_lifecycle(app_metadata, fake_project, fake_services, tmp_path, request
return service


@pytest.mark.parametrize(
"plugin_group",
[
*(pytest.param(group.value, id=group.name) for group in plugins.PluginGroup),
pytest.param({"nil": NilPlugin}, id="nil-only"),
],
)
def test_setup_with_different_plugin_group(
tmp_path, app_metadata, fake_project, fake_services, plugin_group
):
try:
lifecycle_service = LifecycleService(
app=app_metadata,
services=fake_services,
work_dir=tmp_path,
cache_dir=tmp_path / "cache",
)

lifecycle_service.get_plugin_group = mock.Mock(return_value=plugin_group)

lifecycle_service.setup()

assert craft_parts.plugins.get_registered_plugins() == plugin_group
finally:
craft_parts.plugins.plugins.unregister_all()


@pytest.mark.slow
def test_run_and_clean_all_parts(
parts_lifecycle: LifecycleService, build_plan_service, emitter, check, tmp_path
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: merlin-build
description: |
A witchcraft cauldron that fails to pack on Ubuntu 22.04 due to the lack of a rust plugin.
base: [email protected]
platforms:
platform-independent:
build-on: [amd64, arm64, ppc64el, s390x, riscv64]
build-for: [all]

version: "0.1"

parts:
part1:
# The rust plugin is only available for 24.04+ in witchcraft
plugin: rust
source: .
override-build: |
true
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: morgana-build
description: |
A witchcraft cauldron that fails to pack on Ubuntu 24.04 because of the lack of a Python plugin.
base: [email protected]
platforms:
platform-independent:
build-on: [amd64, arm64, ppc64el, s390x, riscv64]
build-for: [all]

version: "0.1"

parts:
part1:
# The Python plugin is only available for 22.04 and below in witchcraft.
plugin: python
source: .
override-build: |
true
28 changes: 28 additions & 0 deletions tests/spread/witchcraft/plugin-groups/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
summary: test setting of plugin groups based on BuildInfo.

systems:
- ubuntu-22.04-64
- ubuntu-24.04-64

execute: |
. /etc/os-release

export DISTRO="${ID}@${VERSION_ID}"

# Check that the valid package for this system packs.
cd "${DISTRO}"
witchcraft pack --destructive-mode
cd -

# Check that the package referencing a plugin not used on this system does not pack.
cd "invalid-${DISTRO}"
witchcraft pack --destructive-mode |& MATCH "Plugin '[a-z]+' in part 'part1' is not registered."


restore: |
for path in $(find ./* -maxdepth 0 -type d); do
cd "${path}"
witchcraft clean --destructive-mode || true
rm -f *.witchcraft
cd ..
done
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: merlin-build
summary: A witchcraft cauldron that packs on Ubuntu 22.04 with the python plugin.
base: [email protected]
platforms:
platform-independent:
build-on: [amd64, arm64, ppc64el, s390x, riscv64]
build-for: [all]

version: "0.1"

parts:
part1:
# The Python plugin is only available for 22.04 and below in witchcraft.
plugin: python
source: .
override-build: |
true
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: morgana-build
summary: A witchcraft cauldron that packs on Ubuntu 22.04 with the rust plugin.
base: [email protected]
platforms:
platform-independent:
build-on: [amd64, arm64, ppc64el, s390x, riscv64]
build-for: [all]

version: "0.1"

parts:
part1:
# The rust plugin is only available for 24.04+
plugin: rust
source: .
override-build: |
true
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions witchcraft/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ def register_services() -> None:
This registers with the ServiceFactory all the services that witchcraft
adds or overrides.
"""
craft_application.ServiceFactory.register(
"lifecycle",
"Lifecycle",
module="witchcraft.services.lifecycle",
)
craft_application.ServiceFactory.register(
"package",
"PackageService",
Expand Down
44 changes: 44 additions & 0 deletions witchcraft/services/lifecycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# This file is part of craft_application.
#
# Copyright 2025 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser 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 warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Witchcraft lifecycle service."""

import craft_platforms
from craft_application.services import lifecycle
from craft_parts.plugins import Plugin
from craft_parts.plugins.plugins import PluginGroup
from craft_parts.plugins.python_plugin import PythonPlugin
from craft_parts.plugins.rust_plugin import RustPlugin
from typing_extensions import override

MERLIN = PluginGroup.MINIMAL.value | {
"python": PythonPlugin,
}
MORGANA = PluginGroup.MINIMAL.value | {"rust": RustPlugin}


class Lifecycle(lifecycle.LifecycleService):
"""Lifecycle service for witchcraft."""

@override
@staticmethod
def get_plugin_group(
build_info: craft_platforms.BuildInfo,
) -> dict[str, type[Plugin]] | None:
if build_info.build_base.distribution != "ubuntu":
return MORGANA
if build_info.build_base <= craft_platforms.DistroBase("ubuntu", "22.04"):
return MERLIN
return MORGANA
Loading