Skip to content

Repository files navigation

Oak - an installer runtime for Linux. You write the YAML and the shell, Oak is the program around it

Every installer is the same program twice: a menu, a set of questions, somewhere to keep the answers, a list of steps and a way to say which one broke. Oak is that program, written once. You supply the part that is actually yours — the questions, in YAML, and the work, in shell.

Oak knows nothing about any operating system. Not a disk, not a package, not a bootloader. That half stays in shell, where you can read it.

Arch OS is a full Arch Linux installer built this way — a good place to see a real one. The example in this repository is a small one you can run in a minute: every screenshot below comes out of it.

What you get

  • One config file per installer. Questions, stages, the last warning before anything changes — all in one YAML file next to your scripts
  • A task pipeline. A task is a folder holding a task.yaml and the shell it runs. The order comes out of the stage each task names and what it declares it needs, so there is no list of steps to keep in step
  • Error handling you did not write. A script that fails is caught, and the frame names the module, the task, the file, the line, the command and the exit code
  • Tests that come with the steps. A task may say how to tell that it took. Those run on the machine as the work goes, read and change nothing, and the run ends by saying how many of them passed
  • Answers that survive. Every answer is written down the moment it is given, as plain shell. An interrupted run picks up where it left off; copy the file to the next machine and every question it answers is skipped
  • Modular. A module is one whole program. Ship an installer and a recovery from the same binary, or add a third by adding a folder
  • One file to ship. A static binary, your YAML and your scripts beside it. Bash is the only thing it expects of the machine

How it works

Oak looks next to itself, and nowhere else:

oak                       the binary
oak.yaml                  the product: name, colour, version, wordmark
modules/setup/            one module — everything below belongs to it
  module.yaml             what it asks and what order it works in
  module.sh               optional: shell everything this module runs gets
  tasks/@prepare/format/  one task, in the folder of the phase it runs in
  hooks/@preflight/uefi/  optional: a hook — can this machine be worked on at all
modules/recovery/         another module, another program

A module is one whole program. A product is the modules a binary ships with, under one name and one colour. One folder under modules/ is opened on the way in; a second is what turns that into a page.

Two modules under modules/, offered as a page

flowchart LR
    subgraph Y["What you write"]
        direction TB
        C["oak.yaml<br/>module.yaml"]
        S["module.sh<br/>task.sh"]
    end
    subgraph O["What Oak does"]
        direction TB
        A["Ask"] --> K["Keep"] --> R["Run"] --> F["Report"]
    end
    C --> O
    S --> O
    O --> U["Terminal interface"]

    style Y fill:#eceff4,stroke:#8fbcbb,color:#2e3440
    style O fill:#8fbcbb,stroke:#8fbcbb,color:#2e3440
Loading

A run, in order

Every page appears only when it has something to show. A module with no presets never shows a page offering none.

flowchart TD
    L["Welcome<br/>the link · the language"] --> W["Which module"] --> Q1["Questions marked first"]
    Q1 --> N["Network"] --> P["Preflight check"] --> PR["Presets"]
    PR --> Q["The questions<br/>one per page"]
    Q --> H["Menu"]
    H --> SE["Settings"] --> H
    H --> CF["Last warning"] --> R["The run<br/>tasks, top to bottom"]
    R --> OK["Done"]
    R --> ER["Failure<br/>script · line · command"]

    style ER fill:#bf616a,stroke:#bf616a,color:#eceff4
    style R fill:#8fbcbb,stroke:#8fbcbb,color:#2e3440
Loading

Build one

1. Get Oak

curl -LO https://github.com/murkl/oak/releases/latest/download/oak-linux-amd64
gh attestation verify oak-linux-amd64 --repo murkl/oak
install -m755 oak-linux-amd64 oak

The middle line reads the provenance CI signed the release with: which repository the file came out of, and which run built it. Without gh, GitHub prints the download's SHA-256 beside it on the release page, which says only that the file arrived whole.

latest is whatever is newest. A product that releases versions of its own pins the Oak it was built against instead — releases/download/vX.Y.Z/oak-linux-amd64 — so the same tag builds the same thing twice. Which one drove it is under the wordmark on the way in: powered by oak X.Y.Z.

Oak is versioned by what a product may declare: a new key is a minor version, and anything that stops a product loading that used to load is a major one. Below 1.0.0 that major is a decision rather than a count, so a break moves the minor until the first one is chosen. An upgrade inside a major is safe to take, so pinning is for building the same thing twice, not for surviving the next release.

2. Say what the product is — oak.yaml

title: Tux Linux
version: 1.0.0
accent: "#8fbcbb"
url: https://github.com/tux/tux-linux

3. Write a module — modules/setup/module.yaml

title: Tux Setup
description: Set a machine up for Tux.
stages: [install]

variables:
  - name: TUX_HOST
    title: Hostname
    description: What the machine calls itself on the network.
    required: true

4. Add a task — modules/setup/tasks/@install/hostname/

The folder over it is the phase it runs in, one of the stages: above, marked with @. task.yaml says what the step is:

title: Write the hostname

task.sh beside it does the work. No shebang, no set -e, no error handling — Oak wraps it:

mkdir -p ./tux/etc
echo "$TUX_HOST" >./tux/etc/hostname

A step short enough to read at a glance skips the file and says it in the yaml instead:

title: Write the hostname
script: |
  mkdir -p ./tux/etc
  echo "$TUX_HOST" >./tux/etc/hostname

# Optional: how to tell that it took. Reads the machine, changes nothing.
test: grep -q "^$TUX_HOST$" ./tux/etc/hostname

5. Run it

./oak

Oak opens on its welcome page, asks the one question that is required and still unanswered, then runs the task. The answers land in setup.conf, everything the script printed in setup.log.

The example is the same shape, filled out: two modules, three stages, a task that only runs under a condition, tests beside the work, and a page the run stops on when it is done.

The page every run opens on One question, on a page of its own

The pipeline

Nothing lists the tasks anywhere. The folder is the list, and the order follows two rules:

  • A task runs after every task of an earlier stage, which is the folder it lies in
  • Inside its stage, it runs after whatever it named in needs:
flowchart LR
    subgraph S1["tasks/@prepare"]
        direction TB
        P1["partition"] --> P2["format"]
    end
    subgraph S2["tasks/@install"]
        direction TB
        B["base"] --> D["desktop"]
        B --> G["graphics"]
    end
    subgraph S3["tasks/@finish"]
        direction TB
        U["users"]
    end
    S1 --> S2 --> S3
Loading

The run, working down the tasks in order The page a run stops on when it is done

A task with conditions: that do not hold is left out of the run entirely. Everything is checked when the module loads, so a renamed variable or a cycle is an error at startup — never a step that silently never fires.

When a step breaks

The run stops there and says so, on the page a finished run stops on with the mark the other way round. Behind it: which module, which task, which file and line, which command, and what the tool said. The rest is in the log.

A failed task: the module, the task, the script, the line, the command and the exit code

A test that disagrees is not that. The work said it worked, so the run carries on and says how many of how many passed — on every page it stops to report something, and again when it is over. Where any of them disagreed, the next page lists them once and opens each on the same report. One switch in the settings turns the whole of it off.

The command line

Five options, and nothing else. Three are about a run:

oak --module=setup     # open that module outright, instead of asking which
oak --debug            # hand every script DEBUG=true and touch nothing
oak --version          # print the Oak release this binary is — `0.1.0` — and exit

Two are about the folder, for whoever is writing one. They print and draw nothing:

oak --inspect          # load the product the way a run does, and report what it holds
oak --strings          # write a module's translation template

Nothing on the command line is an answer. Questions are answered in the interface.

Built with Oak

Arch OS — a reproducible Arch Linux installation: an installer and a recovery, both modules, on one bootable image.

Everything else

➜ Reference — the whole of what a product may declare: questions, presets, tasks, tests, conditions, hooks, the script contract and translations.

➜ Changelog — what each release changed.

➜ Contributing — how to work on Oak itself.

License

GPL-3.0. See LICENSE.

Credits

About

Build Linux installers from YAML and shell scripts. Oak is the program around them: terminal UI, task pipeline, and the exact line a script broke on.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages