Skip to content
Draft
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 debcraft/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"""Main Debcraft Application."""

import craft_application
import craft_parts
from typing_extensions import override

from debcraft import models

Expand All @@ -29,3 +31,8 @@

class Application(craft_application.Application):
"""Debcraft application definition."""

@override
def _enable_craft_parts_features(self) -> None:
"""Enable partitions for packages."""
craft_parts.Features(enable_partitions=True)
54 changes: 54 additions & 0 deletions debcraft/control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# This file is part of debcraft.
#
# Copyright 2025 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 warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, 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/>.

"""Debian control file encoder."""

from typing import TextIO

from debcraft import models


class Encoder:
"""Encoder for Debian control file format."""

def __init__(self, f: TextIO) -> None:
self._file = f

def encode(self, model: models.DebianControl) -> None:
"""Encode the model."""
for name, field in model.__class__.model_fields.items():
value = getattr(model, name)
if value is None:
continue

key = field.alias or name

match value:
case None:
continue
case str() if "\n" in value:
lines = value.splitlines()
self._file.write(f"{key}: {lines[0]}\n")
for line in lines[1:]:
if line.strip() == "":
self._file.write(" .\n")
else:
self._file.write(f" {line}\n")
case list():
line = ", ".join(map(str, value)) # pyright: ignore[reportUnknownVariableType,reportUnknownArgumentType]
self._file.write(f"{key}: {line}\n")
case _:
self._file.write(f"{key}: {value}\n")
38 changes: 38 additions & 0 deletions debcraft/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2025 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/>.

"""Debcraft error definitions."""

from craft_cli import CraftError


class DebcraftError(CraftError):
"""Failure in a Debcraft operation."""


class FeatureNotImplemented(DebcraftError):
"""Attempt to use an unimplemented feature."""

def __init__(self, msg: str) -> None:
super().__init__(f"Command or feature not implemented: {msg}")


class PartsLifecycleError(DebcraftError):
"""Error during parts processing."""


class ProjectValidationError(DebcraftError):
"""Error validating debcraft.yaml."""
4 changes: 3 additions & 1 deletion debcraft/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

from debcraft.models.metadata import Metadata
from debcraft.models.project import Project
from debcraft.models.package import Package
from debcraft.models.control import DebianControl


__all__ = ["Project", "Metadata"]
__all__ = ["Project", "Package", "DebianControl", "Metadata"]
50 changes: 50 additions & 0 deletions debcraft/models/control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# This file is part of debcraft.
#
# Copyright 2025 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 warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, 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/>.

"""Debian control file model for Debcraft."""

from craft_application import models
from pydantic import ConfigDict


def _field_alias(field_name: str) -> str:
parts = field_name.replace("_", "-").split("-")
capitalized_parts = [p.capitalize() for p in parts]
return "-".join(capitalized_parts)


class DebianControl(models.CraftBaseModel):
"""Debian control file definition."""

model_config = ConfigDict(alias_generator=_field_alias, populate_by_name=True)

package: str
source: str
version: str
architecture: str | list[str]
maintainer: str
installed_size: int
depends: list[str] | None = None
recommends: list[str] | None = None
conflicts: list[str] | None = None
breaks: list[str] | None = None
replaces: list[str] | None = None
provides: list[str] | None = None
section: str
priority: str
description: str
original_maintainer: str | None = None
uploaders: list[str] | None = None
19 changes: 16 additions & 3 deletions debcraft/models/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Model for defining deb binary packages."""

from typing import Literal
from typing import Literal, cast

import pydantic
from craft_application import models
Expand All @@ -31,8 +31,11 @@ class Package(models.CraftBaseModel):
See: https://www.debian.org/doc/debian-policy/ch-controlfields.html
"""

architectures: Literal["any", "all"] | list[DebianArchitecture]
description: str | None = None # Only none for the only pkg
architectures: Literal["any", "all"] | list[DebianArchitecture] | None = None
summary: str | None = None # defaults to the project summary
description: str | None = None # defaults to the project description

version: str | None = None # defaults to the project version

# These need validating: https://github.com/canonical/debcraft/issues/42
# https://www.debian.org/doc/debian-policy/ch-relationships.html#s-binarydeps
Expand All @@ -50,3 +53,13 @@ class Package(models.CraftBaseModel):

Use of this key indicates something incomplete in debcraft.
"""

def get_architecture(self) -> str | list[str] | None:
"""Get the formatted package architecture."""
if self.architectures in ("all", "any"):
return cast(str, self.architectures)

if self.architectures:
return [str(x) for x in self.architectures]

return None
28 changes: 28 additions & 0 deletions debcraft/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from craft_application import models
from typing_extensions import Self

from debcraft import errors
from debcraft.models.package import Package

DEBIAN_PACKAGE_NAME_REGEX = r"^[a-z0-9][a-z0-9.+-]+$"
Expand Down Expand Up @@ -97,3 +98,30 @@ def _validate_adopt_info_part_exists(self) -> Self:
if self.adopt_info and self.adopt_info not in self.parts:
raise ValueError("'adopt-info' field must refer to the name of a part.")
return self

def get_package(self, name: str) -> Package:
"""Obtain the package definition for the given package name."""
if not self.packages:
raise errors.DebcraftError("no packages defined")

package = self.packages.get(name)
if not package:
raise errors.DebcraftError(f"package {name} is not defined")

return package


class PackagesProject(models.CraftBaseModel, extra="ignore"):
"""Project definition containing only package data."""

packages: dict[DebianPackageName, Package] | None = None

def get_partitions(self) -> list[str] | None:
"""Get a list of partitions based on the project's packages.

:returns: A list of packages formatted as ['default', 'package/<name>', ...]
"""
if not self.packages:
return ["default"]

return ["default", *[f"package/{name}" for name in self.packages]]
3 changes: 3 additions & 0 deletions debcraft/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ def register_services() -> None:
"""Register debcraft services to the service factory."""
ServiceFactory.register("package", "Package", module="debcraft.services.package")
ServiceFactory.register("project", "Project", module="debcraft.services.project")
ServiceFactory.register(
"lifecycle", "Lifecycle", module="debcraft.services.lifecycle"
)


__all__ = ["BuildPlan", "ServiceFactory"]
72 changes: 72 additions & 0 deletions debcraft/services/lifecycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# This file is part of debcraft.
#
# Copyright 2025 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 warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, 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/>.
#
"""Debcraft Lifecycle Service."""

from pathlib import Path

from craft_application import LifecycleService
from craft_parts import ProjectInfo

from debcraft import errors


class Lifecycle(LifecycleService):
"""Debcraft specialization of the Lifecycle Service."""

def get_prime_dir(self, package: str | None = None) -> Path:
"""Get the prime directory path for the default prime dir or a package.

:param package: Name of the package to get the prime directory for.

:returns: The default prime directory or a package's prime directory.

:raises DebcraftError: If the package does not exist.
"""
try:
return self.prime_dirs[package]
except KeyError as err:
raise errors.DebcraftError(
f"Could not get prime directory for package {package!r} "
"because it does not exist."
) from err

@property
def prime_dirs(self) -> dict[str | None, Path]:
"""Return a mapping of package names to prime directories.

'None' maps to the default prime directory.
"""
return _get_prime_dirs_from_project(self._lcm.project_info)


def _get_prime_dirs_from_project(project_info: ProjectInfo) -> dict[str | None, Path]:
"""Get a mapping of package names to prime directories from a ProjectInfo.

'None' maps to the default prime directory.

:param project_info: The ProjectInfo to get the prime directory mapping from.
"""
partition_prime_dirs = project_info.prime_dirs
package_prime_dirs: dict[str | None, Path] = {None: project_info.prime_dir}

# strip 'component/' prefix so that the package name is the key
for partition, prime_dir in partition_prime_dirs.items():
if partition and partition.startswith("package/"):
package = partition.split("/", 1)[1]
package_prime_dirs[package] = prime_dir

return package_prime_dirs
Loading