<a id="how-build-sdk"></a>

# How to build an SDK

<!-- @tests in tests/docs-how-to/build-an-sdk/task.yaml -->
<!-- @artefact SDK -->
<!-- @artefact SDK definition -->
<!-- @artefact SDK hook -->
<!-- @artefact SDK part -->
<!-- @artefact sdkcraft (CLI) -->
<!-- @artefact setup-base -->
<!-- @artefact setup-project -->
<!-- @artefact check-health -->
<!-- @artefact save-state -->
<!-- @artefact restore-state -->
<!-- @artefact try SDK -->

A standalone SDK is a **SDKcraft** project
with its own `sdkcraft.yaml`,
its own `hooks/` tree,
and the parts and interfaces
that describe what it ships
and how it integrates with workshops.
Build one by laying out the project,
declaring parts and interfaces,
authoring the lifecycle hooks,
and exercising the result locally
before any thought of publishing.

## Prerequisites

Before starting,
ensure the following are in place:

- **SDKcraft** is installed.
- LXD 6.6 or later is running on the host.
- **Workshop** is installed and configured
  so that you can launch a workshop
  to try the SDK against.

<a id="how-build-sdk-scaffold"></a>

## Start from the template

New SDKs start from
[canonical/template-sdk](https://github.com/canonical/template-sdk),
a GitHub-template repository
that ships the project skeleton:

- a `sdkcraft.yaml` ready to be filled in,
- a `hooks/` tree with stubs for the lifecycle hooks,
- a `VERSION` file pinning the upstream release the SDK wraps,
- a `renovate.json` that tracks upstream releases on a long-lived
  version branch and opens PRs to bump `VERSION` as they ship,
- CI workflows under `.github/workflows/` that build on pull requests
  and upload to the SDK Store on push to the version branch,
- a README template aligned to the rest of the project shape.

Use it via GitHub’s “Use this template” button,
or **git clone** if you don’t host on GitHub.
The choice that follows is how to fill the template in.

### With the sdk-designer skill

[template-sdk](https://github.com/canonical/template-sdk)
also ships an agentic skill named `sdk-designer`.
The skill runs an interactive scaffolding conversation:
it asks about the software to package,
the target platforms,
and which interfaces and hooks are needed,
then writes the corresponding files into the template.

1. Aim the agent at the new repository.
2. Run `/sdk-designer` and answer the prompts.
3. Review the generated files
   and adjust where the skill’s defaults don’t match your case.

### By editing the template directly

Without the agentic skill,
edit the template files in place.
Fill `sdkcraft.yaml` per [Fill in the metadata](#how-build-sdk-metadata),
replace each hook stub under `hooks/` per [Author the hooks](#how-build-sdk-hooks),
update `VERSION` to the upstream release you intend to ship first,
and adjust `renovate.json` if the upstream project lives somewhere
other than the GitHub release page the default config targets.

<a id="how-build-sdk-metadata"></a>

## Fill in the metadata

**SDKcraft** needs four pieces of metadata to identify and build the SDK:
`name`, `version`, a one-line `summary`,
and the `platforms` to build for.
Add `license` to declare the SDK’s licensing terms,
and `description` for a multi-line write-up:

```yaml
name: <NAME>
version: "<VERSION>"
summary: One-line description of the SDK
description: |
  A longer description that explains
  what the SDK packages
  and any noteworthy behavior.
license: MIT
platforms:
  ubuntu@22.04:amd64:
  ubuntu@24.04:amd64:
```

Use the SDK’s upstream version for `version`
when the SDK wraps a single tool;
keep it quoted so that values like `1.0` aren’t parsed as floats.

**SDKcraft** builds one artifact per entry in `platforms`.
Each entry pairs an Ubuntu base
with a CPU architecture from the Debian naming scheme
(`amd64`, `arm64`, and so on).
For SDKs that don’t ship compiled binaries,
use `all` instead of a specific architecture.

## Define parts

[Parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts) describe how **SDKcraft**
obtains the SDK’s payload at build time.
A small SDK often gets by with a single part;
larger SDKs split work along functional boundaries.

For a binary downloaded from a release page,
use the `dump` plugin with a tarball source:

```yaml
parts:
  <NAME>:
    plugin: dump
    source: https://example.com/releases/v${CRAFT_PROJECT_VERSION}/<NAME>-linux-${CRAFT_ARCH_BUILD_FOR}.tar.gz
    source-type: tar
```

`$*CRAFT_PROJECT_VERSION*` and `$*CRAFT_ARCH_BUILD_FOR*`
expand at build time
from the `version` field
and the platform **SDKcraft** is currently building for.

If the SDK ships supporting files,
add them as separate parts with the `file` source type:

```yaml
parts:
  <NAME>:
    plugin: dump
    source: https://example.com/releases/v${CRAFT_PROJECT_VERSION}/<NAME>-linux-${CRAFT_ARCH_BUILD_FOR}.tar.gz
    source-type: tar
  service-unit:
    plugin: dump
    source: <NAME>.service
    source-type: file
```

For source-built SDKs,
the `rust`, `go`, and `python` plugins
take over from `dump`.
The Craft Parts
[plugin reference](https://documentation.ubuntu.com/craft-parts/latest/reference/plugins/)
lists every plugin and its options.

## Declare plugs and slots

Plugs and slots wire the SDK
to host resources and to other SDKs in the workshop.
A plug requests access to something the workshop provides;
a slot offers something the SDK exposes.

The most common patterns are:

- A `mount` plug for cache or model directories
  that should survive **workshop refresh**.
- A `gpu` plug for SDKs that need GPU acceleration.
- A `tunnel` slot for services that expose a network endpoint.

For example, an SDK that runs a long-lived HTTP service
and caches data under `~/.cache/<NAME>/`
declares both a plug and a slot:

```yaml
plugs:
  cache:
    interface: mount
    workshop-target: /home/workshop/.cache/<NAME>

slots:
  api:
    interface: tunnel
    endpoint: 8080
```

The `workshop-target` value is the in-workshop path
that **Workshop** backs with persistent host storage;
SDKs can’t pick the host path directly,
which prevents them from reaching arbitrary host files.
Workshop users can override the host side at run time
with **workshop remount**.

<a id="how-build-sdk-hooks"></a>

## Author the hooks

Hooks are the run-time logic of an SDK.
**Workshop** runs them at specific lifecycle stages
of the workshop they’re installed in.
All hooks are shell scripts in `hooks/<HOOK-NAME>`
and are linted with [ShellCheck](https://www.shellcheck.net/)
when the SDK is packed.

`SDK` is available inside every hook.
It points to the SDK’s installation root inside the workshop;
use it to reference files the SDK ships,
for example `"$SDK/bin/<NAME>"`.

`SDK_STATE_DIR` is available only inside
`save-state` and `restore-state`.
It points to a temporary directory
**Workshop** creates for one **workshop refresh** cycle:
`save-state` writes to it before the old workshop is destroyed,
and `restore-state` reads from it
once the new workshop is up.
The directory is gone as soon as the workshop stops,
so don’t use it for long-lived data;
back that with a `mount` plug or store it in the project directory instead.

**Workshop** recognizes five hook names:
`setup-base`, `setup-project`, `check-health`,
`save-state`, and `restore-state`.
A useful SDK rarely needs all five.

### setup-base

`setup-base` runs as root
when the SDK is first installed in a workshop
and on every **workshop refresh**.
It is the place for system-wide configuration:
installing **apt** packages,
wiring `PATH`,
and laying down service unit files.

A minimal `setup-base` for a single-binary SDK
adds the SDK’s `bin/` directory to the system `PATH`:

```shell
cat <<EOF > /etc/profile.d/<NAME>.sh
export PATH="$SDK/bin:\$PATH"
EOF
```

Place system-wide environment variables under `/etc/profile.d/`
so they apply across shells.
Avoid editing `/etc/bash.bashrc` directly;
**Workshop** may support more than one shell
and `/etc/profile.d/` is the portable seam.

Inside `setup-base`,
**apt** is preconfigured to skip recommended and suggested packages
and to answer “yes” to confirmation prompts,
so **apt-get install** calls can be terse:

```shell
apt-get update
apt-get install build-essential cmake ninja-build
```

Operations performed in `setup-base` become part of the workshop’s
[base snapshot](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-sdks),
so subsequent refreshes start from a warmed-up state.

### setup-project

`setup-project` runs as the `workshop` user
after `setup-base`,
once the project directory is mounted
and interface plugs and slots are connected.
This is the right place for per-user configuration:
activating virtual environments,
enabling user **systemd** services,
and writing files under `/home/workshop/`.

A typical `setup-project` for an SDK that ships a service unit
installs and starts the service as a user-level systemd unit:

```shell
install -D --mode=644 --target-directory ~/.config/systemd/user \
    "$SDK/<NAME>.service"

systemctl --user daemon-reload
systemctl --user enable --now <NAME>
```

User-level **systemd** services are preferred over root-level ones
because they cleanly tie their lifetime
to the `workshop` user’s session
and don’t require `sudo`.

Operations in `setup-project` don’t go into the base snapshot,
so use it for anything that depends on project-specific state
or that should be re-evaluated on every launch.

### check-health

`check-health` runs as root once every other hook has finished:
on **workshop launch**,
after `setup-project` has run for every SDK in the workshop;
on **workshop refresh**,
after `restore-state` has run for every SDK.
**Workshop** also re-runs `check-health` on demand
when it reassesses the workshop’s state.
Use it to verify the SDK is functional
and to report status back through **workshopctl set-health**.

The canonical pattern is to exercise a real entry point
and channel any error output back to the user:

```shell
if ! output=$(sudo -u workshop --login <NAME> --version 2>&1); then
  workshopctl set-health error "$output"
  exit
fi
workshopctl set-health okay
```

Run the command as `sudo -u workshop --login`
so it picks up the same environment
that a workshop user would see interactively;
this catches PATH wiring bugs in `setup-base`
that would otherwise stay hidden.

Three health states are meaningful:

- `okay`: The SDK is functional.
- `error`: Something is wrong.
  Supply a message that helps a user understand what failed.
- `waiting`: The hook should be retried.
  **Workshop** retries up to ten times, once per second.
  If the SDK never reaches `okay` or `error`,
  the health flips to `error` after those retries are exhausted.

### save-state and restore-state

`save-state` and `restore-state` are an optional pair
that only runs at **workshop refresh**.
`save-state` runs in the old SDK revision,
before **Workshop** destroys the old workshop.
`restore-state` runs in the new SDK revision,
after `setup-project` has finished for every SDK.
Their job is to carry data
across the refresh boundary in `SDK_STATE_DIR`.

Because `restore-state` runs after `setup-project`,
restored files aren’t yet present
while `setup-project` is still executing;
keep any setup that depends on restored state
inside `restore-state` itself,
or have `check-health` retry by reporting `waiting`
until the state shows up.

Use them when the SDK keeps configuration or transient data
that doesn’t already live in a mount plug or a project file.
Both hooks run as `root`,
so reference the `workshop` user’s home explicitly
rather than relying on `~`:

```shell
if [ -d /home/workshop/.config/<NAME> ]; then
  cp -a /home/workshop/.config/<NAME> "$SDK_STATE_DIR/config"
fi
```

```shell
if [ -d "$SDK_STATE_DIR/config" ]; then
  install -d -o workshop -g workshop /home/workshop/.config/<NAME>
  cp -fa "$SDK_STATE_DIR/config/." /home/workshop/.config/<NAME>/
  chown -R workshop:workshop /home/workshop/.config/<NAME>
fi
```

Skip these hooks entirely when:

- The SDK has no state worth preserving,
  for example a stateless CLI tool.
- The state already lives in a directory
  backed by a `mount` plug,
  which survives refreshes by definition.
- The state is regenerated cheaply by `setup-base`
  or `setup-project`.

#### WARNING
The SDK itself is refreshed as part of any **workshop refresh**.
A bug in `save-state` or `restore-state`
becomes a workshop-wide refresh failure,
so test these hooks aggressively
before relying on them.

<a id="how-build-sdk-try"></a>

## Try the SDK

Once the definition and hooks are in place,
build and install the SDK into a workshop with **sdkcraft try**:

```console
$ sdkcraft try
```

**SDKcraft** packs the SDK for each declared platform
into files of the form `<NAME>_<ARCH>_<BASE>.sdk`
and copies them into the [try area](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-test-try-sdk).

Add the SDK to a workshop definition
using the `try-` prefix:

```yaml
name: dev
base: ubuntu@24.04
sdks:
  - name: try-<NAME>
```

The `base` must match one of the SDK’s `platforms`.
Then launch the workshop with verbose output
and a wait-on-error breakpoint
so that any hook failure leaves a usable container behind for inspection:

```console
$ workshop launch --verbose --wait-on-error
```

Pay particular attention to:

- Hook output in **workshop changes** and **workshop tasks**.
- The SDK’s `status` in **workshop info**;
  a `waiting` or `error` state
  is the SDK telling you something is wrong.
- The interaction between this SDK and any other SDKs
  it’s meant to be installed alongside.

On success,
**workshop info** reports the SDK
and a `status` of `okay`.
On failure,
**workshop changes** and **workshop tasks**
point at the hook that failed;
see [How to debug issues in workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md#how-debug-issues-workshops) for the full troubleshooting flow.

## Test the SDK

If the SDK ships a `tests/` directory with
[spread](https://github.com/canonical/spread) tests,
run them against the freshly packed artifacts:

```console
$ sdkcraft test
```

**SDKcraft** provisions a clean LXD container for each test,
installs the packed SDK into a workshop,
and runs the declared scenarios end-to-end.

Tests live under `tests/`,
organised in suites declared by `tests/spread.yaml`.
The starter test at `tests/main/launch/` illustrates the layout;
add more tests next to the starter,
each in its own subdirectory of the same suite:

```yaml
summary: SDK installs and reports healthy
execute: |
  workshop launch --verbose --wait-on-error
  workshop info | grep -E 'status:\s+okay'
```

## Iterate

Normally, you would use the **workshop sketch-sdk** command
to iterate on an SDK locally.
However, even when it doesn’t fit your purpose,
the build-try-fix loop is fast:

1. Edit the definition or a hook.
2. Run **sdkcraft clean && sdkcraft try**
   to rebuild from a clean state.
3. Run **workshop refresh**
   to reapply the SDK in the existing workshop,
   or **workshop launch --verbose --wait-on-error**
   for a fresh start.

**sdkcraft clean** is optional;
omit it when the change is small enough
that **SDKcraft** can incrementally rebuild.
For build internals, see the Craft Parts
[lifecycle documentation](https://documentation.ubuntu.com/craft-parts/latest/common/craft-parts/explanation/lifecycle/).

## Next steps

When the SDK behaves correctly under **sdkcraft try**
and its test suite passes,
proceed to [How to publish an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/publish-an-sdk.md#how-publish-sdk)
to register the SDK name on the SDK Store and upload a revision.

## See also

Explanation:

- [Interface layout](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-best-interfaces)
- [Parts decomposition](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-best-parts-decomposition)
- [Parts or hooks?](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-best-parts-or-hooks)
- [Interfaces](https://ubuntu.com/workshop/docs//explanation/index.md#exp-interfaces)
- [Design best practices](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-sdk-best-practices)
- [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts)
- [SDK hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks)
- [SDK parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts)
- [Testing and trying SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-test-try-sdk)
- [Using workshopctl with hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-workshopctl)
- [SDKs](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-sdks)

Reference:

- [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition)
- [SDK hooks](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-hooks)
- [SDK parts](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-parts)
- [workshop sketch-sdk](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-sketch-sdk)
- [workshopctl (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshopctl.md#ref-workshopctl-cli)

Tutorial:

- [Craft SDKs with SDKcraft](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks)
