diff --git a/snapcraft/models/project.py b/snapcraft/models/project.py
index ccfdc64b70..9ebbd25cf0 100644
--- a/snapcraft/models/project.py
+++ b/snapcraft/models/project.py
@@ -15,10 +15,12 @@
# along with this program. If not, see .
"""Project file definition and helpers."""
+
from __future__ import annotations
import copy
import re
+import textwrap
from typing import Any, Literal, Mapping, Tuple, cast
import pydantic
@@ -265,7 +267,7 @@ def _validate_component(name: str) -> str:
def _get_partitions_from_components(
- components_data: dict[str, Any] | None
+ components_data: dict[str, Any] | None,
) -> list[str] | None:
"""Get a list of partitions based on the project's components.
@@ -289,8 +291,13 @@ def _validate_mandatory_base(base: str | None, snap_type: str | None) -> None:
class Socket(models.CraftBaseModel):
"""Snapcraft app socket definition."""
- listen_stream: int | str
- socket_mode: int | None = None
+ listen_stream: int | str = pydantic.Field(
+ description="The socket's abstract name or socket path.",
+ examples=["listen-stream: $SNAP_COMMON/lxd/unix.socket", "listen-stream: 80"],
+ )
+ socket_mode: int | None = pydantic.Field(
+ default=None, description="The socket's mode and permissions."
+ )
@pydantic.field_validator("listen_stream")
@classmethod
@@ -324,7 +331,9 @@ class Lint(models.CraftBaseModel):
The "known" linter names are the keys in :ref:`LINTERS`
"""
- ignore: list[str | dict[str, list[str]]]
+ ignore: list[str | dict[str, list[str]]] = pydantic.Field(
+ description="Linters or files to skip when linting."
+ )
# A private field to simplify lookup.
_lint_ignores: dict[str, list[str]] = PrivateAttr(default_factory=dict)
@@ -370,24 +379,91 @@ def ignored_files(self, linter_name: str) -> list[str]:
class App(models.CraftBaseModel):
"""Snapcraft project app definition."""
- command: str
- autostart: str | None = None
- common_id: str | None = None
- bus_name: str | None = None
- desktop: str | None = None
- completer: str | None = None
- stop_command: str | None = None
- post_stop_command: str | None = None
- start_timeout: str | None = None
- stop_timeout: str | None = None
- watchdog_timeout: str | None = None
- reload_command: str | None = None
- restart_delay: str | None = None
- timer: str | None = None
- daemon: Literal["simple", "forking", "oneshot", "notify", "dbus"] | None = None
- after: UniqueList[str] = pydantic.Field(default_factory=list)
- before: UniqueList[str] = pydantic.Field(default_factory=list)
- refresh_mode: Literal["endure", "restart", "ignore-running"] | None = None
+ command: str = pydantic.Field(
+ description="The command to run inside the snap when the app is invoked.",
+ examples=["command: bin/app-1"],
+ )
+ autostart: str | None = pydantic.Field(
+ default=None,
+ description="The desktop file used to start an application when the desktop environment starts.",
+ examples=["autostart: my-app.desktop"],
+ )
+ common_id: str | None = pydantic.Field(
+ default=None,
+ description="The app's unique `AppStream identifier `_.",
+ examples=["common-id: org.canonical.foo"],
+ )
+ bus_name: str | None = pydantic.Field(
+ default=None,
+ description="The bus name that the app or service exposes through D-Bus.",
+ )
+ desktop: str | None = pydantic.Field(
+ default=None, description="The desktop file used to start the app."
+ )
+ completer: str | None = pydantic.Field(
+ default=None, description="The name of the bash completion script for the app."
+ )
+ stop_command: str | None = pydantic.Field(
+ default=None,
+ description="The command that stops the service.",
+ examples=["stop-command: bin/foo-app --halt"],
+ )
+ post_stop_command: str | None = pydantic.Field(
+ default=None, description="The command to run after the service is stopped."
+ )
+ start_timeout: str | None = pydantic.Field(
+ default=None,
+ description="The maximum amount of time to wait for the service to start.",
+ )
+ stop_timeout: str | None = pydantic.Field(
+ default=None,
+ description="The maximum amount of time to wait for the service to stop.",
+ )
+ watchdog_timeout: str | None = pydantic.Field(
+ default=None,
+ description="The maximum amount of time the service can run without sending a heartbeat to the watchdog.",
+ )
+ reload_command: str | None = pydantic.Field(
+ default=None,
+ description="The command to run to restart the service.",
+ examples=["reload-command: bin/foo-app --restart"],
+ )
+ restart_delay: str | None = pydantic.Field(
+ default=None,
+ description="The time to wait between service restarts.",
+ examples=["restart-delay: 10s", "restart-delay: 2m"],
+ )
+ timer: str | None = pydantic.Field(
+ default=None,
+ description="The time or schedule to run a service.",
+ examples=[
+ "timer: 23:00",
+ "timer: 00:00-24:00/24",
+ "timer: mon,10:00,,fri,15:00",
+ ],
+ )
+ daemon: Literal["simple", "forking", "oneshot", "notify", "dbus"] | None = (
+ pydantic.Field(
+ default=None,
+ description="Configures the app as a service, and sets its runtime and notification behavior.",
+ )
+ )
+ after: UniqueList[str] = pydantic.Field(
+ default_factory=list,
+ description="The sequence of apps that the service runs after it launches.",
+ examples=["after: [foo-app, bar-app]"],
+ )
+ before: UniqueList[str] = pydantic.Field(
+ default_factory=list,
+ description="The sequence of apps that the service runs before it launches.",
+ examples=["before: [baz-app, quz-app]"],
+ )
+ refresh_mode: Literal["endure", "restart", "ignore-running"] | None = (
+ pydantic.Field(
+ default=None,
+ description="Determines how the service should restart when the snap refreshed.",
+ )
+ )
stop_mode: (
Literal[
"sigterm",
@@ -402,7 +478,9 @@ class App(models.CraftBaseModel):
"sigint-all",
]
| None
- ) = None
+ ) = pydantic.Field(
+ default=None, description="The signal to send when stopping the service."
+ )
restart_condition: (
Literal[
"on-success",
@@ -414,17 +492,51 @@ class App(models.CraftBaseModel):
"never",
]
| None
- ) = None
- install_mode: Literal["enable", "disable"] | None = None
- slots: UniqueList[str] | None = None
- plugs: UniqueList[str] | None = None
- aliases: UniqueList[str] | None = None
- environment: dict[str, str] | None = None
- command_chain: list[str] = []
- sockets: dict[str, Socket] | None = None
- daemon_scope: Literal["system", "user"] | None = None
- activates_on: UniqueList[str] | None = None
- passthrough: dict[str, Any] | None = None
+ ) = pydantic.Field(
+ default=None, description="The condition under which the service restarts."
+ )
+ install_mode: Literal["enable", "disable"] | None = pydantic.Field(
+ default=None,
+ description="Whether snapd can automatically start the service when the snap is installed.",
+ )
+ slots: UniqueList[str] | None = pydantic.Field(
+ default=None, description="The app's slots."
+ )
+ plugs: UniqueList[str] | None = pydantic.Field(
+ default=None, description="The interfaces that the app can connect to."
+ )
+ aliases: UniqueList[str] | None = pydantic.Field(
+ default=None, description="The app's alternative internal identifiers."
+ )
+ environment: dict[str, str] | None = pydantic.Field(
+ default=None,
+ description="The runtime environment variables available to the snap's apps.",
+ examples=[
+ "environment: {PYTHONPATH: $SNAP/usr/lib/python3/dist-packages, DISABLE_WAYLAND: 1"
+ ],
+ )
+ command_chain: list[str] = pydantic.Field(
+ default_factory=list,
+ description="The sequence of commands to run before the app's command runs. Also applied when running ``snap run --shell``",
+ )
+ sockets: dict[str, Socket] | None = pydantic.Field(
+ default=None,
+ description="The app's sockets.",
+ )
+ daemon_scope: Literal["system", "user"] | None = pydantic.Field(
+ default=None,
+ description="Determines whether the service is run on a system or user instance of systemd.",
+ examples=["daemon-scope: user"],
+ )
+ activates_on: UniqueList[str] | None = pydantic.Field(
+ default=None,
+ description="The slots exposed by the snap to activate the service with D-Bus.",
+ examples=["activates-on: gnome-shell-dbus"],
+ )
+ passthrough: dict[str, Any] | None = pydantic.Field(
+ default=None,
+ description="Attributes to not validate for correctness. Useful for testing experimental snapd features.",
+ )
@pydantic.field_validator("autostart")
@classmethod
@@ -482,10 +594,20 @@ def _validate_aliases(cls, aliases):
class Hook(models.CraftBaseModel):
"""Snapcraft project hook definition."""
- command_chain: list[str] | None = None
- environment: dict[str, str] | None = None
- plugs: UniqueList[str] | None = None
- passthrough: dict[str, Any] | None = None
+ command_chain: list[str] = pydantic.Field(
+ default_factory=list,
+ description="The sequence of commands to run before the app's command runs. Also applied when running ``snap run --shell``",
+ )
+ environment: dict[str, str | None] | None = pydantic.Field(
+ default=None, description="The hook's run-time environment variables."
+ )
+ plugs: UniqueList[str] | None = pydantic.Field(
+ default=None, description="The interfaces that the hook can connect to."
+ )
+ passthrough: dict[str, Any] | None = pydantic.Field(
+ default=None,
+ description="Attributes to not validate for correctness. Useful for testing experimental snapd features.",
+ )
@pydantic.field_validator("command_chain")
@classmethod
@@ -503,17 +625,33 @@ def _validate_plugs(cls, plugs):
class Architecture(models.CraftBaseModel, extra="forbid"):
"""Snapcraft project architecture definition."""
- build_on: str | UniqueList[str]
- build_for: str | UniqueList[str] | None = None
+ build_on: str | UniqueList[str] = pydantic.Field(
+ description="The architectures on which the snap can be built."
+ )
+ build_for: str | UniqueList[str] | None = pydantic.Field(
+ default=None,
+ description="The single element list of the architecture where the snap can be run",
+ )
class ContentPlug(models.CraftBaseModel):
"""Snapcraft project content plug definition."""
- content: str | None = None
- interface: str
- target: str
- default_provider: str | None = None
+ content: str | None = pydantic.Field(
+ default=None,
+ description="The name for the content type.",
+ examples=["content: themes"],
+ )
+ interface: str = pydantic.Field(description="The name of the interface.")
+ target: str = pydantic.Field(
+ description="The path to the producer's files in the snap.",
+ examples=["target: $SNAP/data-dir/themes"],
+ )
+ default_provider: str | None = pydantic.Field(
+ default=None,
+ description="The default snap install to satisfy the interface.",
+ examples=["default-provider: gtk-common-themes"],
+ )
@pydantic.field_validator("default_provider")
@classmethod
@@ -529,8 +667,13 @@ def _validate_default_provider(cls, default_provider):
class Platform(models.Platform):
"""Snapcraft project platform definition."""
- build_on: UniqueList[str] | None = pydantic.Field(min_length=1)
- build_for: SingleEntryList | None = None
+ build_on: UniqueList[str] | None = pydantic.Field(
+ description="The architectures on which the snap can be built.", min_length=1
+ )
+ build_for: SingleEntryList | None = pydantic.Field(
+ default=None,
+ description="The single element list of the architecture the snap is built for.",
+ )
@pydantic.field_validator("build_on", "build_for", mode="before")
@classmethod
@@ -585,11 +728,19 @@ def from_architectures(
class Component(models.CraftBaseModel):
"""Snapcraft component definition."""
- summary: SummaryStr
- description: str
- type: Literal["test", "kernel-modules", "standard"]
- version: VersionStr | None = None
- hooks: dict[str, Hook] | None = None
+ summary: SummaryStr = pydantic.Field(description="The summary of the component.")
+ description: str = pydantic.Field(
+ description="The full description of the component."
+ )
+ type: Literal["test", "kernel-modules", "standard"] = pydantic.Field(
+ description="The component's type."
+ )
+ version: VersionStr | None = pydantic.Field(
+ default=None, description="The version of the component."
+ )
+ hooks: dict[str, Hook] | None = pydantic.Field(
+ default=None, description="Configures the component's hooks."
+ )
MANDATORY_ADOPTABLE_FIELDS = ("version", "summary", "description")
@@ -606,42 +757,137 @@ class Project(models.Project):
# snapcraft's `name` is more general than craft-application
name: ProjectName # type: ignore[assignment]
- build_base: str | None = pydantic.Field(validate_default=True, default=None)
- compression: Literal["lzo", "xz"] = "xz"
+ build_base: str | None = pydantic.Field(
+ validate_default=True,
+ default=None,
+ description="The build environment to use when building the snap",
+ )
+ compression: Literal["lzo", "xz"] = pydantic.Field(
+ default="xz", description="Specifies the algorithm that compresses the snap."
+ )
version: VersionStr | None = None
- donation: UniqueList[str] | None = None
+ donation: UniqueList[str] | None = pydantic.Field(
+ default=None, description="The snap's donation links."
+ )
# snapcraft's `source_code` is more general than craft-application
- source_code: UniqueList[str] | None = None # type: ignore[assignment]
- contact: UniqueList[str] | None = None # type: ignore[assignment]
- issues: UniqueList[str] | None = None # type: ignore[assignment]
- website: UniqueList[str] | None = None
+ source_code: UniqueList[str] | None = pydantic.Field( # type: ignore[assignment]
+ default=None,
+ description="The links to the source code of the snap or the original project.",
+ examples=["source-code: https://example.com/source-code"],
+ )
+ contact: UniqueList[str] | None = pydantic.Field( # type: ignore[reportIncompatibleVariableOverride]
+ default=None, description="The snap author's contact links and email addresses."
+ )
+ issues: UniqueList[str] | None = pydantic.Field( # type: ignore[reportIncompatibleVariableOverride]
+ default=None,
+ description="The links and email addresses for submitting issues, bugs, and feature requests.",
+ )
+ website: UniqueList[str] | None = pydantic.Field(
+ default=None, description="The links to the original software's web pages."
+ )
type: Literal["app", "base", "gadget", "kernel", "snapd"] | None = None
- icon: str | None = None
- confinement: Literal["classic", "devmode", "strict"]
+ icon: str | None = pydantic.Field(
+ default=None, description="The path to the snap's icon."
+ )
+ confinement: Literal["classic", "devmode", "strict"] = pydantic.Field(
+ description="The amount of isolation the snap has from the host system."
+ )
layout: (
dict[str, SingleEntryDict[Literal["symlink", "bind", "bind-file", "type"], str]]
| None
- ) = None
- grade: Literal["stable", "devel"] | None = None
- architectures: list[str | Architecture] | None = None
+ ) = pydantic.Field(
+ default=None, description="The file layouts in the execution environment."
+ )
+ grade: Literal["stable", "devel"] | None = pydantic.Field(
+ default=None, description="Publication guardrail for the snap"
+ )
+ architectures: list[str | Architecture] | None = pydantic.Field(
+ default=None,
+ description="Determines which instruction set architectures the snap builds on and runs on.",
+ examples=[
+ "architectures: [amd64, riscv64]",
+ "architectures: [{build-on: [amd64], build-for: [amd64]}]",
+ "architectures: [{build-on: [amd64, riscv64], build-for: [riscv64]}]",
+ ],
+ )
_architectures_in_yaml: bool | None = None
- platforms: dict[str, Platform] | None = None # type: ignore[assignment,reportIncompatibleVariableOverride]
- assumes: UniqueList[str] = pydantic.Field(default_factory=list)
- hooks: dict[str, Hook] | None = None
- passthrough: dict[str, Any] | None = None
- apps: dict[str, App] | None = None
- plugs: dict[str, ContentPlug | Any] | None = None
- slots: dict[str, Any] | None = None
- lint: Lint | None = None
- epoch: str | None = None
- adopt_info: str | None = None
- system_usernames: dict[str, Any] | None = None
- environment: dict[str, str | None] | None = None
- build_packages: Grammar[list[str]] | None = None
- build_snaps: Grammar[list[str]] | None = None
- ua_services: set[str] | None = None
- provenance: str | None = None
- components: dict[ProjectName, Component] | None = None
+ platforms: dict[str, Platform] | None = pydantic.Field( # type: ignore[assignment,reportIncompatibleVariableOverride]
+ default=None,
+ description="Determines which instruction set architectures the snap builds on and runs on.",
+ examples=[
+ "platforms: {amd64: {build-on: [amd64], build-for: [amd64]}, arm64: {build-on: [amd64, arm64], build-for: [arm64]}}"
+ ],
+ )
+ assumes: UniqueList[str] = pydantic.Field(
+ default_factory=list,
+ description="The snapd features or minimum version of snapd required by the snap.",
+ examples=["assumes: [snapd2.66]", "assumes: [common-data-dir]"],
+ )
+ hooks: dict[str, Hook] | None = pydantic.Field(
+ default=None, description="Configures the snap's hooks."
+ )
+ passthrough: dict[str, Any] | None = pydantic.Field(
+ default=None,
+ description="Attributes to not validate for correctness. Useful for testing experimental snapd features.",
+ )
+ apps: dict[str, App] | None = pydantic.Field(
+ default=None,
+ description="Declares the individual programs and services that the snap runs.",
+ examples=["apps: {foo-app: {command: bin/foo-app}}"],
+ )
+ plugs: dict[str, ContentPlug | Any] | None = pydantic.Field(
+ default=None, description="Declares the snap's plugs."
+ )
+ slots: dict[str, Any] | None = pydantic.Field(
+ default=None, description="Declares the snap's slots."
+ )
+ lint: Lint | None = pydantic.Field(
+ default=None,
+ description="The linter configuration.",
+ examples=["lint: {ignore: [classic, library: [usr/lib/**/libfoo.so*]]}"],
+ )
+ epoch: str | None = pydantic.Field(
+ default=None, description="The epoch associated with this version of the snap."
+ )
+ adopt_info: str | None = pydantic.Field(
+ default=None,
+ description=textwrap.dedent(
+ """\
+ Selects a part to inherit metadata from and reuse for the snap's metadata.
+
+ Required if one of ``version``, ``summary``, or ``description`` isn't set."""
+ ),
+ examples=["adopt-info: foo-part"],
+ )
+ system_usernames: dict[str, Any] | None = pydantic.Field(
+ default=None,
+ description="The system usernames that the snap can use to run services.",
+ )
+ environment: dict[str, str | None] | None = pydantic.Field(
+ default=None, description="The snap's run-time environment variables."
+ )
+ build_packages: Grammar[list[str]] | None = pydantic.Field(
+ default=None,
+ description="Base dependent build dependencies required to build parts.",
+ examples=["build-packages: libssl-dev, libyaml-dev"],
+ )
+ build_snaps: Grammar[list[str]] | None = pydantic.Field(
+ default=None,
+ description="Snap dependencies required to build parts.",
+ examples=["build-snaps: go/1.22/stable, yq"],
+ )
+ ua_services: set[str] | None = pydantic.Field(
+ default=None,
+ description="The Ubuntu Pro services to enable when building the snap.",
+ )
+ provenance: str | None = pydantic.Field(
+ default=None,
+ description="The primary-key header for snaps signed by third parties.",
+ )
+ components: dict[ProjectName, Component] | None = pydantic.Field(
+ default=None,
+ description="Declares the components to pack in conjunction with the snap.",
+ )
@override
@classmethod
@@ -970,7 +1216,9 @@ class _GrammarAwarePart(_GrammarAwareModel):
class GrammarAwareProject(_GrammarAwareModel):
"""Project definition containing grammar-aware components."""
- parts: dict[str, _GrammarAwarePart]
+ parts: dict[str, _GrammarAwarePart] = pydantic.Field(
+ description="Declares the self-contained software pieces needed to create the snap."
+ )
@classmethod
def validate_grammar(cls, data: dict[str, Any]) -> None: