<a id="how-write-runtime-hooks"></a>

# How to write runtime hooks

<!-- @artefact SDK hook -->
<!-- @artefact setup-base -->
<!-- @artefact setup-project -->
<!-- @artefact check-health -->
<!-- @artefact save-state -->
<!-- @artefact restore-state -->

This guide shows how to write each of the five
runtime hooks an SDK can ship,
with a synthesized SDK that exercises the contract differences:
which user the hook runs as,
which working directory it starts in,
and which environment variables it can rely on.

**Workshop** runs each hook as a **bash** script
with `errexit` and `pipefail` set,
so any non-zero exit aborts the hook.
Where it differs is the privilege, working directory, and extra environment.

## Prerequisites

You need a working **SDKcraft** installation
and a workshop you can launch and refresh
on a host with **Workshop** installed.
The examples use a synthesized SDK named `dotfiles-sdk`.
If you don’t have an SDK yet,
[Craft SDKs with SDKcraft](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks) walks through scaffolding one with
**sdkcraft init**.

## Lay out the hooks directory

**SDKcraft** picks up any executable file in the SDK’s `hooks/` directory
whose name matches one of the five hook names.
Hooks are not listed in `sdkcraft.yaml`;
**SDKcraft** enumerates them automatically when packing.

A complete hook tree looks like this:

```console
$ ls hooks/

  check-health
  restore-state
  save-state
  setup-base
  setup-project
```

Each file is a bash script; mark it executable
(**sdkcraft init** already does this for the scaffolded ones).

## Write setup-base

`setup-base` runs as `root`,
before the project directory is mounted
and before plugs and slots are connected.
It runs when the SDK is installed
and again when its revision changes;
a refresh that leaves the SDK intact skips it.
The working directory is the SDK’s own `hooks/` directory.
Use it for system-wide preparation
that other SDKs in the workshop may want to rely on:

```shell
cat <<PROFILE >/etc/profile.d/dotfiles.sh
export DOTFILES_SDK="$SDK"
PROFILE
```

`$SDK` always points at the SDK installation directory in the workshop,
so hooks can reference files the SDK shipped
without hardcoding a path.

## Write setup-project

`setup-project` runs as the `workshop` user,
not root,
with the working directory set to `/project/`.
It also has `$HOME`, `$XDG_RUNTIME_DIR`,
and `$DBUS_SESSION_BUS_ADDRESS` available,
so it can touch the user’s home tree
and talk to user-session services.

Use it for per-project initialization:

```shell
id -u >"$HOME/.dotfiles-uid"
install -m 0644 -t "$HOME" "$SDK/skel/.bash_aliases"
```

The hook runs after auto-connect has finished,
so any mounts the SDK plugged into are visible at this point,
as well as the project directory itself,
and the home directory is writable by the `workshop` user.

## Write check-health

`check-health` runs as `root`
from the SDK’s `hooks/` directory.
It is meant to be quick:
each attempt has five seconds
to report its result through **workshopctl set-health**
and exit.
A hook that runs past that window,
or exits without reporting a status,
moves the SDK’s health to `error`.

Call **workshopctl set-health okay**
when everything is in order;
otherwise, set `error` with a short message:

```shell
if ! sudo -u workshop --login bash -c 'test -f "$HOME/.dotfiles-uid"'; then
  workshopctl set-health error "setup-project marker missing"
  exit 0
fi
workshopctl set-health okay
```

Because `check-health` runs as root,
use **sudo -u workshop** whenever the check needs the workshop user’s
shell, environment, or file ownership.

## Persist state with save-state and restore-state

When a workshop refreshes an SDK to a new revision,
anything that lives outside a connected plug
disappears unless the SDK explicitly preserves it.
`save-state` and `restore-state`
solve that case.
Both hooks run as `root`
from the SDK’s `hooks/` directory
and have `$SDK_STATE_DIR` available,
pointing at a directory that survives the refresh.

`save-state` runs from the *old* SDK revision
before the swap.
Write whatever needs to outlive the revision
into `$SDK_STATE_DIR`:

```shell
if [ -f /home/workshop/.dotfiles-uid ]; then
  cp /home/workshop/.dotfiles-uid "$SDK_STATE_DIR/"
fi
```

`restore-state` runs from the *new* SDK revision
after the swap and after `setup-project` has finished
for every SDK in the workshop:

```shell
if [ -f "$SDK_STATE_DIR/.dotfiles-uid" ]; then
  install -m 0644 -o 1000 -g 1000 \
    "$SDK_STATE_DIR/.dotfiles-uid" \
    /home/workshop/.dotfiles-restored-uid
fi
```

Keep the hook idempotent and tolerant of missing input,
since the new revision may be installed onto a workshop
that was originally launched without `save-state`.

## Verify the hooks

Build and install the SDK into a workshop with **sdkcraft try**:

```console
$ sdkcraft try
```

**SDKcraft** lints every hook with
[ShellCheck](https://www.shellcheck.net/) while packing,
so a shell error in a hook fails the build at this step.

List the SDK in a workshop definition with the `try-` prefix
and launch the workshop:

```yaml
name: dev
base: ubuntu@22.04
sdks:
  - name: try-dotfiles-sdk
```

```console
$ workshop launch dev
```

At this point `setup-base`, `setup-project`,
and `check-health` have all run.
Confirm:

```console
$ workshop exec dev -- cat /etc/profile.d/dotfiles.sh

  export DOTFILES_SDK="/var/lib/workshop/sdk/dotfiles-sdk"

$ workshop exec dev -- cat /home/workshop/.dotfiles-uid

  1000

$ workshop info dev

  name:     dev
  base:     ubuntu@22.04
  project:  /home/user/workshop/dev
  status:   ready
```

`save-state` and `restore-state` only run
when **workshop refresh** has work to do:
a new SDK revision to swap in,
an added or removed SDK,
or a change to the workshop definition.
A bare **workshop refresh dev** against an unchanged workshop
is a no-op and skips every hook.

To exercise the state hooks,
edit the workshop definition so the refresh has something to apply,
for example by adding a mount,
and run **workshop refresh** for the workshop.
After the refresh,
`.dotfiles-restored-uid` exists in the workshop user’s home,
confirming that `save-state` wrote into `$SDK_STATE_DIR`
and `restore-state` read it back.

## See also

Explanation:

- [Design best practices](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-sdk-best-practices)
- [Runtime hooks](https://ubuntu.com/workshop/docs//explanation/sdks/runtime-hooks.md#exp-sdk-hooks)
- [workshopctl (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/workshopctl-cli.md#exp-workshopctl-cli)

Reference:

- [SDK hooks](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-hooks)
- [SDK state](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-state)
- [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)
