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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ docs: ## Build documentation

.PHONY: docs-auto
docs-auto: ## Build and host docs with sphinx-autobuild
uv run --extra docs sphinx-autobuild -b html --open-browser --port=8080 --watch $(PROJECT) -W docs docs/_build
uv run --extra docs sphinx-autobuild -b html --open-browser --port=8080 --watch $(PROJECT) --ignore "*.kate-swp" docs docs/_build

---------------- : ## ----------------

Expand Down
2 changes: 0 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@
exclude_patterns = [
# Exclude the empty quadrants
"tutorials/index.rst",
"how-to/index.rst",
"explanation/index.rst",
]

# endregion
Expand Down
4 changes: 3 additions & 1 deletion docs/explanation/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
.. _explanation:

Explanation
***********
===========

.. toctree::
:maxdepth: 1

platforms-definitions
59 changes: 59 additions & 0 deletions docs/explanation/platforms-definitions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
.. py:module:: craft_platforms

Platforms definitions
=====================

In general, the most complex part of the data structures that can form a build
plan is the ``platforms`` definition. However, the general rules for developers
using ``craft-platforms`` are:

0. Each platform entry declares a single artifact (or artifact set), possibly with
multiple ways to build it.
#. Each platform may only be built once per run.
#. The order of ``BuildInfo`` objects in the build plan is not meaningful for the
selection process.
#. An error in a build does not necessitate trying a different ``BuildInfo`` for
the same platform.
#. A consumer of the build plan may filter build plans using any rules not in
conflict with the rules here.
#. It is an error if, after filtering, the build plan is empty.
#. If after filtering, multiple ``BuildInfo`` objects remain with the same
``platform``, any one of those may be used regardless of their order.
#. If a platform can be built under the rules above, it must be built unless further
specified by the user.

Valid filtering rules
---------------------

While not an exhaustive list, the following rules are examples of valid rules that may
be used to filter the build plan:

- Only build on the current host architecture (use
:meth:`DebianArchitecture.from_host`).
- Only build on the host's running distro and series.
- Only build for a specified architecture.
- Only build for a specified platform.
- Only build for build bases with the distribution ``"debian"``.
- Only build for build bases with the series ``"12"``.
- Only build on an arbitrary list of architectures.
- Do not cross-compile.

The last of these is not as straightforward as checking
``if info.build_on == info.build_for``, as the string ``"all"`` is a valid ``build_for``
value and should not be considered cross-compiling.

Selecting a ``BuildInfo``
-------------------------

In some cases, an application may be left with multiple ``BuildInfo`` objects that match
a single platform even after filtering above. Any of the following methods (as well as
many others) are valid for further filtering which ``BuildInfo`` to use:

- Randomly select a ``BuildInfo`` item.
- Prefer not to cross-compile (but allow it if no native builds are available).
- Prefer the newest build base.
- Prefer a specific architecture based on availability.
- Reject the build (failing the entire build)

Currently, the only known **invalid** way to proceed at this point is to ignore the
platform with duplicates, but continue the build.
94 changes: 94 additions & 0 deletions docs/how-to/filter-build-plans.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
Filter a build plan
===================

A craft-platforms consumer should attempt to build exactly one artifact for as many
platforms as are feasible. To do this, the build plan must be filtered. In general, a
set of steps for filtering a build plan are:

1. Remove un-buildable ``BuildInfo`` objects.
2. For each remaining platform, select one ``BuildInfo``.

Consider the following ``platforms`` definition for a nonexistent ``fakecraft``:

.. code-block:: yaml

platforms:
laptop:
build-on:
- ubuntu@24.04:amd64
- ubuntu@24.04:riscv64
- debian@12:i386
- ubuntu@4.10:riscv64
build-for: [ubuntu@24.04:amd64]
nostalgia:
build-on: [windows@5.0:i386]
build-for: [windows@5.0:i386]
phone:
build-on:
- ubuntu@22.04:s390x
- ubuntu@24.04:riscv64
build-for: [ubuntu@24.04:arm64]
jpeg:
build-on:
- debian@12:i386
- ubuntu@24.04:amd64
- sunos@4:i386
build-for: [all@all:all]

Each key under ``platforms`` can be considered a desired artifact with one or more possible
ways to build it. For example, the ``nostalgia`` platform has only a single

Modern build infrastructure such as Launchpad or Open Build Service
is unlikely to have any builders that can build the ``nostalgia`` platform, as Windows 2000
left extended support over a decade ago. Likewise, the ``jpeg`` platform is not likely to
find any takers to build on ``sunos@4:i386``. Other ``BuildItem`` objects may be removed by
availability of hardware or operating systems. If a build system has access to any Ubuntu
version on any hardware (even the rare ``ubuntu@4.10:riscv64``), it could filter the build
plan as follows:

.. literalinclude:: filter_build_plans.py
:start-at: def filter_build_plan(
:end-before: # :docs:end_filter

This would result in the following filtered build plan:

.. literalinclude:: filter_build_plans.py
:start-at: ubuntu_only_build_plan = [
:end-before: # :docs:end_filtered_build_plan

This still results in three options for the ``laptop`` platform and two for ``phone``.
A build plan should not be considered ordered. The order does not state a user preference
and can change. Rather, at this point the builder may choose one of each at its preference.
For example, a builder with excess RISC-V infrastructure and a preference for the oldest
build base may result in this final build plan:

.. literalinclude:: filter_build_plans.py
:start-at: oldest_with_riscv_preference = [
:end-before: # :docs:end_oldest_with_riscv_preference

A builder which does not do cross-compilation may not be able to build for the ``phone``
platform at all:

.. literalinclude:: filter_build_plans.py
:start-at: native_only_prefer_amd64 = [
:end-before: # :docs:end_native_only_prefer_amd64

Build plans that result in errors
---------------------------------

craft-platforms will always create an **exhaustive build plan**, but not all build plans
result in something buildable. If, after filtering for builds it is capable of running,
a craft-platforms consumer is left with an empty build plan, it is the consumer's
responsibility to gracefully exit with an error. The following definition could result in a
valid ``fakecraft`` build plan, but would create an error in any known environment:

.. code-block:: yaml

platforms:
you-shall-not-build:
build-on:
- debian@3.0:riscv64 # Nobody has one of these!
build-for: [all@all:all]

Whether a build plan where some, but not all, platforms can be built is considered
erroneous is undefined and may be decided by the consumer.
157 changes: 157 additions & 0 deletions docs/how-to/filter_build_plans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from typing import Iterable, Sequence
from craft_platforms import BuildInfo, DistroBase, DebianArchitecture

build_plan = [
BuildInfo(
platform="laptop",
build_for=DebianArchitecture.AMD64,
build_on=DebianArchitecture.AMD64,
build_base=DistroBase("ubuntu", "24.04"),
),
BuildInfo(
platform="laptop",
build_for=DebianArchitecture.AMD64,
build_on=DebianArchitecture.RISCV64,
build_base=DistroBase("ubuntu", "24.04"),
),
BuildInfo(
platform="laptop",
build_for=DebianArchitecture.AMD64,
build_on=DebianArchitecture.I386,
build_base=DistroBase("debian", "12"),
),
BuildInfo(
platform="laptop",
build_for=DebianArchitecture.AMD64,
build_on=DebianArchitecture.RISCV64,
build_base=DistroBase("ubuntu", "4.10"),
),
BuildInfo(
platform="nostalgia",
build_for=DebianArchitecture.I386,
build_on=DebianArchitecture.I386,
build_base=DistroBase("windows", "5"),
),
BuildInfo(
platform="phone",
build_for=DebianArchitecture.ARM64,
build_on=DebianArchitecture.S390X,
build_base=DistroBase("ubuntu", "22.04"),
),
BuildInfo(
platform="phone",
build_for=DebianArchitecture.ARM64,
build_on=DebianArchitecture.RISCV64,
build_base=DistroBase("ubuntu", "24.04"),
),
BuildInfo(
platform="jpeg",
build_for="all",
build_on=DebianArchitecture.I386,
build_base=DistroBase("debian", "12"),
),
BuildInfo(
platform="jpeg",
build_for="all",
build_on=DebianArchitecture.AMD64,
build_base=DistroBase("ubuntu", "24.04"),
),
BuildInfo(
platform="jpeg",
build_for="all",
build_on=DebianArchitecture.I386,
build_base=DistroBase("sunos", "4"),
),
]
# :docs:end_build_plan


def filter_build_plan(exhaustive_build_plan):
"""Filter the build plan to only include Ubuntu runners."""
return [
info
for info in exhaustive_build_plan
if info.build_base.distribution == "ubuntu"
]
# :docs:end_filter


ubuntu_only_build_plan = [
BuildInfo(
platform="laptop",
build_for=DebianArchitecture.AMD64,
build_on=DebianArchitecture.AMD64,
build_base=DistroBase("ubuntu", "24.04"),
),
BuildInfo(
platform="laptop",
build_for=DebianArchitecture.AMD64,
build_on=DebianArchitecture.RISCV64,
build_base=DistroBase("ubuntu", "24.04"),
),
BuildInfo(
platform="laptop",
build_for=DebianArchitecture.AMD64,
build_on=DebianArchitecture.RISCV64,
build_base=DistroBase("ubuntu", "4.10"),
),
BuildInfo(
platform="phone",
build_for=DebianArchitecture.ARM64,
build_on=DebianArchitecture.S390X,
build_base=DistroBase("ubuntu", "22.04"),
),
BuildInfo(
platform="phone",
build_for=DebianArchitecture.ARM64,
build_on=DebianArchitecture.RISCV64,
build_base=DistroBase("ubuntu", "24.04"),
),
BuildInfo(
platform="jpeg",
build_for="all",
build_on=DebianArchitecture.AMD64,
build_base=DistroBase("ubuntu", "24.04"),
),
]
# :docs:end_filtered_build_plan
for left, right in zip(filter_build_plan(build_plan), ubuntu_only_build_plan):
assert left == right, (left, right)

oldest_with_riscv_preference = [
BuildInfo(
platform="laptop",
build_for=DebianArchitecture.AMD64,
build_on=DebianArchitecture.RISCV64,
build_base=DistroBase("ubuntu", "4.10"),
),
BuildInfo(
platform="phone",
build_for=DebianArchitecture.ARM64,
build_on=DebianArchitecture.RISCV64,
build_base=DistroBase("ubuntu", "24.04"),
),
BuildInfo(
platform="jpeg",
build_for="all",
build_on=DebianArchitecture.AMD64,
build_base=DistroBase("ubuntu", "24.04"),
),
]
# :docs:end_oldest_with_riscv_preference

native_only_prefer_amd64 = [
BuildInfo(
platform="laptop",
build_for=DebianArchitecture.AMD64,
build_on=DebianArchitecture.AMD64,
build_base=DistroBase("ubuntu", "24.04"),
),
BuildInfo(
platform="jpeg",
build_for="all",
build_on=DebianArchitecture.AMD64,
build_base=DistroBase("ubuntu", "24.04"),
),
]
# :docs:end_native_only_prefer_amd64
2 changes: 2 additions & 0 deletions docs/how-to/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ How-to guides

.. toctree::
:maxdepth: 1

filter-build-plans
14 changes: 14 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,28 @@ Craft Platforms
:maxdepth: 1
:hidden:

how-to/index
reference/index
explanation/index

.. grid:: 1 1 2 2

.. grid-item-card::

.. grid-item-card:: :ref:`How-to guides <howto>`

**Step-by-step guides** covering key operations and common tasks

.. grid:: 1 1 2 2

.. grid-item-card:: :ref:`Reference <reference>`

**Technical information** about Craft Platforms

.. grid-item-card:: :ref:`explanation`

**Discussion and clarification** of key topics

Project and community
=====================

Expand Down