How to add a custom source type to an application

An application may need additional source types not included in craft-parts by default. In this case, you can create and register a new non-default source handler.

Warning

New source handlers may be added in minor releases of craft-parts. If a custom source handler shares a source_type value with a new source handler, registering a custom source handler will override the existing source handler.

Some source types are considered mandatory and cannot be overridden or unregistered. In a major release, the set of mandatory source types may change. At that point, an application-defined source type may not override the mandatory source type.

Write a new source handler

The first step in adding a new source handler is to write one. The components of a source handler are its source model (a Pydantic model that defines the source-* keys that may be used in a part) and the handler, a class that defines how the source type behaves during the PULL step.

The Pydantic model is a child class of BaseSourceModel. The only mandatory field is source_type.

class RsyncDirectorySourceModel(sources.BaseSourceModel, frozen=True):
    pattern = "^rsync://"
    source_type: Literal["rsync"] = "rsync"

The pattern attribute allows Craft Parts to infer the source based on a regular expression. The first source handler with a matching regular expression will be used, with built-in source types matching before externally registered source types.

Once this is defined, a SourceHandler is needed to define the PULL behaviour of the source type.

class RsyncSource(sources.SourceHandler):
    source_model = RsyncDirectorySourceModel

    @override
    def pull(self) -> None:
        self._run(
            [
                "rsync",
                "--archive",
                "--delete",
                self.source,
                self.part_src_dir.as_posix(),
            ]
        )

Note

Craft Parts does not install any required tools for custom source handlers. The handler in this example will fail on a machine that does not have rsync installed before the part is pulled.

Register the source handler

Once created, a source must be registered to be used. This must occur before entering the ExecutionContext with the LifecycleManager’s action_executor() method.

sources.register(RsyncSource)

Run the lifecycle

With those steps completed, the new source handler is ready to use. A parts.yaml file such as the following will use the example rsync source type:

parts:
  my-part:
    plugin: make
    source: rsync://code-host.internal/my-app

After loading the parts into a YAML structure, all that’s left is to run it:

lcm = LifecycleManager(
    parts,
    application_name="rsync_parts",
    cache_dir=pathlib.Path.home() / ".cache",
)
with lcm.action_executor() as ctx:
    ctx.execute(Action("my-part", Step.PULL))