diff --git a/pkgs/disko/.github/dependabot.yml b/pkgs/disko/.github/dependabot.yml
new file mode 100644
index 0000000..5ace460
--- /dev/null
+++ b/pkgs/disko/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/pkgs/disko/.github/workflows/publish.yml b/pkgs/disko/.github/workflows/publish.yml
new file mode 100644
index 0000000..58660af
--- /dev/null
+++ b/pkgs/disko/.github/workflows/publish.yml
@@ -0,0 +1,22 @@
+name: "Publish a flake to flakestry"
+on:
+ push:
+ tags:
+ - "v?[0-9]+.[0-9]+.[0-9]+"
+ - "v?[0-9]+.[0-9]+"
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: "The existing tag to publish"
+ type: "string"
+ required: true
+jobs:
+ publish-flake:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: "write"
+ contents: "read"
+ steps:
+ - uses: flakestry/flakestry-publish@main
+ with:
+ version: "${{ inputs.tag || github.ref_name }}"
diff --git a/pkgs/disko/.github/workflows/update-flake-lock.yml b/pkgs/disko/.github/workflows/update-flake-lock.yml
new file mode 100644
index 0000000..046db72
--- /dev/null
+++ b/pkgs/disko/.github/workflows/update-flake-lock.yml
@@ -0,0 +1,21 @@
+name: update-flake-lock
+on:
+ workflow_dispatch: # allows manual triggering
+ schedule:
+ - cron: '0 0 * * 1,4' # Run twice a week
+permissions:
+ pull-requests: write
+ contents: write
+jobs:
+ lockfile:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ - name: Install Nix
+ uses: cachix/install-nix-action@v30
+ - name: Update flake.lock
+ uses: DeterminateSystems/update-flake-lock@v24
+ with:
+ pr-labels: |
+ merge-queue
diff --git a/pkgs/disko/.gitignore b/pkgs/disko/.gitignore
new file mode 100644
index 0000000..31b0a4b
--- /dev/null
+++ b/pkgs/disko/.gitignore
@@ -0,0 +1,4 @@
+result
+
+# Created by the NixOS interactive test driver
+.nixos-test-history
\ No newline at end of file
diff --git a/pkgs/disko/.mergify.yml b/pkgs/disko/.mergify.yml
new file mode 100644
index 0000000..8c1155e
--- /dev/null
+++ b/pkgs/disko/.mergify.yml
@@ -0,0 +1,12 @@
+queue_rules:
+ - name: default
+ merge_conditions:
+ - check-success=buildbot/nix-build
+ merge_method: rebase
+pull_request_rules:
+ - name: merge using the merge queue
+ conditions:
+ - base=master
+ - label=merge-queue
+ actions:
+ queue: {}
diff --git a/pkgs/disko/CONTRIBUTING.md b/pkgs/disko/CONTRIBUTING.md
new file mode 100644
index 0000000..e8540b5
--- /dev/null
+++ b/pkgs/disko/CONTRIBUTING.md
@@ -0,0 +1,39 @@
+# Contributing
+
+We welcome contributions of all kinds, be it in terms of bug fixes,
+reproductions, features or documentation.
+
+In general, PRs are more likely to be merged quickly if they contain tests which
+prove that a feature is working as intended or that a bug was indeed present and
+has now been fixed. Creating a draft PR that reproduces a bug is also a great
+way to help us fix issues quickly. Check out
+[this PR](https://github.com/nix-community/disko/pull/330) as an example.
+
+For more information on how to run and debug tests, check out
+[Running and debugging tests](./docs/testing.md).
+
+## How to find issues to work on
+
+If you're looking for a low-hanging fruit, check out
+[this list of `good first issue`s](https://github.com/nix-community/disko/labels/good%20first%20issue).
+These are issues that we have confirmed to be real and which have a strategy for
+a fix already lined out in the comments. All you need to do is implement.
+
+If you're looking for something more challenging, check out
+[this list of issues tagged `contributions welcome`](https://github.com/nix-community/disko/labels/contributions%20welcome).
+These are issues that we have confirmed to be real and we know we want to be
+fixed.
+
+For the real though nuts, we also have
+[the `help wanted` label](https://github.com/nix-community/disko/labels/help%20wanted)
+for issues that we feel like we need external help with. If you want a real
+challenge, take a look there!
+
+If you're looking for bugs that still need to be reproduced, have a look at
+[this list of non-`confirmed` bugs](https://github.com/nix-community/disko/issues?q=is%3Aissue+is%3Aopen+label%3Abug+-label%3Aconfirmed+).
+These are things that look like bugs but that we haven't reproduced yet.
+
+If you're looking to contribute to the documentation, check out
+[the `documentation` tag](https://github.com/nix-community/disko/issues?q=is%3Aissue+is%3Aopen+label%3Adocumentation)
+or just read through [our docs](./docs/INDEX.md) and see if you can find any
+issues.
diff --git a/pkgs/disko/LICENSE b/pkgs/disko/LICENSE
new file mode 100644
index 0000000..f099180
--- /dev/null
+++ b/pkgs/disko/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018, 2019, 2022–2024 Nix community projects
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pkgs/disko/README.md b/pkgs/disko/README.md
new file mode 100644
index 0000000..88a4f66
--- /dev/null
+++ b/pkgs/disko/README.md
@@ -0,0 +1,142 @@
+# disko - Declarative disk partitioning
+
+
+
+[Documentation Index](./docs/INDEX.md)
+
+NixOS is a Linux distribution where everything is described as code, with one
+exception: during installation, the disk partitioning and formatting are manual
+steps. **disko** aims to correct this sad 🤡 omission.
+
+This is especially useful for unattended installations, re-installation after a
+system crash or for setting up more than one identical server.
+
+## Overview
+
+**disko** can either be used after booting from a NixOS installer, or in
+conjunction with [nixos-anywhere](https://github.com/numtide/nixos-anywhere) if
+you're installing remotely.
+
+Before using **disko**, the specifications of the disks, partitions, type of
+formatting and the mount points must be defined in a Nix configuration. You can
+find [examples](./example) of typical configurations in the Nix community
+repository, and use one of these as the basis of your own configuration.
+
+You can keep your configuration and re-use it for other installations, or for a
+system rebuild.
+
+**disko** is flexible, in that it supports most of the common formatting and
+partitioning options, including:
+
+- Disk layouts: GPT, MBR, and mixed.
+- Partition tools: LVM, mdadm, LUKS, and more.
+- Filesystems: ext4, btrfs, ZFS, bcachefs, tmpfs, and others.
+
+It can work with these in various configurations and orders, and supports
+recursive layouts.
+
+## How to use disko
+
+Disko doesn't require installation: it can be run directly from nix-community
+repository. The [Quickstart Guide](./docs/quickstart.md) documents how to run
+Disko in its simplest form when installing NixOS. Alternatively, you can also
+use the new [disko-install](./docs/disko-install.md) tool, which combines
+`disko` and `nixos-install` into one step.
+
+For information on other use cases, including upgrading from an older version of
+**disko**, using **disko** without NixOS and downloading the module, see the
+[How To Guide](./docs/HowTo.md)
+
+For more detailed options, such as command line switches, see the
+[Reference Guide](./docs/reference.md)
+
+To access sample configurations for commonly-used disk layouts, refer to the
+[examples](./example) provided.
+
+Disko can be also used to create [disk images](./docs/disko-images.md).
+
+## Sample Configuration and CLI command
+
+A simple disko configuration may look like this:
+
+```nix
+{
+ disko.devices = {
+ disk = {
+ my-disk = {
+ device = "/dev/sda";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ type = "EF00";
+ size = "500M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
+```
+
+If you'd saved this configuration in /tmp/disk-config.nix, and wanted to create
+a disk named /dev/sda, you would run the following command to partition, format
+and mount the disk.
+
+```console
+sudo nix --experimental-features "nix-command flakes" run github:nix-community/disko/latest -- --mode destroy,format,mount /tmp/disk-config.nix
+```
+
+## Related Tools
+
+This tool is used by
+[nixos-anywhere](https://github.com/numtide/nixos-anywhere), which carries out a
+fully-automated remote install of NixOS.
+
+We also acknowledge https://github.com/NixOS/nixpart, the conceptual ancestor of
+this project.
+
+## Licensing and Contribution details
+
+This software is provided free under the
+[MIT Licence](https://opensource.org/licenses/MIT).
+
+If you want to contribute, check out [CONTRIBUTING.md](./CONTRIBUTING.md).
+
+## Get in touch
+
+We have a public matrix channel at
+[disko](https://matrix.to/#/#disko:nixos.org).
+
+---
+
+This project is supported by [Numtide](https://numtide.com/).
+
+
+We are a team of independent freelancers that love open source. We help our
+customers make their project lifecycles more efficient by:
+
+- Providing and supporting useful tools such as this one
+- Building and deploying infrastructure, and offering dedicated DevOps support
+- Building their in-house Nix skills, and integrating Nix with their workflows
+- Developing additional features and tools
+- Carrying out custom research and development.
+
+[Contact us](https://numtide.com/contact) if you have a project in mind, or if
+you need help with any of our supported tools, including this one. We'd love to
+hear from you.
diff --git a/pkgs/disko/cli.nix b/pkgs/disko/cli.nix
new file mode 100644
index 0000000..d32e492
--- /dev/null
+++ b/pkgs/disko/cli.nix
@@ -0,0 +1,116 @@
+{ pkgs ? import { }
+, lib ? pkgs.lib
+, mode ? "mount"
+, flake ? null
+, flakeAttr ? null
+, diskoFile ? null
+, rootMountPoint ? "/mnt"
+, noDeps ? false
+, ...
+}@args:
+let
+ disko = import ./. {
+ inherit rootMountPoint;
+ inherit lib;
+ };
+
+ hasDiskoFile = diskoFile != null;
+
+ diskoAttr =
+ (if noDeps then
+ (if hasDiskoFile then
+ {
+ destroy = "_cliDestroyNoDeps";
+ format = "_cliFormatNoDeps";
+ mount = "_cliMountNoDeps";
+ unmount = "_cliUnmountNoDeps";
+
+ "format,mount" = "_cliFormatMountNoDeps";
+ "destroy,format,mount" = "_cliDestroyFormatMountNoDeps";
+ }
+ else
+ {
+ destroy = "destroyNoDeps";
+ format = "formatNoDeps";
+ mount = "mountNoDeps";
+ unmount = "unmountNoDeps";
+
+ "format,mount" = "formatMountNoDeps";
+ "destroy,format,mount" = "destroyFormatMountNoDeps";
+ }) // {
+ # legacy aliases
+ disko = "diskoScriptNoDeps";
+ create = "createScriptNoDeps";
+ zap_create_mount = "diskoScriptNoDeps";
+ }
+ else
+ (if hasDiskoFile then
+ {
+ destroy = "_cliDestroy";
+ format = "_cliFormat";
+ mount = "_cliMount";
+ unmount = "_cliUnmount";
+
+ "format,mount" = "_cliFormatMount";
+ "destroy,format,mount" = "_cliDestroyFormatMount";
+ }
+ else
+ {
+ destroy = "destroy";
+ format = "format";
+ mount = "mount";
+ unmount = "unmount";
+
+ "format,mount" = "formatMount";
+ "destroy,format,mount" = "destroyFormatMount";
+ }) // {
+ # legacy aliases
+ disko = "diskoScript";
+ create = "createScript";
+ zap_create_mount = "diskoScript";
+ }
+ ).${mode};
+
+ hasDiskoConfigFlake =
+ hasDiskoFile || lib.hasAttrByPath [ "diskoConfigurations" flakeAttr ] (builtins.getFlake flake);
+
+ hasDiskoModuleFlake =
+ lib.hasAttrByPath [ "nixosConfigurations" flakeAttr "config" "disko" "devices" ] (builtins.getFlake flake);
+
+
+ diskFormat =
+ let
+ diskoConfig =
+ if hasDiskoFile then
+ import diskoFile
+ else
+ (builtins.getFlake flake).diskoConfigurations.${flakeAttr};
+ in
+ if builtins.isFunction diskoConfig then
+ diskoConfig ({ inherit lib; } // args)
+ else
+ diskoConfig;
+
+ diskoEval =
+ disko.${diskoAttr} diskFormat pkgs;
+
+ diskoScript =
+ if hasDiskoConfigFlake then
+ diskoEval
+ else if hasDiskoModuleFlake then
+ (builtins.getFlake flake).nixosConfigurations.${flakeAttr}.config.system.build.${diskoAttr} or (
+ pkgs.writeShellScriptBin "disko-compat-error" ''
+ echo 'Error: Attribute `nixosConfigurations.${flakeAttr}.config.system.build.${diskoAttr}` >&2
+ echo ' not found in flake `${flake}`!' >&2
+ echo ' This is probably caused by the locked version of disko in the flake' >&2
+ echo ' being different from the version of disko you executed.' >&2
+ echo 'EITHER set the `disko` input of your flake to `github:nix-community/disko/latest`,' >&2
+ echo ' run `nix flake update disko` in the flake directory and then try again,' >&2
+ echo 'OR run `nix run github:nix-community/disko/v1.9.0 -- --help` and use one of its modes.' >&2
+ exit 1;''
+ )
+ else
+ (builtins.abort "couldn't find `diskoConfigurations.${flakeAttr}` or `nixosConfigurations.${flakeAttr}.config.disko.devices`");
+
+in
+diskoScript
diff --git a/pkgs/disko/default.nix b/pkgs/disko/default.nix
new file mode 100644
index 0000000..02f429c
--- /dev/null
+++ b/pkgs/disko/default.nix
@@ -0,0 +1,65 @@
+{ lib ? import
+, rootMountPoint ? "/mnt"
+, checked ? false
+, diskoLib ? import ./lib { inherit lib rootMountPoint; }
+}:
+let
+ eval = cfg: lib.evalModules {
+ modules = lib.singleton {
+ # _file = toString input;
+ imports = lib.singleton { disko.devices = cfg.disko.devices; };
+ options = {
+ disko.devices = lib.mkOption {
+ type = diskoLib.toplevel;
+ };
+ };
+ };
+ };
+ # We might instead reuse some of the deprecated output names to refer to the values the _cli* outputs currently do,
+ # but this warning allows us to get feedback from users early in case they have a use case we haven't considered.
+ warnDeprecated = name: lib.warn "the ${name} output is deprecated and will be removed, please open an issue if you're using it!";
+in
+{
+ lib = warnDeprecated ".lib.lib" diskoLib;
+
+ _cliDestroy = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).destroy;
+ _cliDestroyNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).destroyNoDeps;
+
+ _cliFormat = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).format;
+ _cliFormatNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatNoDeps;
+
+ _cliMount = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).mount;
+ _cliMountNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).mountNoDeps;
+
+ _cliUnmount = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).unmount;
+ _cliUnmountNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).unmountNoDeps;
+
+ _cliFormatMount = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatMount;
+ _cliFormatMountNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatMountNoDeps;
+
+ _cliDestroyFormatMount = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).destroyFormatMount;
+ _cliDestroyFormatMountNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).destroyFormatMountNoDeps;
+
+ # legacy aliases
+ create = cfg: warnDeprecated "create" (eval cfg).config.disko.devices._create;
+ createScript = cfg: pkgs: warnDeprecated "createScript" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatScript;
+ createScriptNoDeps = cfg: pkgs: warnDeprecated "createScriptNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatScriptNoDeps;
+
+ format = cfg: warnDeprecated "format" (eval cfg).config.disko.devices._create;
+ formatScript = cfg: pkgs: warnDeprecated "formatScript" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatScript;
+ formatScriptNoDeps = cfg: pkgs: warnDeprecated "formatScriptNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatScriptNoDeps;
+
+ mount = cfg: warnDeprecated "mount" (eval cfg).config.disko.devices._mount;
+ mountScript = cfg: pkgs: warnDeprecated "mountScript" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).mountScript;
+ mountScriptNoDeps = cfg: pkgs: warnDeprecated "mountScriptNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).mountScriptNoDeps;
+
+ disko = cfg: warnDeprecated "disko" (eval cfg).config.disko.devices._disko;
+ diskoScript = cfg: pkgs: warnDeprecated "diskoScript" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).diskoScript;
+ diskoScriptNoDeps = cfg: pkgs: warnDeprecated "diskoScriptNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).diskoScriptNoDeps;
+
+ # we keep this old output for backwards compatibility
+ diskoNoDeps = cfg: pkgs: warnDeprecated "diskoNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).diskoScriptNoDeps;
+
+ config = cfg: (eval cfg).config.disko.devices._config;
+ packages = cfg: (eval cfg).config.disko.devices._packages;
+}
diff --git a/pkgs/disko/disk-deactivate/disk-deactivate b/pkgs/disko/disk-deactivate/disk-deactivate
new file mode 100755
index 0000000..9ddb2de
--- /dev/null
+++ b/pkgs/disko/disk-deactivate/disk-deactivate
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -efux -o pipefail
+# dependencies: bash jq util-linux lvm2 mdadm zfs gnugrep sed
+disk=$(realpath "$1")
+
+lsblk -a -f >&2
+lsblk --output-all --json | jq -r --arg disk_to_clear "$disk" -f "$(dirname "$0")/disk-deactivate.jq" | bash -x
+lsblk -a -f >&2
diff --git a/pkgs/disko/disk-deactivate/disk-deactivate.jq b/pkgs/disko/disk-deactivate/disk-deactivate.jq
new file mode 100644
index 0000000..cf6e943
--- /dev/null
+++ b/pkgs/disko/disk-deactivate/disk-deactivate.jq
@@ -0,0 +1,86 @@
+# since lsblk lacks zfs support, we have to do it this way
+def remove:
+ if .fstype == "zfs_member" then
+ "if type zpool >/dev/null; then zpool destroy -f \(.label); zpool labelclear -f \(.label); fi"
+ elif .fstype == "LVM2_member" then
+ [
+ "vg=$(pvs \(.path) --noheadings --options vg_name | grep -o '[a-zA-Z0-9-]*')",
+ "vgchange -a n \"$vg\"",
+ "vgremove -f \"$vg\""
+ ]
+ elif .fstype == "swap" then
+ "swapoff \(.path)"
+ elif .fstype == null then
+ # maybe its zfs
+ [
+ # the next line has some horrible escaping
+ "zpool=$(if type zdb >/dev/null; then zdb -l \(.path) | sed -nr $'s/ +name: \\'(.*)\\'/\\\\1/p'; fi)",
+ "if [[ -n \"${zpool}\" ]]; then zpool destroy -f \"$zpool\"; zpool labelclear -f \"$zpool\"; fi",
+ "unset zpool"
+ ]
+ else
+ []
+ end
+;
+
+def deactivate:
+ if .type == "disk" or .type == "loop" then
+ [
+ # If this disk is a member of raid, stop that raid
+ "md_dev=$(lsblk \(.path) -l -p -o type,name | awk 'match($1,\"raid.*\") {print $2}')",
+ "if [[ -n \"${md_dev}\" ]]; then umount \"$md_dev\"; mdadm --stop \"$md_dev\"; fi",
+ # Remove all file-systems and other magic strings
+ "wipefs --all -f \(.path)",
+ # Remove the MBR bootstrap code
+ "dd if=/dev/zero of=\(.path) bs=440 count=1"
+ ]
+ elif .type == "part" then
+ [
+ "wipefs --all -f \(.path)"
+ ]
+ elif .type == "crypt" then
+ [
+ "cryptsetup luksClose \(.path)",
+ "wipefs --all -f \(.path)"
+ ]
+ elif .type == "lvm" then
+ (.name | split("-")[0]) as $vgname |
+ (.name | split("-")[1]) as $lvname |
+ [
+ "lvremove -fy \($vgname)/\($lvname)"
+ ]
+ elif (.type | contains("raid")) then
+ [
+ "mdadm --stop \(.name)"
+ ]
+ else
+ ["echo Warning: unknown type '\(.type)'. Consider handling this in https://github.com/nix-community/disko/blob/master/disk-deactivate/disk-deactivate.jq"]
+ end
+;
+
+def walk:
+ [
+ (.mountpoints[] | select(. != null) | "umount -R \(.)"),
+ ((.children // []) | map(walk)),
+ remove,
+ deactivate
+ ]
+;
+
+def init:
+ "/dev/\(.name)" as $disk |
+ "/dev/disk/by-id/\(."id-link")" as $disk_by_id |
+ "/dev/disk/by-id/\(.tran)-\(.id)" as $disk_by_id2 |
+ "/dev/disk/by-id/\(.tran)-\(.wwn)" as $disk_by_wwn |
+ if $disk == $disk_to_clear or $disk_by_id == $disk_to_clear or $disk_by_id2 == $disk_to_clear or $disk_by_wwn == $disk_to_clear then
+ [
+ "set -fu",
+ walk
+ ]
+ else
+ []
+ end
+;
+
+.blockdevices | map(init) | flatten | join("\n")
+
diff --git a/pkgs/disko/disko b/pkgs/disko/disko
new file mode 100755
index 0000000..3dd784c
--- /dev/null
+++ b/pkgs/disko/disko
@@ -0,0 +1,193 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+readonly libexec_dir="${0%/*}"
+
+# a file with the disko config
+declare disko_config
+
+# mount was chosen as the default mode because it's less destructive
+mode=mount
+nix_args=()
+skip_destroy_safety_check=false
+
+# DISKO_VERSION is set by the wrapper in package.nix
+DISKO_VERSION="${DISKO_VERSION:="unknown! This is a bug, please report it!"}"
+onlyPrintVersion=false
+
+showUsage() {
+ cat <&2
+ exit 1
+}
+
+## Main ##
+
+[[ $# -eq 0 ]] && {
+ showUsage
+ exit 1
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --debug)
+ set -x
+ ;;
+ -m | --mode)
+ mode=$2
+ shift
+ ;;
+ -f | --flake)
+ flake=$2
+ shift
+ ;;
+ --argstr | --arg)
+ nix_args+=("$1" "$2" "$3")
+ shift
+ shift
+ ;;
+ -h | --help)
+ showUsage
+ exit 0
+ ;;
+ --dry-run)
+ dry_run=y
+ ;;
+ --root-mountpoint)
+ nix_args+=(--argstr rootMountPoint "$2")
+ shift
+ ;;
+ --no-deps)
+ nix_args+=(--arg noDeps true)
+ ;;
+ --yes-wipe-all-disks)
+ skip_destroy_safety_check=true
+ ;;
+ --show-trace)
+ nix_args+=("$1")
+ ;;
+ --version)
+ onlyPrintVersion=true
+ ;;
+ *)
+ if [ -z ${disko_config+x} ]; then
+ disko_config=$1
+ else
+ showUsage
+ exit 1
+ fi
+ ;;
+ esac
+ shift
+done
+
+if [[ "$onlyPrintVersion" = true ]]; then
+ echo "$DISKO_VERSION"
+ exit 0
+fi
+# Always print version information to help with debugging
+echo "disko version $DISKO_VERSION"
+
+nixBuild() {
+ if command -v nom-build > /dev/null; then
+ nom-build "$@"
+ else
+ nix-build "$@"
+ fi
+}
+
+if ! {
+ # Base modes
+ [[ $mode = "destroy" ]] || [[ $mode = "format" ]] || [[ $mode = "mount" ]] || [[ $mode = "unmount" ]] ||
+ # Combined modes
+ [[ $mode = "format,mount" ]] ||
+ [[ $mode = "destroy,format,mount" ]] || # Replaces --mode disko
+ # Legacy modes, will be removed in next major version
+ [[ $mode = "disko" ]] || [[ $mode = "create" ]] || [[ $mode = "zap_create_mount" ]] ;
+}; then
+ abort 'mode must be one of "destroy", "format", "mount", "destroy,format,mount" or "format,mount"'
+fi
+
+if [[ -n "${flake+x}" ]]; then
+ if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then
+ flake="${BASH_REMATCH[1]}"
+ flakeAttr="${BASH_REMATCH[2]}"
+ fi
+ if [[ -z "${flakeAttr-}" ]]; then
+ echo "Please specify the name of the NixOS configuration to be installed, as a URI fragment in the flake-uri."
+ echo "For example, to use the output diskoConfigurations.foo from the flake.nix, append \"#foo\" to the flake-uri."
+ exit 1
+ fi
+ if [[ -e "$flake" ]]; then
+ flake="$(realpath "$flake")"
+ fi
+ nix_args+=("--arg" "flake" "\"$flake\"")
+ nix_args+=("--argstr" "flakeAttr" "$flakeAttr")
+ nix_args+=(--extra-experimental-features flakes)
+elif [[ -n "${disko_config+x}" ]] && [[ -e "$disko_config" ]]; then
+ nix_args+=("--arg" "diskoFile" "$(realpath "$disko_config")")
+else
+ abort "disko config must be an existing file or flake must be set"
+fi
+
+# The "--impure" is still pure, as the path is within the nix store.
+script=$(nixBuild "${libexec_dir}"/cli.nix \
+ --no-out-link \
+ --impure \
+ --argstr mode "$mode" \
+ "${nix_args[@]}"
+)
+
+command=("$(echo "$script"/bin/*)")
+if [[ $mode = "destroy,format,mount" && $skip_destroy_safety_check = true ]]; then
+ command+=("--yes-wipe-all-disks")
+fi
+
+# Legacy modes don't support --yes-wipe-all-disks and are not in `$script/bin`
+if [[ $mode = "disko" ]] || [[ $mode = "create" ]] || [[ $mode = "zap_create_mount" ]] ; then
+ command=("$script")
+fi
+
+if [[ -n "${dry_run+x}" ]]; then
+ echo "${command[@]}"
+else
+ exec "${command[@]}"
+fi
diff --git a/pkgs/disko/disko-install b/pkgs/disko/disko-install
new file mode 100755
index 0000000..2c5596e
--- /dev/null
+++ b/pkgs/disko/disko-install
@@ -0,0 +1,273 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+showUsage() {
+ cat <&2
+ exit 1
+ fi
+ case $2 in
+ format)
+ diskoAttr=diskoScript
+ ;;
+ mount)
+ diskoAttr=mountScript
+ ;;
+ *)
+ echo "Invalid mode: $2" >&2
+ echo "Valid modes are: format, mount" >&2
+ exit 1
+ ;;
+ esac
+ shift
+ ;;
+ --system-config)
+ if [[ $# -lt 2 ]]; then
+ echo "Option $1 requires one JSON argument." >&2
+ exit 1
+ fi
+ # shellcheck disable=SC2034
+ extraSystemConfig="$2"
+ shift
+ ;;
+ --extra-files)
+ if [[ $# -lt 3 ]]; then
+ echo "Option $1 requires two arguments: source, destination" >&2
+ exit 1
+ fi
+ extraFiles[$2]=$3
+ shift
+ shift
+ ;;
+ --option)
+ if [[ $# -lt 3 ]]; then
+ echo "Option $1 requires two arguments: key, value" >&2
+ exit 1
+ fi
+ nix_args+=(--option "$2" "$3")
+ shift
+ shift
+ ;;
+ --disk)
+ if [[ $# -lt 3 ]]; then
+ echo "Option $1 requires two arguments: disk_name, device_path" >&2
+ exit 1
+ fi
+ # shellcheck disable=SC2034
+ diskMappings[$2]=$3
+ shift
+ shift
+ ;;
+ --mount-point)
+ if [[ $# -lt 2 ]]; then
+ echo "Option $1 requires an argument" >&2
+ exit 1
+ fi
+ mountPoint=$2
+ shift
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ showUsage
+ exit 1
+ ;;
+ esac
+ shift
+ done
+}
+
+cleanupMountPoint() {
+ if mountpoint -q "${mountPoint}"; then
+ umount -R "${mountPoint}"
+ fi
+ rmdir "${mountPoint}"
+}
+
+nixBuild() {
+ if command -v nom-build > /dev/null; then
+ nom-build "$@"
+ else
+ nix-build "$@"
+ fi
+}
+
+maybeRun () {
+ if [[ -z ${dry_run-} ]]; then
+ "$@"
+ else
+ echo "Would run: $*"
+ fi
+}
+
+main() {
+ parseArgs "$@"
+
+ if [[ -z ${flake-} ]]; then
+ echo "Please specify the flake-uri with --flake to use for installation." >&2
+ exit 1
+ fi
+
+ # check if we are root
+ if [[ "$EUID" -ne 0 ]] && [[ -z ${dry_run-} ]]; then
+ echo "This script must be run as root" >&2
+ exit 1
+ fi
+
+ if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then
+ flake="${BASH_REMATCH[1]}"
+ flakeAttr="${BASH_REMATCH[2]}"
+ fi
+
+ if [[ -e "$flake" ]]; then
+ flake=$(realpath "$flake")
+ fi
+
+ if [[ -z ${flakeAttr-} ]]; then
+ echo "Please specify the name of the NixOS configuration to be installed, as a URI fragment in the flake-uri." >&2
+ echo 'For example, to use the output nixosConfigurations.foo from the flake.nix, append "#foo" to the flake-uri.' >&2
+ exit 1
+ fi
+
+ maybeRun mkdir -p "${mountPoint}"
+ maybeRun chmod 755 "${mountPoint}" # bcachefs wants 755
+ # shellcheck disable=SC2064
+ if [[ -z ${dry_run-} ]]; then
+ trap cleanupMountPoint EXIT
+ fi
+
+ outputs=$(nixBuild "${libexec_dir}"/install-cli.nix \
+ "${nix_args[@]}" \
+ --no-out-link \
+ --impure \
+ --argstr flake "$flake" \
+ --argstr flakeAttr "$flakeAttr" \
+ --argstr rootMountPoint "$mountPoint" \
+ --arg writeEfiBootEntries "$writeEfiBootEntries" \
+ --arg diskMappings "$(serialiaseArrayToNix diskMappings)" \
+ --argstr extraSystemConfig "$extraSystemConfig" \
+ -A installToplevel \
+ -A closureInfo \
+ -A "$diskoAttr")
+
+ IFS=$'\n' mapfile -t artifacts <<<"$outputs"
+ nixos_system=${artifacts[0]}
+ closure_info=${artifacts[1]}
+ disko_script=${artifacts[2]}
+
+ if [[ -n ${dry_run-} ]]; then
+ echo "Would run: $disko_script"
+ echo "Would run: nixos-install --system '$nixos_system' --root '$mountPoint'"
+ exit 0
+ fi
+
+ # We don't want swap as can break your running system in weird ways if you eject the disk
+ # Hopefully disko-install has enough RAM to run without swap, otherwise we can make this configurable in future.
+ DISKO_SKIP_SWAP=1 "$disko_script"
+
+ for source in "${!extraFiles[@]}"; do
+ destination=${extraFiles[$source]}
+ mkdir -p "$mountPoint/$(dirname "$destination")"
+ cp -ar "$source" "$mountPoint/$destination"
+ done
+
+ # nix copy uses up a lot of memory and we work around issues with incorrect checksums in our store
+ # that can be caused by using closureInfo in combination with multiple builders and non-deterministic builds.
+ # Therefore if we have a blank store, we copy the store paths and registration from the closureInfo.
+ if [[ ! -f "${mountPoint}/nix/var/nix/db/db.sqlite" ]]; then
+ echo "Copying store paths" >&2
+ mkdir -p "${mountPoint}/nix/store"
+ xargs cp --recursive --target "${mountPoint}/nix/store" < "${closure_info}/store-paths"
+ echo "Loading nix database" >&2
+ NIX_STATE_DIR=${mountPoint}/nix/var/nix nix-store --load-db < "${closure_info}/registration"
+ fi
+
+ nixos-install --no-channel-copy --no-root-password --system "$nixos_system" --root "$mountPoint"
+}
+
+if main "$@"; then
+ echo "disko-install succeeded"
+ exit 0
+else
+ echo "disko-install failed" >&2
+ exit 1
+fi
diff --git a/pkgs/disko/doc.nix b/pkgs/disko/doc.nix
new file mode 100644
index 0000000..d07f8f3
--- /dev/null
+++ b/pkgs/disko/doc.nix
@@ -0,0 +1,43 @@
+{ lib, nixosOptionsDoc, runCommand, fetchurl, pandoc }:
+
+let
+ diskoLib = import ./lib {
+ inherit lib;
+ rootMountPoint = "/mnt";
+ };
+ eval = lib.evalModules {
+ modules = [
+ {
+ options.disko = {
+ devices = lib.mkOption {
+ type = diskoLib.toplevel;
+ default = { };
+ description = "The devices to set up";
+ };
+ };
+ }
+ ];
+ };
+ options = nixosOptionsDoc {
+ options = eval.options;
+ };
+ md = (runCommand "disko-options.md" { } ''
+ cat >$out <>$out
+ '').overrideAttrs (_o: {
+ # Work around https://github.com/hercules-ci/hercules-ci-agent/issues/168
+ allowSubstitutes = true;
+ });
+ css = fetchurl {
+ url = "https://gist.githubusercontent.com/killercup/5917178/raw/40840de5352083adb2693dc742e9f75dbb18650f/pandoc.css";
+ sha256 = "sha256-SzSvxBIrylxBF6B/mOImLlZ+GvCfpWNLzGFViLyOeTk=";
+ };
+in
+runCommand "disko.html" { nativeBuildInputs = [ pandoc ]; } ''
+ mkdir $out
+ cp ${css} $out/pandoc.css
+ pandoc --css="pandoc.css" ${md} --to=html5 -s -f markdown+smart --metadata pagetitle="Disko options" -o $out/index.html
+''
diff --git a/pkgs/disko/docs/HowTo.md b/pkgs/disko/docs/HowTo.md
new file mode 100644
index 0000000..e57d73a
--- /dev/null
+++ b/pkgs/disko/docs/HowTo.md
@@ -0,0 +1,178 @@
+# How-to Guide: Disko
+
+## How to use Disko without NixOS
+
+TODO: Still to be documented
+
+## Upgrading From Older disko versions
+
+TODO: Include documentation here.
+
+For now, see the
+[upgrade guide](https://github.com/JillThornhill/disko/blob/master/docs/upgrade-guide.md)
+
+## Installing NixOS module
+
+You can use the NixOS module in one of the following ways:
+
+
+ Flakes (Current recommendation)
+
+If you use nix flakes support:
+
+```nix
+{
+ inputs.disko.url = "github:nix-community/disko/latest";
+ inputs.disko.inputs.nixpkgs.follows = "nixpkgs";
+
+ outputs = { self, nixpkgs, disko }: {
+ # change `yourhostname` to your actual hostname
+ nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
+ # change to your system:
+ system = "x86_64-linux";
+ modules = [
+ ./configuration.nix
+ disko.nixosModules.disko
+ ];
+ };
+ };
+}
+```
+
+
+
+ niv
+
+First add it to [niv](https://github.com/nmattia/niv):
+
+```console
+niv add nix-community/disko
+```
+
+Then add the following to your configuration.nix in the `imports` list:
+
+```nix
+{
+ imports = [ "${(import ./nix/sources.nix).disko}/module.nix" ];
+}
+```
+
+
+
+ npins
+
+First add it to [npins](https://github.com/andir/npins):
+
+```console
+npins add github nix-community disko
+```
+
+Then add the following to your configuration.nix in the `imports` list:
+
+```nix
+let
+ sources = import ./npins;
+ disko = import sources.disko {};
+in
+{
+ imports = [ "${disko}/module.nix" ];
+ …
+}
+```
+
+
+
+ nix-channel
+
+As root run:
+
+```console
+nix-channel --add https://github.com/nix-community/disko/archive/master.tar.gz disko
+nix-channel --update
+```
+
+Then add the following to your configuration.nix in the `imports` list:
+
+```nix
+{
+ imports = [ ];
+}
+```
+
+
+
+ fetchTarball
+
+Add the following to your configuration.nix:
+
+```nix
+{
+ imports = [ "${builtins.fetchTarball "https://github.com/nix-community/disko/archive/master.tar.gz"}/module.nix" ];
+}
+```
+
+or with pinning:
+
+```nix
+{
+ imports = let
+ # replace this with an actual commit id or tag
+ commit = "f2783a8ef91624b375a3cf665c3af4ac60b7c278";
+ in [
+ "${builtins.fetchTarball {
+ url = "https://github.com/nix-community/disko/archive/${commit}.tar.gz";
+ # replace this with an actual hash
+ sha256 = "0000000000000000000000000000000000000000000000000000";
+ }}/module.nix"
+ ];
+}
+```
+
+
+
+## Using the NixOS module
+
+```nix
+{
+ # checkout the example folder for how to configure different disko layouts
+ disko.devices = {
+ disk = {
+ vdb = {
+ device = "/dev/disk/by-id/some-disk-id";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ type = "EF00";
+ size = "100M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
+```
+
+this will configure `fileSystems` and other required NixOS options to boot the
+specified configuration.
+
+If you are on an installer, you probably want to disable `enableConfig`.
+
+disko will create the scripts `disko-create` and `disko-mount` which can be used
+to create/mount the configured disk layout.
diff --git a/pkgs/disko/docs/INDEX.md b/pkgs/disko/docs/INDEX.md
new file mode 100644
index 0000000..2c1b027
--- /dev/null
+++ b/pkgs/disko/docs/INDEX.md
@@ -0,0 +1,22 @@
+# disko - Declarative disk partitioning
+
+
+
+## Table of Contents
+
+### For users
+
+- [README](../README.md)
+- [Quickstart](./quickstart.md)
+- [System Requirements](./requirements.md)
+- [How to Guide](./HowTo.md)
+- [Disko-Install](./disko-install.md)
+- [Disko-Images](./disko-images.md)
+- [Support Matrix](./supportmatrix.md)
+- [Reference](./reference.md)
+- [Upgrade Guide](./upgrade-guide.md)
+ - [Migrating to the new GPT layout](./table-to-gpt.md)
+
+### For contributors
+
+- [Running and debugging tests](./testing.md)
diff --git a/pkgs/disko/docs/disko-images.md b/pkgs/disko/docs/disko-images.md
new file mode 100644
index 0000000..51b2e86
--- /dev/null
+++ b/pkgs/disko/docs/disko-images.md
@@ -0,0 +1,122 @@
+# Generating Disk Images with Secrets Included using Disko
+
+Using Disko on NixOS allows you to efficiently create `.raw` VM images from a
+system configuration. The generated image can be used as a VM or directly
+written to a physical drive to create a bootable disk. Follow the steps below to
+generate disk images:
+
+## Generating the `.raw` VM Image
+
+1. **Create a NixOS configuration that includes the disko and the disk
+ configuration of your choice**
+
+In the this example we create a flake containing a nixos configuration for
+`myhost`.
+
+```nix
+# save this as flake.nix
+{
+ description = "A disko images example";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ disko.url = "github:nix-community/disko/latest";
+ disko.inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ outputs = { self, disko, nixpkgs }: {
+ nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
+ system = "x86_64-linux";
+ modules = [
+ # You can get this file from here: https://github.com/nix-community/disko/blob/master/example/simple-efi.nix
+ ./simple-efi.nix
+ disko.nixosModules.disko
+ ({ config, ... }: {
+ # shut up state version warning
+ system.stateVersion = config.system.nixos.version;
+ # Adjust this to your liking.
+ # WARNING: if you set a too low value the image might be not big enough to contain the nixos installation
+ disko.devices.disk.main.imageSize = "10G";
+ })
+ ];
+ };
+ };
+}
+```
+
+2. **Build the disko image script:** Replace `myhost` in the command below with
+ your specific system configuration name:
+
+ ```console
+ nix build .#nixosConfigurations.myhost.config.system.build.diskoImagesScript
+ ```
+
+3. **Execute the disko image script:** Execute the generated disko image script.
+ Running `./result --help` will output the available options:
+
+ ```console
+ ./result --help
+ Usage: $script [options]
+
+ Options:
+ * --pre-format-files
+ copies the src to the dst on the VM, before disko is run
+ This is useful to provide secrets like LUKS keys, or other files you need for formatting
+ * --post-format-files
+ copies the src to the dst on the finished image
+ These end up in the images later and is useful if you want to add some extra stateful files
+ They will have the same permissions but will be owned by root:root
+ * --build-memory
+ specify the amount of memory in MiB that gets allocated to the build VM
+ This can be useful if you want to build images with a more involed NixOS config
+ The default is 1024 MiB
+ ```
+
+ An example run may look like this:
+
+ ```
+ sudo ./result --build-memory 2048
+ ```
+
+ The script will generate the actual image outside of the nix store in the
+ current working directory. The create image names depend on the names used in
+ `disko.devices.disk` attrset in the NixOS configuration. In our code example
+ it will produce the following image:
+
+ ```
+ $ ls -la main.raw
+ .rw-r--r-- root root 10 GB 2 minutes ago main.raw
+ ```
+
+## Additional Configuration
+
+- For custom image name output, define the image name in your Disko configuration:
+
+ ```console
+ disko.devices.disk..imageName = "nixos-x86_64-linux-generic-btrfs"; # Set your preferred name
+ ```
+
+ The image scirpt will produce `nixos-x86_64-linux-generic-btrfs.raw` instead of `.raw`.
+
+- For virtual drive use, define the image size in your Disko configuration:
+
+ ```console
+ disko.devices.disk..imageSize = "32G"; # Set your preferred size
+ ```
+
+## Understanding the Image Generation Process
+
+1. Files specified in `--pre-format-files` and `--post-format-files` are
+ temporarily copied to `/tmp`.
+2. Files are then moved to their respective locations in the VM both before and
+ after the Disko partitioning script runs.
+3. The NixOS installer is executed, having access only to `--post-format-files`.
+4. Upon installer completion, the VM is shutdown, and the `.raw` disk files are
+ moved to the local directory.
+
+> **Note**: The auto-resizing feature is currently not available in Disko.
+> Contributions for this feature are welcomed. Adjust the `imageSize`
+> configuration to prevent issues related to file size and padding.
+
+By following these instructions and understanding the process, you can smoothly
+generate disk images with Disko for your NixOS system configurations.
diff --git a/pkgs/disko/docs/disko-install.md b/pkgs/disko/docs/disko-install.md
new file mode 100644
index 0000000..7abaff9
--- /dev/null
+++ b/pkgs/disko/docs/disko-install.md
@@ -0,0 +1,233 @@
+# disko-install
+
+**disko-install** enhances the normal `nixos-install` with disko's partitioning
+feature. It can be started from the NixOS installer but it can also be used to
+create bootable USB-Sticks from your normal workstation. Furthermore
+`disko-install` has a mount mode that will only mount but not destroy existing
+partitions. The mount mode can be used to mount and repair existing NixOS
+installations. This document provides a comprehensive guide on how to use
+**disko-install**, including examples for typical usage scenarios.
+
+## Requirements
+
+- a Linux system with Nix installed.
+- a target disk or partition for the NixOS installation.
+- a Nix flake that defines your desired NixOS configuration.
+
+## Usage
+
+### Fresh Installation
+
+For a fresh installation, where **disko-install** will handle partitioning and
+setting up the disk, use the following syntax:
+
+```console
+sudo nix run 'github:nix-community/disko/latest#disko-install' -- --flake # --disk
+```
+
+Example:
+
+First run `nixos-generate-config --root /tmp/config --no-filesystems` and edit
+`configuration.nix` to your liking.
+
+Then add the following `flake.nix` inside `/tmp/config/etc/nixos`. In this
+example we assume a system that has been booted with EFI:
+
+```nix
+{
+ inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+ inputs.disko.url = "github:nix-community/disko/latest";
+ inputs.disko.inputs.nixpkgs.follows = "nixpkgs";
+
+ outputs = { self, disko, nixpkgs }: {
+ nixosConfigurations.mymachine = nixpkgs.legacyPackages.x86_64-linux.nixos [
+ ./configuration.nix
+ disko.nixosModules.disko
+ {
+ disko.devices = {
+ disk = {
+ main = {
+ # When using disko-install, we will overwrite this value from the commandline
+ device = "/dev/disk/by-id/some-disk-id";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ MBR = {
+ type = "EF02"; # for grub MBR
+ size = "1M";
+ priority = 1; # Needs to be first partition
+ };
+ ESP = {
+ type = "EF00";
+ size = "500M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ }
+ ];
+ };
+}
+```
+
+Identify the device name that you want to install NixOS to:
+
+```console
+$ lsblk
+NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
+sda 8:0 1 14.9G 0 disk
+└─sda1 8:1 1 14.9G 0 part
+zd0 230:0 0 10G 0 disk
+├─zd0p1 230:1 0 500M 0 part
+└─zd0p2 230:2 0 9.5G 0 part /mnt
+nvme0n1 259:0 0 1.8T 0 disk
+├─nvme0n1p1 259:1 0 1G 0 part /boot
+├─nvme0n1p2 259:2 0 16M 0 part
+├─nvme0n1p3 259:3 0 250G 0 part
+└─nvme0n1p4 259:4 0 1.6T 0 part
+```
+
+In our example, we want to install to a USB-stick (/dev/sda):
+
+```console
+$ sudo nix run 'github:nix-community/disko/latest#disko-install' -- --flake '/tmp/config/etc/nixos#mymachine' --disk main /dev/sda
+```
+
+Afterwards you can test your USB-stick by either selecting during the boot or
+attaching it to a qemu-vm:
+
+```
+$ sudo qemu-kvm -enable-kvm -hda /dev/sda
+```
+
+### Persisting boot entries to EFI vars flash
+
+**disko-install** is designed for NixOS installations on portable storage or
+disks that may be transferred between computers. As such, it does not modify the
+host's NVRAM by default. To ensure your NixOS installation boots seamlessly on
+new hardware or to prioritize it in your current machine's boot order, use the
+--write-efi-boot-entries option:
+
+```console
+$ sudo nix run 'github:nix-community/disko/latest#disko-install' -- --write-efi-boot-entries --flake '/tmp/config/etc/nixos#mymachine' --disk main /dev/sda
+```
+
+This command installs NixOS with **disko-install** and sets the newly installed
+system as the default boot option, without affecting the flexibility to boot
+from other devices if needed.
+
+### Using disko-install in an offline installer
+
+If you want to use **disko-install** from a custom installer without internet,
+you need to make sure that in addition to the toplevel of your NixOS closure
+that you plan to install, it also needs to contain **diskoScript** and all the
+flake inputs for evaluation.
+
+#### Example configuration to install
+
+Add this to your flake.nix output:
+
+```nix
+{
+ nixosConfigurations.your-machine = inputs.nixpkgs.lib.nixosSystem {
+ system = "x86_64-linux";
+ # to pass this flake into your configuration (see the example below)
+ specialArgs = {inherit self;};
+ modules = [
+ {
+ # TODO: add your NixOS configuration here, don't forget your hardware-configuration.nix as well!
+ boot.loader.systemd-boot.enable = true;
+ imports = [ self.inputs.disko.nixosModules.disko ];
+ disko.devices = {
+ disk = {
+ vdb = {
+ device = "/dev/disk/by-id/some-disk-id";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ type = "EF00";
+ size = "500M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ }
+ ];
+ };
+}
+```
+
+#### Example for a NixOS installer
+
+```nix
+# `self` here is referring to the flake `self`, you may need to pass it using `specialArgs` or define your NixOS installer configuration
+# in the flake.nix itself to get direct access to the `self` flake variable.
+{ pkgs, self, ... }:
+let
+ dependencies = [
+ self.nixosConfigurations.your-machine.config.system.build.toplevel
+ self.nixosConfigurations.your-machine.config.system.build.diskoScript
+ self.nixosConfigurations.your-machine.config.system.build.diskoScript.drvPath
+ self.nixosConfigurations.your-machine.pkgs.stdenv.drvPath
+
+ # https://github.com/NixOS/nixpkgs/blob/f2fd33a198a58c4f3d53213f01432e4d88474956/nixos/modules/system/activation/top-level.nix#L342
+ self.nixosConfigurations.your-machine.pkgs.perlPackages.ConfigIniFiles
+ self.nixosConfigurations.your-machine.pkgs.perlPackages.FileSlurp
+
+ (self.nixosConfigurations.your-machine.pkgs.closureInfo { rootPaths = [ ]; }).drvPath
+ ] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
+
+ closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
+in
+# Now add `closureInfo` to your NixOS installer
+{
+ environment.etc."install-closure".source = "${closureInfo}/store-paths";
+
+ environment.systemPackages = [
+ (pkgs.writeShellScriptBin "install-nixos-unattended" ''
+ set -eux
+ # Replace "/dev/disk/by-id/some-disk-id" with your actual disk ID
+ exec ${pkgs.disko}/bin/disko-install --flake "${self}#your-machine" --disk vdb "/dev/disk/by-id/some-disk-id"
+ '')
+ ];
+}
+```
+
+Also see the
+[NixOS test of disko-install](https://github.com/nix-community/disko/blob/master/tests/disko-install/default.nix)
+that also runs without internet.
diff --git a/pkgs/disko/docs/interactive-vm.md b/pkgs/disko/docs/interactive-vm.md
new file mode 100644
index 0000000..02d1244
--- /dev/null
+++ b/pkgs/disko/docs/interactive-vm.md
@@ -0,0 +1,26 @@
+# Running Interactive VMs with disko
+
+disko now exports its own flavor of interactive VMs (similiar to
+config.system.build.vm). Simply import the disko module and build the vm runner
+with:
+
+```
+nix run -L '.#nixosConfigurations.mymachine.config.system.build.vmWithDisko'
+```
+
+You can configure the VM using the `virtualisation.vmVariantWithDisko` NixOS
+option:
+
+```nix
+{
+ virtualisation.vmVariantWithDisko = {
+ virtualisation.fileSystems."/persist".neededForBoot = true;
+ # For running VM on macos: https://www.tweag.io/blog/2023-02-09-nixos-vm-on-macos/
+ # virtualisation.host.pkgs = inputs.nixpkgs.legacyPackages.aarch64-darwin;
+ };
+}
+```
+
+extraConfig that is set in disko.tests.extraConfig is also applied to the
+interactive VMs. imageSize of the VMs will be determined by the imageSize in the
+disk type in your disko config. memorySize is set by disko.memSize
diff --git a/pkgs/disko/docs/logo.png b/pkgs/disko/docs/logo.png
new file mode 100644
index 0000000..56688e6
Binary files /dev/null and b/pkgs/disko/docs/logo.png differ
diff --git a/pkgs/disko/docs/quickstart.md b/pkgs/disko/docs/quickstart.md
new file mode 100644
index 0000000..b0f3bb8
--- /dev/null
+++ b/pkgs/disko/docs/quickstart.md
@@ -0,0 +1,225 @@
+# disko - Declarative disk partitioning
+
+
+
+[Documentation Index](./INDEX.md)
+
+## Quickstart Guide
+
+This tutorial describes how to install NixOS on a single disk system using
+`disko`. You will also need to refer to the NixOS manual, which is available
+[here.](https://nixos.org/manual/nixos/stable/index.html#ex-config)
+
+Please note that `disko` will reformat the entire disk and overwrite any
+existing partitions. Dual booting with other operating systems is not supported.
+
+### Step 1: Choose a Disk Configuration
+
+Real-world templates are provided in this
+[repository](https://github.com/nix-community/disko-templates).
+
+More disk layouts for all filesystems can be also found in the
+[example](https://github.com/nix-community/disko/tree/master/example) directory
+of disko. However these examples are also used for regression tests in disko and
+may have uncommon options in them to fully exercise all features of disko, that
+you may need to change or remove.
+
+Decide which of these layouts best suits your requirements. If you're not sure
+which layout to pick, use the
+[single-disk-ext4](https://github.com/nix-community/disko-templates/blob/main/single-disk-ext4/disko-config.nix)
+configuration. This layout is compatible with both BIOS and EFI systems.
+
+Refer to the [reference manual](./reference.md) for more information about the
+sample layouts and how to build your own configuration.
+
+To copy a template use this command in your nixos configuration directory:
+
+```
+nix flake init --template github:nix-community/disko-templates#single-disk-ext4
+```
+
+This will write a file called `disko-config.nix` into the current directory.
+Import this file in your NixOS configuration:
+
+```nix
+{
+ imports = [ ./disko-config.nix ];
+}
+```
+
+If you want to choose a layout from the disko example directory instead, you'll
+need to make a note of the URL to the raw file. To do this, open the file in
+Github. Immediately below the list of contributors, you will see a button
+labelled 'RAW' near the right hand side. Click this. The URL of the raw file
+will appear in the search bar of your browser. It will look something like this:
+
+```
+https://raw.githubusercontent.com/nix-community/disko/master/example/hybrid.nix
+```
+
+### Step 2: Boot the installer
+
+Download the NixOS ISO image from the NixOS
+[download page](https://nixos.org/download.html#nixos-iso), and create a
+bootable USB drive following the instructions
+in [Section 2.4.1 "Booting from a USB flash drive"](https://nixos.org/manual/nixos/stable/index.html#sec-booting-from-usb) of
+the NixOS manual. Boot the machine from this USB drive.
+
+### Step 3: Retrieve the disk name
+
+Identify the name of your system disk by using the `lsblk` command as follows:
+
+```console
+lsblk
+```
+
+The output from this command will look something like this:
+
+```
+NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
+nvme0n1 259:0 0 1,8T 0 disk
+```
+
+In this example, an empty NVME SSD with 2TB space is shown with the disk name
+"nvme0n1". Make a note of the disk name as you will need it later.
+
+### Step 4: Copy the disk configuration to your machine
+
+In Step 1, you chose a disk layout configuration from the
+[examples directory](https://github.com/nix-community/disko/tree/master/example),
+and made a note of its URL.
+
+Your configuration needs to be saved on the new machine for example
+as /tmp/disk-config.nix. You can do this using the `curl` command to download
+from the url you noted above, using the `-o` option to save the file as
+disk-config.nix. Your commands would look like this if you had chosen the hybrid
+layout:
+
+```console
+cd /tmp
+curl https://raw.githubusercontent.com/nix-community/disko/master/example/hybrid.nix -o /tmp/disk-config.nix
+```
+
+### Step 5: Adjust the device in the disk configuration
+
+Inside the disk-config.nix the device needs to point to the correct disk name.
+
+Open the configuration in your favorite editor i.e.:
+
+```console
+nano /tmp/disk-config.nix
+```
+
+Replace `` with the name of your disk obtained in Step 1.
+
+```nix
+# ...
+main = {
+ type = "disk";
+ device = "";
+ content = {
+ type = "gpt";
+# ...
+```
+
+### Step 6: Run disko to partition, format and mount your disks
+
+The following step will partition and format your disk, and mount it to `/mnt`.
+
+**Please note: This will erase any existing data on your disk.**
+
+```console
+sudo nix --experimental-features "nix-command flakes" run github:nix-community/disko/latest -- --mode destroy,format,mount /tmp/disk-config.nix
+```
+
+After the command has run, your file system should have been formatted and
+mounted. You can verify this by running the following command:
+
+```console
+mount | grep /mnt
+```
+
+The output should look like this if your disk name is `nvme0n1`.
+
+```
+/dev/nvme0n1p1 on /mnt type ext4 (rw,relatime,stripe=2)
+/dev/nvme0n1p2 on /mnt/boot type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro)
+```
+
+### Step 7: Complete the NixOS installation.
+
+Your disks have now been formatted and mounted, and you are ready to complete
+the NixOS installation as described in the
+[NixOS manual](https://nixos.org/manual/nixos/stable/index.html#sec-installation) -
+see the section headed "**Installing**", Steps 3 onwards. However, you will need
+to include the partitioning and formatting configurations that you copied into
+`/tmp/disk-config.nix` in your configuration, rather than allowing NixOS to
+generate information about your file systems. When you are configuring the
+system as per Step 4 of the manual, you should:
+
+a) Include the `no-filesystems` switch when using the `nixos-generate-config`
+command to generate an initial `configuration.nix`. You will be supplying the
+file system configuration details from `disk-config.nix`. Your CLI command to
+generate the configuration will be:
+
+```console
+nixos-generate-config --no-filesystems --root /mnt
+```
+
+This will create the file `configuration.nix` in `/mnt/etc/nixos`.
+
+b) Move the `disko` configuration to /etc/nixos
+
+```console
+mv /tmp/disk-config.nix /mnt/etc/nixos
+```
+
+c) You can now edit `configuration.nix` as per your requirements. This is
+described in Step 4 of the manual. For more information about configuring your
+system, refer to the NixOS manual.
+[Chapter 6, Configuration Syntax](https://nixos.org/manual/nixos/stable/index.html#sec-configuration-syntax)
+describes the NixOS configuration syntax, and
+[Appendix A, Configuration Options](https://nixos.org/manual/nixos/stable/options.html)
+gives a list of available options. You can find also find a minimal example of a
+NixOS configuration in the manual:
+[Example: NixOS Configuration](https://nixos.org/manual/nixos/stable/index.html#ex-config).
+
+d) When editing `configuration.nix`, you will need to add the `disko` NixOS
+module and `disk-config.nix` to the imports section. This section will already
+include the file `./hardware-configuration.nix`, and you can add the new entries
+just below this. This section will now include:
+
+```nix
+imports =
+ [ # Include the results of the hardware scan.
+ ./hardware-configuration.nix
+ "${builtins.fetchTarball "https://github.com/nix-community/disko/archive/master.tar.gz"}/module.nix"
+ ./disk-config.nix
+ ];
+```
+
+e) If you chose the hybrid-partition scheme, then choose `grub` as a bootloader,
+otherwise follow the recommendations in Step 4 of the **Installation** section
+of the NixOS manual. The following configuration for `grub` works for both EFI
+and BIOS systems. Add this to your configuration.nix, commenting out the
+existing lines that configure `systemd-boot`. The entries will look like this:
+
+**Note:** Its not necessary to set `boot.loader.grub.device` here, since Disko
+will take care of that automatically.
+
+```nix
+# ...
+ #boot.loader.systemd-boot.enable = true;
+ #boot.loader.efi.canTouchEfiVariables = true;
+ boot.loader.grub.enable = true;
+ boot.loader.grub.efiSupport = true;
+ boot.loader.grub.efiInstallAsRemovable = true;
+# ...
+```
+
+f) Finish the installation and reboot your machine,
+
+```console
+nixos-install
+reboot
+```
diff --git a/pkgs/disko/docs/reference.md b/pkgs/disko/docs/reference.md
new file mode 100644
index 0000000..3d390b9
--- /dev/null
+++ b/pkgs/disko/docs/reference.md
@@ -0,0 +1,47 @@
+# Reference Manual: disko
+
+## Module Options
+
+We are currently having issues being able to generate proper module option
+documentation for our recursive disko types. However you can read the available
+options [here](https://github.com/nix-community/disko/tree/master/lib/types).
+Combined with the
+[examples](https://github.com/nix-community/disko/tree/master/example) this
+hopefully gives you an overview.
+
+## Command Line Options
+
+```
+Usage: ./disko [options] disk-config.nix
+or ./disko [options] --flake github:somebody/somewhere#disk-config
+
+With flakes, disk-config is discovered first under the .diskoConfigurations top level attribute
+or else from the disko module of a NixOS configuration of that name under .nixosConfigurations.
+
+Options:
+
+* -m, --mode mode
+ set the mode, either distroy, format, mount, format,mount or destroy,format,mount
+ destroy: unmount filesystems and destroy partition tables of the selected disks
+ format: create partition tables, zpools, lvms, raids and filesystems if they don't exist yet
+ mount: mount the partitions at the specified root-mountpoint
+ format,mount: run format and mount in sequence
+ destroy,format,mount: run all three modes in sequence. Previously known as --mode disko
+* -f, --flake uri
+ fetch the disko config relative to this flake's root
+* --arg name value
+ pass value to nix-build. can be used to set disk-names for example
+* --argstr name value
+ pass value to nix-build as string
+* --root-mountpoint /some/other/mnt
+ where to mount the device tree (default: /mnt)
+* --dry-run
+ just show the path to the script instead of running it
+* --no-deps
+ avoid adding another dependency closure to an in-memory installer
+ requires all necessary dependencies to be available in the environment
+* --debug
+ run with set -x
+* --yes-wipe-all-disks
+ skip the safety check for destroying partitions, useful for automation
+```
diff --git a/pkgs/disko/docs/requirements.md b/pkgs/disko/docs/requirements.md
new file mode 100644
index 0000000..93d8398
--- /dev/null
+++ b/pkgs/disko/docs/requirements.md
@@ -0,0 +1,9 @@
+# disko - Declarative disk partitioning
+
+
+
+[Documentation Index](./INDEX.md)
+
+## System Requirements
+
+TODO: Populate this
diff --git a/pkgs/disko/docs/supportmatrix.md b/pkgs/disko/docs/supportmatrix.md
new file mode 100644
index 0000000..53ee715
--- /dev/null
+++ b/pkgs/disko/docs/supportmatrix.md
@@ -0,0 +1,9 @@
+# disko - Declarative disk partitioning
+
+
+
+[Documentation Index](./INDEX.md)
+
+## Support Matrix
+
+TODO: Populate this
diff --git a/pkgs/disko/docs/table-to-gpt.md b/pkgs/disko/docs/table-to-gpt.md
new file mode 100644
index 0000000..7ca5d80
--- /dev/null
+++ b/pkgs/disko/docs/table-to-gpt.md
@@ -0,0 +1,137 @@
+# Migrating to the new GPT layout
+
+## Situation
+
+When evaluating your NixOS system closure the following trace appears:
+
+```
+trace: warning: The legacy table is outdated and should not be used. We recommend using the gpt type instead.
+Please note that certain features, such as the test framework, may not function properly with the legacy table type.
+If you encounter errors similar to:
+"error: The option `disko.devices.disk.disk1.content.partitions."[definition 1-entry 1]".content._config` is read-only, but it's set multiple times,"
+this is likely due to the use of the legacy table type.
+```
+
+The solution is to migrate to the new `gpt` layout type.
+
+## Precondition
+
+Disko was set up with
+
+- `type = "table"` and
+- `format = "gpt"`,
+
+for example like this:
+
+```nix
+{
+ disko.devices.disk.example = {
+ type = "disk";
+ device = "/dev/nvme0n1";
+ content = {
+ type = "table";
+ format = "gpt";
+ partitions = [
+ {
+ name = "ESP";
+ start = "0";
+ end = "512MiB";
+ fs-type = "fat32";
+ bootable = true;
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ };
+ }
+ {
+ name = "root";
+ start = "512MiB";
+ end = "100%";
+ content.format = "ext4";
+ }
+ ];
+ };
+ };
+}
+```
+
+## Remediation
+
+The new GPT layout (`type = "gpt"`) uses partlabels to realize the partiton
+numbering. For this reason you have to manually set up partition labels, if you
+want to resolve this issue.
+
+### Create GPT partition labels
+
+For each partition involved, create the partition label from these components:
+
+- The partition number (e.g. /dev/nvme0n**1**, or /dev/sda**1**)
+- The parent type in your disko config (value of
+ `disko.device.disk.example.type = "disk";`)
+- The parent name in your disko config (attribute name of
+ `disko.devices.disk.example`, so `example` in this example)
+- The partition name in your disko config (attribute name of
+ `disko.devices.disk.content.partitions.*.name`)
+
+```bash
+# sgdisk -c 1:disk-example-ESP /dev/nvme0n1
+# sgdisk -c 2:disk-example-zfs /dev/nvme0n1
+Warning: The kernel is still using the old partition table.
+The new table will be used at the next reboot or after you
+run partprobe(8) or kpartx(8)
+The operation has completed successfully.
+```
+
+### Update disko configuration
+
+Make the following changes to your disko configuration:
+
+1. Set `disko.devices.disk.example.content.type = "gpt"`
+1. Remove `disko.devices.disk.example.content.format`
+1. Convert `disko.devices.disk.example.content.partitions` to an attribute set and
+ promote the `name` field to the key for its partition
+1. Add a `priority` field to each partition, to reflect the intended partition
+ number
+
+Then rebuild your system and reboot.
+
+### Recovering from mistake
+
+If you made a mistake here, your system will be waiting for devices to appear,
+and then run into timeouts. You can easily recover from this, since rebooting
+into an old generation will still use the legacy way of numbering of partitions.
+
+## Result
+
+The fixed disko configuration would look like this:
+
+```nix
+{
+ disko.devices.disk.example = {
+ type = "disk";
+ device = "/dev/nvme0n1";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "512MiB";
+ type = "EF00";
+ priority = 1;
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ priority = 2;
+ content.format = "ext4";
+ };
+ };
+ };
+ };
+}
+```
diff --git a/pkgs/disko/docs/testing.md b/pkgs/disko/docs/testing.md
new file mode 100644
index 0000000..769b1a3
--- /dev/null
+++ b/pkgs/disko/docs/testing.md
@@ -0,0 +1,160 @@
+# Running and debugging tests
+
+Disko makes extensive use of VM tests. All examples you can find in
+[the example directory](../example) have a respective test suite that verifies
+the example is working in [the tests directory](../tests/). They utilize the
+[NixOS test functionality](https://nixos.org/manual/nixos/stable/#sec-nixos-tests).
+
+We use a wrapper around this called `makeDiskoTest`. There is currently (as of
+2024-10-16) no documentation for all its arguments, but you can have a look at
+[its current code](https://github.com/nix-community/disko/blob/master/lib/tests.nix#L44C5-L58C10),
+that should already be helpful.
+
+However, you don't need to know about all of the inner workings to interact with
+the tests effectively. For some of the most common operations, see the sections
+below.
+
+## Run just one of the tests
+
+```sh
+nix build --no-link .#checks.x86_64-linux.simple-efi
+```
+
+This will run the test in [`tests/simple-efi.nix`](../tests/simple-efi.nix),
+which builds a VM with all disks specified in the
+[`example/simple-efi.nix`](../example/simple-efi.nix) config connected as
+virtual devices, run disko to format them, reboot, verify the VM boots properly,
+and then run the code specified in `extraTestScript` to validate that the
+partitions have been created and were mounted as expected.
+
+### How `extraTestScript` works
+
+This is written in Python. The most common lines you'll see look something like
+this:
+
+```python
+machine.succeed("test -b /dev/md/raid1");
+machine.succeed("mountpoint /");
+```
+
+The `machine` in these is a machine object, which defines
+[a multitude of functions to interact with and test](https://nixos.org/manual/nixos/stable/#ssec-machine-objects),
+assumptions about the state of the VM after formatting and rebooting.
+
+Disko currently (as of 2024-10-16) doesn't have any tests that utilize multiple
+VMs at once, so the only machine available in these scripts is always just the
+default `machine`.
+
+## Debugging tests
+
+If you make changes to disko, you might break a test, or you may want to modify
+a test to prevent regressions. In these cases, running the full test with
+`nix build` every time is time-consuming and tedious.
+
+Instead, you can build and then run the VM for a test in interactive mode. This
+will create the VM and all virtual disks as required by the test's config, but
+allow you to interact with the machine on a terminal afterwards.
+
+First, build the interactive test driver and run it:
+
+```
+nix build .#checks.x86_64-linux.simple-efi.driverInteractive
+result/bin/nixos-test-driver --keep-vm-state
+```
+
+This will open an IPython prompt in which you can use th same objects and
+functions as in `extraTestScript`. In there, you can run
+
+```
+machine.shell_interact()
+```
+
+to start the VM and attach the terminal to it. This will also open a QEMU
+window, in which you can log in as `root` with no password, but that makes it
+more difficult to paste input and output. Instead, wait for the systemd messages
+to settle down, and then **simply start typing**. This should make a `$` prompt
+appear, indicating that the machine is ready to take commands. The NixOS manual
+calls out a few special messages to look for, but these are buried underneath
+the systemd logs.
+
+Once you are in this terminal, you're running commands on the VM. The only thing
+that doesn't work here is the `exit` command. Instead, you need to press Ctrl+D
+and wait for a second to return to the IPython prompt.
+
+In summary, a full session looks something like this:
+
+```
+# nix build .#checks.x86_64-linux.simple-efi.driverInteractive
+# result/bin/nixos-test-driver --keep-vm-state
+start all VLans
+start vlan
+running vlan (pid 146244; ctl /tmp/vde1.ctl)
+(finished: start all VLans, in 0.00 seconds)
+additionally exposed symbols:
+ machine,
+ vlan1,
+ start_all, test_script, machines, vlans, driver, log, os, create_machine, subtest, run_tests, join_all, retry, serial_stdout_off, serial_stdout_on, polling_condition, Machine
+>>> machine.shell_interact()
+machine: waiting for the VM to finish booting
+machine: starting vm
+machine: QEMU running (pid 146286)
+machine # [ 0.000000] Linux version 6.6.48 (nixbld@localhost) (gcc (GCC) 13.3.0, GNU ld (GNU Binutils) 2.42) #1-NixOS SMP PREEMPT_DYNAMIC Thu Aug 29 15:33:59 UTC 2024
+machine # [ 0.000000] Command line: console=ttyS0 panic=1 boot.panic_on_fail clocksource=acpi_pm loglevel=7 net.ifnames=0 init=/nix/store/0a52bbvxr5p7xijbbk17qqlk8xm4790y-nixos-system-machine-test/init regInfo=/nix/store/3sh5nl75bnj1jg87p5gcrdzs0lk154ma-closure-info/registration console=ttyS0
+machine # [ 0.000000] BIOS-provided physical RAM map:
+...
+... more systemd messages
+...
+machine # [ 6.135577] dhcpcd[679]: DUID 00:01:00:01:2e:a2:74:e6:52:54:00:12:34:56
+machine # [ 6.142785] systemd[1]: Finished Kernel Auditing.
+machine: Guest shell says: b'Spawning backdoor root shell...\n'
+machine: connected to guest root shell
+machine: (connecting took 6.61 seconds)
+(finished: waiting for the VM to finish booting, in 6.99 seconds)
+machine: Terminal is ready (there is no initial prompt):
+machine # [ 6.265451] 8021q: 802.1Q VLAN Support v1.8
+machine # [ 6.186797] nsncd[669]: Oct 16 13:11:55.010 INFO started, config: Config { ignored_request_types: {}, worker_count: 8, handoff_timeout: 3s }, path: "/var/run/nscd/socket"
+...
+... more systemd messages
+...
+machine # [ 12.376900] systemd[1]: Reached target Host and Network Name Lookups.
+machine # [ 12.379265] systemd[1]: Reached target User and Group Name Lookups.
+$ lsblk
+NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
+fd0 2:0 1 4K 0 disk
+sr0 11:0 1 1024M 0 rom
+vda 253:0 0 1G 0 disk /
+vdb 253:16 0 4G 0 disk
+├─vdb1 253:17 0 500M 0 part
+└─vdb2 253:18 0 3.5G 0 part
+```
+
+You can find some additional details in
+[the NixOS manual's section on interactive testing](https://nixos.org/manual/nixos/stable/#sec-running-nixos-tests-interactively).
+
+## Running all tests at once
+
+If you have a bit of experience, you might be inclined to run `nix flake check`
+to run all tests at once. However, we instead recommend using
+[nix-fast-build](https://github.com/Mic92/nix-fast-build). The reason for this
+is that each individual test takes a while to run, but only uses <=4GiB of RAM
+and a limited amount of CPU resources. This means they can easily be evaluated
+and run in parallel to save time, but `nix` doesn't to that, so a full test run
+takes >40 minutes on a mid-range system. With `nix-fast-build` you can scale up
+the number of workers depending on your system's capabilities. It also utilizes
+[`nix-output-monitor`](https://github.com/maralorn/nix-output-monitor) to give
+you a progress indicator during the build process as well. For example, on a
+machine with 16GB of RAM, this gives you a 2x speed up without clogging your
+system:
+
+```sh
+nix shell nixpkgs#nix-fast-build
+nix-fast-build --no-link -j 2 --eval-workers 2 --flake .#checks
+```
+
+You can try higher numbers if you want to. Be careful with scaling up
+`--eval-workers`, each of these will use 100% of a CPU core and they don't leave
+any time for hyperthreading, so 4 workers will max out a a CPU with 4 cores and
+8 threads, potentially rendering your system unresponsive! `-j` is less
+dangerous to scale up, but you probably don't want to go higher than
+`( - 4GB)/4GB` to prevent excessive swap usage, which will
+would slow down the test VMs to a crawl.
diff --git a/pkgs/disko/docs/upgrade-guide.md b/pkgs/disko/docs/upgrade-guide.md
new file mode 100644
index 0000000..e6aa835
--- /dev/null
+++ b/pkgs/disko/docs/upgrade-guide.md
@@ -0,0 +1,173 @@
+# 2023-07-09 121df48
+
+Changes:
+
+- BTRFS subvolumes are mounted if and only their `mountpoint` is set.
+
+Especially, if you have specific mount options for a subvolume (through
+`mountOptions`), make sure to set `mountpoint` otherwise the subvolume will not
+be mounted.
+
+This change allows more flexibility when using BTRFS, giving access to its
+volume management functionality.
+
+It allows layouts as the following:
+
+```nix
+content = {
+ type = "btrfs";
+ # BTRFS partition is not mounted as it doesn't set a mountpoint explicitly
+ subvolumes = {
+ # This subvolume will not be mounted
+ "SYSTEM" = { };
+ # mounted as "/"
+ "SYSTEM/rootfs" = {
+ mountpoint = "/";
+ };
+ # mounted as "/nix"
+ "SYSTEM/nix" = {
+ mountOptions = [ "compress=zstd" "noatime" ];
+ mountpoint = "/nix";
+ };
+ # This subvolume will not be mounted
+ "DATA" = { };
+ # mounted as "/home"
+ "DATA/home" = {
+ mountOptions = [ "compress=zstd" ];
+ mountpoint = "/home";
+ };
+ # mounted as "/var/www"
+ "DATA/www" = {
+ mountpoint = "/var/www";
+ };
+ };
+};
+```
+
+corresponding to the following BTRFS layout:
+
+```
+BTRFS partition # not mounted
+ |
+ |-SYSTEM # not mounted
+ | |-rootfs # mounted as "/"
+ | |-nix # mounted as "/nix"
+ |
+ |-DATA # not mounted
+ |-home # mounted as "/home"
+ |-www # mounted as "/var/www"
+```
+
+# 2023-04-07 7d70009
+
+Changes:
+
+- ZFS datasets have been split into two types: `zfs_fs` and `zfs_volume`.
+- The `zfs_type` attribute has been removed.
+- The size attribute is now only available for `zfs_volume`.
+
+Updated example/zfs.nix file:
+
+```nix
+{
+datasets = {
+ zfs_fs = {
+ type = "zfs_fs";
+ mountpoint = "/zfs_fs";
+ options."com.sun:auto-snapshot" = "true";
+ };
+ zfs_unmounted_fs = {
+ type = "zfs_fs";
+ options.mountpoint = "none";
+ };
+ zfs_legacy_fs = {
+ type = "zfs_fs";
+ options.mountpoint = "legacy";
+ mountpoint = "/zfs_legacy_fs";
+ };
+ zfs_testvolume = {
+ type = "zfs_volume";
+ size = "10M";
+ content = {
+ type = "filesystem";
+ # ...
+}
+```
+
+Note: The `zfs_type` attribute has been replaced with a type attribute for each
+dataset, and the `size` attribute is only available for `zfs_volume`. These
+changes have been reflected in the `example/zfs.nix` file.
+
+# 2023-04-07 654ecb3
+
+The `lvm_lv` type is always part of an `lvm_vg` and it is no longer necessary to
+specify the type.
+
+This means that if you were using the `lvm_lv` type in your code, you should
+remove it. For example, if you were defining an `lvm_lv` type like this:
+
+```nix
+{
+ type = "lvm_lv";
+ size = "10G";
+ # ...
+}
+```
+
+You should now define it like this:
+
+```nix
+{
+ size = "10G";
+ # ...
+}
+```
+
+Note that the `type` field is no longer necessary and should be removed from
+your code.
+
+# 2023-04-07 d6f062e
+
+Partition types are now always part of a table and cannot be specified
+individually anymore. This change makes the library more consistent and easier
+to use.
+
+Example of how to change code:
+
+Before:
+
+```nix
+{
+ type = "partition";
+ name = "ESP";
+ start = "1MiB";
+ end = "100MiB";
+ part-type = "primary";
+}
+```
+
+After:
+
+```nix
+{
+ name = "ESP";
+ start = "1MiB";
+ end = "100MiB";
+ part-type = "primary";
+}
+```
+
+Note that the `type` field is no longer necessary and should be removed from
+your code.
+
+# 2023-03-22 2624af6
+
+disk config now needs to be inside a disko.devices attrset always
+
+# 2023-03-22 0577409
+
+the extraArgs option in the luks type was renamed to extraFormatArgs
+
+# 2023-02-14 6d630b8
+
+btrfs, `btrfs_subvol` filesystem and `lvm_lv` extraArgs are now lists
diff --git a/pkgs/disko/example/bcachefs.nix b/pkgs/disko/example/bcachefs.nix
new file mode 100644
index 0000000..c7e3bf3
--- /dev/null
+++ b/pkgs/disko/example/bcachefs.nix
@@ -0,0 +1,34 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/disk/by-path/pci-0000:02:00.0-nvme-1";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ end = "500M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ name = "root";
+ end = "-0";
+ content = {
+ type = "filesystem";
+ format = "bcachefs";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/boot-raid1.nix b/pkgs/disko/example/boot-raid1.nix
new file mode 100644
index 0000000..39569c1
--- /dev/null
+++ b/pkgs/disko/example/boot-raid1.nix
@@ -0,0 +1,89 @@
+{
+ disko.devices = {
+ disk = {
+ one = {
+ type = "disk";
+ device = "/dev/sda";
+ content = {
+ type = "gpt";
+ partitions = {
+ BOOT = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ ESP = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "mdraid";
+ name = "boot";
+ };
+ };
+ mdadm = {
+ size = "100%";
+ content = {
+ type = "mdraid";
+ name = "raid1";
+ };
+ };
+ };
+ };
+ };
+ two = {
+ type = "disk";
+ device = "/dev/sdb";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ ESP = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "mdraid";
+ name = "boot";
+ };
+ };
+ mdadm = {
+ size = "100%";
+ content = {
+ type = "mdraid";
+ name = "raid1";
+ };
+ };
+ };
+ };
+ };
+ };
+ mdadm = {
+ boot = {
+ type = "mdadm";
+ level = 1;
+ metadata = "1.0";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ };
+ };
+ raid1 = {
+ type = "mdadm";
+ level = 1;
+ content = {
+ type = "gpt";
+ partitions.primary = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/btrfs-only-root-subvolume.nix b/pkgs/disko/example/btrfs-only-root-subvolume.nix
new file mode 100644
index 0000000..46e7334
--- /dev/null
+++ b/pkgs/disko/example/btrfs-only-root-subvolume.nix
@@ -0,0 +1,38 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ type = "disk";
+ device = "/dev/disk/by-diskseq/1";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ priority = 1;
+ name = "ESP";
+ start = "1M";
+ end = "128M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "btrfs";
+ extraArgs = [ "-f" ]; # Override existing partition
+ mountpoint = "/";
+ mountOptions = [ "compress=zstd" "noatime" ];
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
+
diff --git a/pkgs/disko/example/btrfs-subvolumes.nix b/pkgs/disko/example/btrfs-subvolumes.nix
new file mode 100644
index 0000000..8f36443
--- /dev/null
+++ b/pkgs/disko/example/btrfs-subvolumes.nix
@@ -0,0 +1,77 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ type = "disk";
+ device = "/dev/disk/by-diskseq/1";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ priority = 1;
+ name = "ESP";
+ start = "1M";
+ end = "128M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "btrfs";
+ extraArgs = [ "-f" ]; # Override existing partition
+ # Subvolumes must set a mountpoint in order to be mounted,
+ # unless their parent is mounted
+ subvolumes = {
+ # Subvolume name is different from mountpoint
+ "/rootfs" = {
+ mountpoint = "/";
+ };
+ # Subvolume name is the same as the mountpoint
+ "/home" = {
+ mountOptions = [ "compress=zstd" ];
+ mountpoint = "/home";
+ };
+ # Sub(sub)volume doesn't need a mountpoint as its parent is mounted
+ "/home/user" = { };
+ # Parent is not mounted so the mountpoint must be set
+ "/nix" = {
+ mountOptions = [ "compress=zstd" "noatime" ];
+ mountpoint = "/nix";
+ };
+ # This subvolume will be created but not mounted
+ "/test" = { };
+ # Subvolume for the swapfile
+ "/swap" = {
+ mountpoint = "/.swapvol";
+ swap = {
+ swapfile.size = "20M";
+ swapfile2.size = "20M";
+ swapfile2.path = "rel-path";
+ };
+ };
+ };
+
+ mountpoint = "/partition-root";
+ swap = {
+ swapfile = {
+ size = "20M";
+ };
+ swapfile1 = {
+ size = "20M";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
+
diff --git a/pkgs/disko/example/complex.nix b/pkgs/disko/example/complex.nix
new file mode 100644
index 0000000..210105e
--- /dev/null
+++ b/pkgs/disko/example/complex.nix
@@ -0,0 +1,192 @@
+{
+ disko.devices = {
+ disk = {
+ disk0 = {
+ type = "disk";
+ device = "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ start = "1M";
+ end = "128M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ };
+ };
+ };
+ disk1 = {
+ type = "disk";
+ device = "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00002";
+ content = {
+ type = "gpt";
+ partitions = {
+ luks = {
+ start = "1M";
+ size = "100%";
+ content = {
+ type = "luks";
+ name = "crypted1";
+ settings.keyFile = "/tmp/secret.key";
+ additionalKeyFiles = [ "/tmp/additionalSecret.key" ];
+ extraFormatArgs = [
+ "--iter-time 1" # insecure but fast for tests
+ ];
+ content = {
+ type = "lvm_pv";
+ vg = "pool";
+ };
+ };
+ };
+ };
+ };
+ };
+ disk2 = {
+ type = "disk";
+ device = "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00003";
+ content = {
+ type = "gpt";
+ partitions = {
+ luks = {
+ start = "1M";
+ size = "100%";
+ content = {
+ type = "luks";
+ name = "crypted2";
+ settings = {
+ keyFile = "/tmp/secret.key";
+ keyFileSize = 8;
+ keyFileOffset = 2;
+ };
+ extraFormatArgs = [
+ "--iter-time 1" # insecure but fast for tests
+ ];
+ content = {
+ type = "lvm_pv";
+ vg = "pool";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ mdadm = {
+ raid1 = {
+ type = "mdadm";
+ level = 1;
+ content = {
+ type = "gpt";
+ partitions = {
+ bla = {
+ start = "1M";
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/ext4_mdadm_lvm";
+ };
+ };
+ };
+ };
+ };
+ };
+ lvm_vg = {
+ pool = {
+ type = "lvm_vg";
+ lvs = {
+ root = {
+ size = "10M";
+ lvm_type = "mirror";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/ext4_on_lvm";
+ mountOptions = [
+ "defaults"
+ ];
+ postMountHook = ''
+ touch /mnt/ext4_on_lvm/file-from-postMountHook
+ '';
+ };
+ };
+ raid1 = {
+ size = "30M";
+ lvm_type = "raid0";
+ content = {
+ type = "mdraid";
+ name = "raid1";
+ };
+ };
+ raid2 = {
+ size = "30M";
+ lvm_type = "raid0";
+ content = {
+ type = "mdraid";
+ name = "raid1";
+ };
+ };
+ zfs1 = {
+ size = "128M";
+ lvm_type = "raid0";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ zfs2 = {
+ size = "128M";
+ lvm_type = "raid0";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ zpool = {
+ zroot = {
+ type = "zpool";
+ mode = "mirror";
+ rootFsOptions = {
+ compression = "zstd";
+ "com.sun:auto-snapshot" = "false";
+ };
+ mountpoint = "/";
+
+ datasets = {
+ zfs_fs = {
+ type = "zfs_fs";
+ mountpoint = "/zfs_fs";
+ options."com.sun:auto-snapshot" = "true";
+ };
+ zfs_unmounted_fs = {
+ type = "zfs_fs";
+ options.mountpoint = "none";
+ };
+ zfs_legacy_fs = {
+ type = "zfs_fs";
+ options.mountpoint = "legacy";
+ mountpoint = "/zfs_legacy_fs";
+ };
+ zfs_testvolume = {
+ type = "zfs_volume";
+ size = "10M";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/ext4onzfs";
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/f2fs.nix b/pkgs/disko/example/f2fs.nix
new file mode 100644
index 0000000..baf8852
--- /dev/null
+++ b/pkgs/disko/example/f2fs.nix
@@ -0,0 +1,41 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/disk/by-path/pci-0000:02:00.0-nvme-1";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ end = "500M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ name = "root";
+ end = "-0";
+ content = {
+ type = "filesystem";
+ format = "f2fs";
+ mountpoint = "/";
+ extraArgs = [
+ "-O"
+ "extra_attr,inode_checksum,sb_checksum,compression"
+ ];
+ mountOptions = [
+ "compress_algorithm=zstd:6,compress_chksum,atgc,gc_merge,lazytime,nodiscard"
+ ];
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/gpt-bios-compat.nix b/pkgs/disko/example/gpt-bios-compat.nix
new file mode 100644
index 0000000..4be1942
--- /dev/null
+++ b/pkgs/disko/example/gpt-bios-compat.nix
@@ -0,0 +1,28 @@
+# Example to create a bios compatible gpt partition
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/vdb";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/gpt-name-with-whitespace.nix b/pkgs/disko/example/gpt-name-with-whitespace.nix
new file mode 100644
index 0000000..22a74b5
--- /dev/null
+++ b/pkgs/disko/example/gpt-name-with-whitespace.nix
@@ -0,0 +1,48 @@
+{
+ disko.devices = {
+ disk = {
+ vdb = {
+ device = "/dev/disk/by-id/some-disk-id";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ type = "EF00";
+ size = "100M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ };
+ };
+ "name with spaces" = {
+ size = "100M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/name with spaces";
+ };
+ };
+ "name^with\\some@special#chars" = {
+ size = "100M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/name^with\\some@special#chars";
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/gpt-unformatted.nix b/pkgs/disko/example/gpt-unformatted.nix
new file mode 100644
index 0000000..871e972
--- /dev/null
+++ b/pkgs/disko/example/gpt-unformatted.nix
@@ -0,0 +1,36 @@
+# Example to create a GPT partition but doesn't format it
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/vdb";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ type = "EF00";
+ size = "100M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ };
+ };
+ empty = {
+ size = "1G";
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/hybrid-mbr.nix b/pkgs/disko/example/hybrid-mbr.nix
new file mode 100644
index 0000000..d96c898
--- /dev/null
+++ b/pkgs/disko/example/hybrid-mbr.nix
@@ -0,0 +1,48 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ type = "disk";
+ device = "/dev/sdb";
+ content = {
+ type = "gpt";
+ efiGptPartitionFirst = false;
+ partitions = {
+ TOW-BOOT-FI = {
+ priority = 1;
+ type = "EF00";
+ size = "32M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = null;
+ };
+ hybrid = {
+ mbrPartitionType = "0x0c";
+ mbrBootableFlag = false;
+ };
+ };
+ ESP = {
+ type = "EF00";
+ size = "512M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/hybrid-tmpfs-on-root.nix b/pkgs/disko/example/hybrid-tmpfs-on-root.nix
new file mode 100644
index 0000000..55dbbf5
--- /dev/null
+++ b/pkgs/disko/example/hybrid-tmpfs-on-root.nix
@@ -0,0 +1,44 @@
+{
+ disko.devices = {
+ disk.main = {
+ device = "/dev/disk/by-id/ata-Samsung_SSD_860_EVO_500GB_S3Z1NB0K303456L";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ ESP = {
+ name = "ESP";
+ size = "512M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ nix = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/nix";
+ };
+ };
+ };
+ };
+ };
+ nodev."/" = {
+ fsType = "tmpfs";
+ mountOptions = [
+ "size=2G"
+ "defaults"
+ "mode=755"
+ ];
+ };
+ };
+}
diff --git a/pkgs/disko/example/hybrid.nix b/pkgs/disko/example/hybrid.nix
new file mode 100644
index 0000000..9825e85
--- /dev/null
+++ b/pkgs/disko/example/hybrid.nix
@@ -0,0 +1,37 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ type = "disk";
+ device = "/dev/disk/by-id/ata-Samsung_SSD_850_EVO_250GB_S21PNXAGB12345";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ ESP = {
+ size = "512M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/legacy-table-with-whitespace.nix b/pkgs/disko/example/legacy-table-with-whitespace.nix
new file mode 100644
index 0000000..5eb28ab
--- /dev/null
+++ b/pkgs/disko/example/legacy-table-with-whitespace.nix
@@ -0,0 +1,50 @@
+{
+ disko.devices = {
+ disk = {
+ vdb = {
+ device = "/dev/sda";
+ type = "disk";
+ content = {
+ type = "table";
+ format = "gpt";
+ partitions = [
+ {
+ name = "ESP";
+ start = "1MiB";
+ end = "100MiB";
+ bootable = true;
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ };
+ }
+ {
+ name = "name with spaces";
+ start = "100MiB";
+ end = "200MiB";
+ bootable = true;
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/name_with_spaces";
+ };
+ }
+ {
+ name = "root";
+ start = "200MiB";
+ end = "100%";
+ part-type = "primary";
+ bootable = true;
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ }
+ ];
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/legacy-table.nix b/pkgs/disko/example/legacy-table.nix
new file mode 100644
index 0000000..b022e8f
--- /dev/null
+++ b/pkgs/disko/example/legacy-table.nix
@@ -0,0 +1,40 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/sda";
+ type = "disk";
+ content = {
+ type = "table";
+ format = "gpt";
+ partitions = [
+ {
+ name = "ESP";
+ start = "1M";
+ end = "500M";
+ bootable = true;
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ };
+ }
+ {
+ name = "root";
+ start = "500M";
+ end = "100%";
+ part-type = "primary";
+ bootable = true;
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ }
+ ];
+ };
+ };
+ };
+ };
+}
+
diff --git a/pkgs/disko/example/long-device-name.nix b/pkgs/disko/example/long-device-name.nix
new file mode 100644
index 0000000..ab43696
--- /dev/null
+++ b/pkgs/disko/example/long-device-name.nix
@@ -0,0 +1,34 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/disk/by-id/some-disk-id";
+ name = "this-is-some-super-long-name-to-test-what-happens-when-the-name-is-too-long";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ type = "EF00";
+ size = "500M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/luks-btrfs-raid.nix b/pkgs/disko/example/luks-btrfs-raid.nix
new file mode 100644
index 0000000..13ac197
--- /dev/null
+++ b/pkgs/disko/example/luks-btrfs-raid.nix
@@ -0,0 +1,78 @@
+{
+ disko.devices = {
+ disk = {
+ # Devices will be mounted and formatted in alphabetical order, and btrfs can only mount raids
+ # when all devices are present. So we define an "empty" luks device on the first disk,
+ # and the actual btrfs raid on the second disk, and the name of these entries matters!
+ disk1 = {
+ type = "disk";
+ device = "/dev/sda";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "512M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ crypt_p1 = {
+ size = "100%";
+ content = {
+ type = "luks";
+ name = "p1"; # device-mapper name when decrypted
+ # Remove settings.keyFile if you want to use interactive password entry
+ settings = {
+ allowDiscards = true;
+ keyFile = "/tmp/secret.key";
+ };
+ };
+ };
+ };
+ };
+ };
+ disk2 = {
+ type = "disk";
+ device = "/dev/sdb";
+ content = {
+ type = "gpt";
+ partitions = {
+ crypt_p2 = {
+ size = "100%";
+ content = {
+ type = "luks";
+ name = "p2";
+ # Remove settings.keyFile if you want to use interactive password entry
+ settings = {
+ allowDiscards = true;
+ keyFile = "/tmp/secret.key"; # Same key for both devices
+ };
+ content = {
+ type = "btrfs";
+ extraArgs = [
+ "-d raid1"
+ "/dev/mapper/p1" # Use decrypted mapped device, same name as defined in disk1
+ ];
+ subvolumes = {
+ "/root" = {
+ mountpoint = "/";
+ mountOptions = [
+ "rw"
+ "relatime"
+ "ssd"
+ ];
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/luks-btrfs-subvolumes.nix b/pkgs/disko/example/luks-btrfs-subvolumes.nix
new file mode 100644
index 0000000..eb1b6c0
--- /dev/null
+++ b/pkgs/disko/example/luks-btrfs-subvolumes.nix
@@ -0,0 +1,61 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ type = "disk";
+ device = "/dev/vdb";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "512M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ luks = {
+ size = "100%";
+ content = {
+ type = "luks";
+ name = "crypted";
+ # disable settings.keyFile if you want to use interactive password entry
+ #passwordFile = "/tmp/secret.key"; # Interactive
+ settings = {
+ allowDiscards = true;
+ keyFile = "/tmp/secret.key";
+ };
+ additionalKeyFiles = [ "/tmp/additionalSecret.key" ];
+ content = {
+ type = "btrfs";
+ extraArgs = [ "-f" ];
+ subvolumes = {
+ "/root" = {
+ mountpoint = "/";
+ mountOptions = [ "compress=zstd" "noatime" ];
+ };
+ "/home" = {
+ mountpoint = "/home";
+ mountOptions = [ "compress=zstd" "noatime" ];
+ };
+ "/nix" = {
+ mountpoint = "/nix";
+ mountOptions = [ "compress=zstd" "noatime" ];
+ };
+ "/swap" = {
+ mountpoint = "/.swapvol";
+ swap.swapfile.size = "20M";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/luks-interactive-login.nix b/pkgs/disko/example/luks-interactive-login.nix
new file mode 100644
index 0000000..0aaf776
--- /dev/null
+++ b/pkgs/disko/example/luks-interactive-login.nix
@@ -0,0 +1,39 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ type = "disk";
+ device = "/dev/vdb";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ luks = {
+ size = "100%";
+ content = {
+ type = "luks";
+ name = "crypted";
+ settings.allowDiscards = true;
+ passwordFile = "/tmp/secret.key";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/luks-lvm.nix b/pkgs/disko/example/luks-lvm.nix
new file mode 100644
index 0000000..1bede94
--- /dev/null
+++ b/pkgs/disko/example/luks-lvm.nix
@@ -0,0 +1,73 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ type = "disk";
+ device = "/dev/vdb";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ luks = {
+ size = "100%";
+ content = {
+ type = "luks";
+ name = "crypted";
+ extraOpenArgs = [ ];
+ settings = {
+ # if you want to use the key for interactive login be sure there is no trailing newline
+ # for example use `echo -n "password" > /tmp/secret.key`
+ keyFile = "/tmp/secret.key";
+ allowDiscards = true;
+ };
+ additionalKeyFiles = [ "/tmp/additionalSecret.key" ];
+ content = {
+ type = "lvm_pv";
+ vg = "pool";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ lvm_vg = {
+ pool = {
+ type = "lvm_vg";
+ lvs = {
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ mountOptions = [
+ "defaults"
+ ];
+ };
+ };
+ home = {
+ size = "10M";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/home";
+ };
+ };
+ raw = {
+ size = "10M";
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/luks-on-mdadm.nix b/pkgs/disko/example/luks-on-mdadm.nix
new file mode 100644
index 0000000..6a33208
--- /dev/null
+++ b/pkgs/disko/example/luks-on-mdadm.nix
@@ -0,0 +1,58 @@
+{ lib, ... }:
+{
+ disko.devices.disk = lib.genAttrs [ "a" "b" ] (name: {
+ type = "disk";
+ device = "/dev/sd${name}";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ ESP = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "mdraid";
+ name = "boot";
+ };
+ };
+ mdadm = {
+ size = "100%";
+ content = {
+ type = "mdraid";
+ name = "raid1";
+ };
+ };
+ };
+ };
+ });
+ disko.devices.mdadm = {
+ boot = {
+ type = "mdadm";
+ level = 1;
+ metadata = "1.0";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ };
+ };
+ raid1 = {
+ type = "mdadm";
+ level = 1;
+ content = {
+ type = "luks";
+ name = "crypted";
+ settings.keyFile = "/tmp/secret.key";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+}
+
diff --git a/pkgs/disko/example/lvm-raid.nix b/pkgs/disko/example/lvm-raid.nix
new file mode 100644
index 0000000..2e36fff
--- /dev/null
+++ b/pkgs/disko/example/lvm-raid.nix
@@ -0,0 +1,94 @@
+{
+ disko.devices = {
+ disk = {
+ one = {
+ type = "disk";
+ device = "/dev/sda";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "mdraid";
+ name = "boot";
+ };
+ };
+ primary = {
+ size = "100%";
+ content = {
+ type = "lvm_pv";
+ vg = "pool";
+ };
+ };
+ };
+ };
+ };
+ two = {
+ type = "disk";
+ device = "/dev/sdb";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "mdraid";
+ name = "boot";
+ };
+ };
+ primary = {
+ size = "100%";
+ content = {
+ type = "lvm_pv";
+ vg = "pool";
+ };
+ };
+ };
+ };
+ };
+ };
+ mdadm = {
+ boot = {
+ type = "mdadm";
+ level = 1;
+ metadata = "1.0";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ };
+ };
+ };
+ lvm_vg = {
+ pool = {
+ type = "lvm_vg";
+ lvs = {
+ root = {
+ size = "100M";
+ lvm_type = "mirror";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ mountOptions = [
+ "defaults"
+ ];
+ };
+ };
+ home = {
+ size = "10M";
+ lvm_type = "raid0";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/home";
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/lvm-sizes-sort.nix b/pkgs/disko/example/lvm-sizes-sort.nix
new file mode 100644
index 0000000..1309e3b
--- /dev/null
+++ b/pkgs/disko/example/lvm-sizes-sort.nix
@@ -0,0 +1,64 @@
+{
+ disko.devices = {
+ disk = {
+ one = {
+ type = "disk";
+ device = "/dev/disk/by-id/ata-VMware_Virtual_SATA_CDRW_Drive_00000000000000000001";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ primary = {
+ size = "100%";
+ content = {
+ type = "lvm_pv";
+ vg = "pool";
+ };
+ };
+ };
+ };
+ };
+ };
+ lvm_vg = {
+ pool = {
+ type = "lvm_vg";
+ lvs = {
+ aaa = {
+ size = "1M";
+ };
+ zzz = {
+ size = "1M";
+ };
+ root = {
+ size = "100M";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ mountOptions = [
+ "defaults"
+ ];
+ };
+ };
+ home = {
+ size = "100%FREE";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/home";
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/lvm-thin.nix b/pkgs/disko/example/lvm-thin.nix
new file mode 100644
index 0000000..054f5a3
--- /dev/null
+++ b/pkgs/disko/example/lvm-thin.nix
@@ -0,0 +1,69 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ type = "disk";
+ device = "/dev/vdb";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ primary = {
+ size = "100%";
+ content = {
+ type = "lvm_pv";
+ vg = "mainpool";
+ };
+ };
+ };
+ };
+ };
+ };
+ lvm_vg = {
+ mainpool = {
+ type = "lvm_vg";
+ lvs = {
+ thinpool = {
+ size = "100M";
+ lvm_type = "thin-pool";
+ };
+ root = {
+ size = "10M";
+ lvm_type = "thinlv";
+ pool = "thinpool";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ mountOptions = [
+ "defaults"
+ ];
+ };
+ };
+ home = {
+ size = "10M";
+ lvm_type = "thinlv";
+ pool = "thinpool";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/home";
+ };
+ };
+ raw = {
+ size = "10M";
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/mdadm-raid0.nix b/pkgs/disko/example/mdadm-raid0.nix
new file mode 100644
index 0000000..dcdebc5
--- /dev/null
+++ b/pkgs/disko/example/mdadm-raid0.nix
@@ -0,0 +1,65 @@
+{
+ disko.devices = {
+ disk = {
+ disk1 = {
+ type = "disk";
+ device = "/dev/my-disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ mdadm = {
+ size = "100%";
+ content = {
+ type = "mdraid";
+ name = "raid0";
+ };
+ };
+ };
+ };
+ };
+ disk2 = {
+ type = "disk";
+ device = "/dev/my-disk2";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ mdadm = {
+ size = "100%";
+ content = {
+ type = "mdraid";
+ name = "raid0";
+ };
+ };
+ };
+ };
+ };
+ };
+ mdadm = {
+ raid0 = {
+ type = "mdadm";
+ level = 0;
+ content = {
+ type = "gpt";
+ partitions = {
+ primary = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/mdadm.nix b/pkgs/disko/example/mdadm.nix
new file mode 100644
index 0000000..b6c7883
--- /dev/null
+++ b/pkgs/disko/example/mdadm.nix
@@ -0,0 +1,65 @@
+{
+ disko.devices = {
+ disk = {
+ disk1 = {
+ type = "disk";
+ device = "/dev/my-disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ mdadm = {
+ size = "100%";
+ content = {
+ type = "mdraid";
+ name = "raid1";
+ };
+ };
+ };
+ };
+ };
+ disk2 = {
+ type = "disk";
+ device = "/dev/my-disk2";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # for grub MBR
+ };
+ mdadm = {
+ size = "100%";
+ content = {
+ type = "mdraid";
+ name = "raid1";
+ };
+ };
+ };
+ };
+ };
+ };
+ mdadm = {
+ raid1 = {
+ type = "mdadm";
+ level = 1;
+ content = {
+ type = "gpt";
+ partitions = {
+ primary = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/multi-device-no-deps.nix b/pkgs/disko/example/multi-device-no-deps.nix
new file mode 100644
index 0000000..dda6782
--- /dev/null
+++ b/pkgs/disko/example/multi-device-no-deps.nix
@@ -0,0 +1,40 @@
+{
+ disko.devices = {
+ disk = {
+ disk0 = {
+ device = "/dev/vda";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ nix = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/a";
+ };
+ };
+ };
+ };
+ };
+ disk1 = {
+ device = "/dev/vdb";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/b";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/negative-size.nix b/pkgs/disko/example/negative-size.nix
new file mode 100644
index 0000000..d4d4753
--- /dev/null
+++ b/pkgs/disko/example/negative-size.nix
@@ -0,0 +1,23 @@
+{
+ disko.devices = {
+ disk = {
+ disk0 = {
+ device = "/dev/disk/by-id/ata-disk0";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ nix = {
+ end = "-10M";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/non-root-zfs.nix b/pkgs/disko/example/non-root-zfs.nix
new file mode 100644
index 0000000..2e3f4ac
--- /dev/null
+++ b/pkgs/disko/example/non-root-zfs.nix
@@ -0,0 +1,109 @@
+{
+ disko.devices = {
+ disk = {
+ x = {
+ type = "disk";
+ device = "/dev/sdx";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "64M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ y = {
+ type = "disk";
+ device = "/dev/sdy";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "storage";
+ };
+ };
+ };
+ };
+ };
+ z = {
+ type = "disk";
+ device = "/dev/sdz";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "storage";
+ };
+ };
+ };
+ };
+ };
+ a = {
+ type = "disk";
+ device = "/dev/sda";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "storage2";
+ };
+ };
+ };
+ };
+ };
+ };
+ zpool = {
+ storage = {
+ type = "zpool";
+ mode = "mirror";
+ mountpoint = "/storage";
+
+ datasets = {
+ dataset = {
+ type = "zfs_fs";
+ mountpoint = "/storage/dataset";
+ };
+ };
+ };
+ storage2 = {
+ type = "zpool";
+ mountpoint = "/storage2";
+ rootFsOptions = {
+ canmount = "off";
+ };
+
+ datasets = {
+ dataset = {
+ type = "zfs_fs";
+ mountpoint = "/storage2/dataset";
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/simple-efi.nix b/pkgs/disko/example/simple-efi.nix
new file mode 100644
index 0000000..435596b
--- /dev/null
+++ b/pkgs/disko/example/simple-efi.nix
@@ -0,0 +1,33 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/disk/by-id/some-disk-id";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ type = "EF00";
+ size = "500M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/stand-alone/configuration.nix b/pkgs/disko/example/stand-alone/configuration.nix
new file mode 100644
index 0000000..6161cea
--- /dev/null
+++ b/pkgs/disko/example/stand-alone/configuration.nix
@@ -0,0 +1,54 @@
+{ pkgs
+, lib
+, ...
+}:
+let
+ # We just import from the repository for testing here:
+ disko = import ../../. {
+ inherit lib;
+ };
+ # In your own system use something like this:
+ #import (builtins.fetchGit {
+ # url = "https://github.com/nix-community/disko";
+ # ref = "master";
+ #}) {
+ # inherit lib;
+ #};
+ cfg.disko.devices = {
+ disk = {
+ sda = {
+ device = "/dev/sda";
+ type = "disk";
+ content = {
+ type = "table";
+ format = "msdos";
+ partitions = [
+ {
+ name = "root";
+ part-type = "primary";
+ start = "1M";
+ end = "100%";
+ bootable = true;
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ }
+ ];
+ };
+ };
+ };
+ };
+in
+{
+ imports = [
+ (disko.config cfg)
+ ];
+ boot.loader.grub.devices = [ "/dev/sda" ];
+ system.stateVersion = "22.05";
+ environment.systemPackages = with pkgs; [
+ (pkgs.writeScriptBin "tsp-create" (disko.create cfg))
+ (pkgs.writeScriptBin "tsp-mount" (disko.mount cfg))
+ ];
+}
diff --git a/pkgs/disko/example/swap.nix b/pkgs/disko/example/swap.nix
new file mode 100644
index 0000000..b5a61b7
--- /dev/null
+++ b/pkgs/disko/example/swap.nix
@@ -0,0 +1,49 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/vdb";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ end = "-1G";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ encryptedSwap = {
+ size = "10M";
+ content = {
+ type = "swap";
+ randomEncryption = true;
+ priority = 100; # prefer to encrypt as long as we have space for it
+ };
+ };
+ plainSwap = {
+ size = "100%";
+ content = {
+ type = "swap";
+ discardPolicy = "both";
+ resumeDevice = true; # resume from hiberation from this device
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/tmpfs.nix b/pkgs/disko/example/tmpfs.nix
new file mode 100644
index 0000000..4f0c3a3
--- /dev/null
+++ b/pkgs/disko/example/tmpfs.nix
@@ -0,0 +1,41 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/vdb";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ };
+ nodev = {
+ "/tmp" = {
+ fsType = "tmpfs";
+ mountOptions = [
+ "size=200M"
+ ];
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/with-lib.nix b/pkgs/disko/example/with-lib.nix
new file mode 100644
index 0000000..5558fac
--- /dev/null
+++ b/pkgs/disko/example/with-lib.nix
@@ -0,0 +1,27 @@
+# Example to create a bios compatible gpt partition
+{ disks ? [ "/dev/vdb" ], lib, ... }: {
+ disko.devices = {
+ disk = lib.genAttrs disks (device: {
+ name = lib.replaceStrings [ "/" ] [ "_" ] device;
+ device = device;
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02";
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ });
+ };
+}
diff --git a/pkgs/disko/example/xfs-with-quota.nix b/pkgs/disko/example/xfs-with-quota.nix
new file mode 100644
index 0000000..f4b0fab
--- /dev/null
+++ b/pkgs/disko/example/xfs-with-quota.nix
@@ -0,0 +1,34 @@
+{
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/disk/by-id/some-disk-id";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ type = "EF00";
+ size = "500M";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "xfs";
+ mountpoint = "/";
+ mountOptions = [ "defaults" "pquota" ];
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/zfs-over-legacy.nix b/pkgs/disko/example/zfs-over-legacy.nix
new file mode 100644
index 0000000..273975b
--- /dev/null
+++ b/pkgs/disko/example/zfs-over-legacy.nix
@@ -0,0 +1,58 @@
+# systemd will mount an ext4 filesystem at / and zfs will mount the dataset underneath it
+{
+ disko.devices = {
+ disk = {
+ disk1 = {
+ type = "disk";
+ device = "/dev/vdb";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "500M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ primary = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ };
+ };
+ };
+ };
+ };
+ disk2 = {
+ type = "disk";
+ device = "/dev/vdc";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ zpool = {
+ zroot = {
+ type = "zpool";
+ datasets = {
+ "root" = {
+ type = "zfs_fs";
+ options.mountpoint = "none";
+ };
+ "root/zfs_fs" = {
+ type = "zfs_fs";
+ mountpoint = "/zfs_fs";
+ options."com.sun:auto-snapshot" = "true";
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/zfs-with-vdevs.nix b/pkgs/disko/example/zfs-with-vdevs.nix
new file mode 100644
index 0000000..effe53e
--- /dev/null
+++ b/pkgs/disko/example/zfs-with-vdevs.nix
@@ -0,0 +1,305 @@
+{
+ disko.devices = {
+ disk = {
+ data1 = {
+ type = "disk";
+ device = "/dev/vda";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "64M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ data2 = {
+ type = "disk";
+ device = "/dev/vdb";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ data3 = {
+ type = "disk";
+ device = "/dev/vdc";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ spare = {
+ type = "disk";
+ device = "/dev/vdd";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ log1 = {
+ type = "disk";
+ device = "/dev/vde";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ log2 = {
+ type = "disk";
+ device = "/dev/vdf";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ log3 = {
+ type = "disk";
+ device = "/dev/vdg";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ dedup1 = {
+ type = "disk";
+ device = "/dev/vdh";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ dedup2 = {
+ type = "disk";
+ device = "/dev/vdi";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ dedup3 = {
+ type = "disk";
+ device = "/dev/vdj";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ special1 = {
+ type = "disk";
+ device = "/dev/vdk";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ special2 = {
+ type = "disk";
+ device = "/dev/vdl";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ special3 = {
+ type = "disk";
+ device = "/dev/vdm";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ cache = {
+ type = "disk";
+ device = "/dev/vdn";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ };
+ zpool = {
+ zroot = {
+ type = "zpool";
+ mode = {
+ topology = {
+ type = "topology";
+ vdev = [
+ {
+ # This syntax expects a disk called 'data3' with a gpt partition called 'zfs'.
+ members = [ "data1" ];
+ # It's also possible to use the full path of the device or partition
+ # members = [ "/dev/disk/by-id/wwn-0x5000c500af8b2a14" ];
+ }
+ {
+ mode = "mirror";
+ members = [ "data2" "data3" ];
+ }
+ ];
+ spare = [ "spare" ];
+ log = [
+ {
+ mode = "mirror";
+ members = [ "log1" "log2" ];
+ }
+ {
+ members = [ "log3" ];
+ }
+ ];
+ dedup = [
+ {
+ mode = "mirror";
+ members = [ "dedup1" "dedup2" ];
+ }
+ {
+ members = [ "dedup3" ];
+ }
+ ];
+ special = [
+ {
+ mode = "mirror";
+ members = [ "special1" "special2" ];
+ }
+ {
+ members = [ "special3" ];
+ }
+ ];
+ cache = [ "cache" ];
+ };
+ };
+
+ rootFsOptions = {
+ compression = "zstd";
+ "com.sun:auto-snapshot" = "false";
+ };
+ mountpoint = "/";
+ datasets = {
+ # See examples/zfs.nix for more comprehensive usage.
+ zfs_fs = {
+ type = "zfs_fs";
+ mountpoint = "/zfs_fs";
+ options."com.sun:auto-snapshot" = "true";
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/example/zfs.nix b/pkgs/disko/example/zfs.nix
new file mode 100644
index 0000000..5ec99b4
--- /dev/null
+++ b/pkgs/disko/example/zfs.nix
@@ -0,0 +1,119 @@
+{
+ disko.devices = {
+ disk = {
+ x = {
+ type = "disk";
+ device = "/dev/sdx";
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "64M";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ y = {
+ type = "disk";
+ device = "/dev/sdy";
+ content = {
+ type = "gpt";
+ partitions = {
+ zfs = {
+ size = "100%";
+ content = {
+ type = "zfs";
+ pool = "zroot";
+ };
+ };
+ };
+ };
+ };
+ };
+ zpool = {
+ zroot = {
+ type = "zpool";
+ mode = "mirror";
+ # Workaround: cannot import 'zroot': I/O error in disko tests
+ options.cachefile = "none";
+ rootFsOptions = {
+ compression = "zstd";
+ "com.sun:auto-snapshot" = "false";
+ };
+ mountpoint = "/";
+ postCreateHook = "zfs list -t snapshot -H -o name | grep -E '^zroot@blank$' || zfs snapshot zroot@blank";
+
+ datasets = {
+ zfs_fs = {
+ type = "zfs_fs";
+ mountpoint = "/zfs_fs";
+ options."com.sun:auto-snapshot" = "true";
+ };
+ zfs_unmounted_fs = {
+ type = "zfs_fs";
+ options.mountpoint = "none";
+ };
+ zfs_legacy_fs = {
+ type = "zfs_fs";
+ options.mountpoint = "legacy";
+ mountpoint = "/zfs_legacy_fs";
+ };
+ zfs_volume = {
+ type = "zfs_volume";
+ size = "10M";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/ext4onzfs";
+ };
+ };
+ zfs_encryptedvolume = {
+ type = "zfs_volume";
+ size = "10M";
+ options = {
+ encryption = "aes-256-gcm";
+ keyformat = "passphrase";
+ keylocation = "file:///tmp/secret.key";
+ };
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/ext4onzfsencrypted";
+ };
+ };
+ encrypted = {
+ type = "zfs_fs";
+ options = {
+ mountpoint = "none";
+ encryption = "aes-256-gcm";
+ keyformat = "passphrase";
+ keylocation = "file:///tmp/secret.key";
+ };
+ # use this to read the key during boot
+ # postCreateHook = ''
+ # zfs set keylocation="prompt" "zroot/$name";
+ # '';
+ };
+ "encrypted/test" = {
+ type = "zfs_fs";
+ mountpoint = "/zfs_crypted";
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/pkgs/disko/flake.lock b/pkgs/disko/flake.lock
new file mode 100644
index 0000000..ca4a6b9
--- /dev/null
+++ b/pkgs/disko/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1734435836,
+ "narHash": "sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "4989a246d7a390a859852baddb1013f825435cee",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/pkgs/disko/flake.nix b/pkgs/disko/flake.nix
new file mode 100644
index 0000000..da3bfcb
--- /dev/null
+++ b/pkgs/disko/flake.nix
@@ -0,0 +1,155 @@
+{
+ description = "Disko - declarative disk partitioning";
+
+ # FIXME: in future we don't want lock here to give precedence to a USB live-installer's registry,
+ # but garnix currently does not allow this.
+ #inputs.nixpkgs.url = "nixpkgs";
+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+
+ outputs = { self, nixpkgs, ... }:
+ let
+ lib = nixpkgs.lib;
+ supportedSystems = [
+ "x86_64-linux"
+ "i686-linux"
+ "aarch64-linux"
+ "riscv64-linux"
+ ];
+ forAllSystems = lib.genAttrs supportedSystems;
+
+ versionInfo = import ./version.nix;
+ version = versionInfo.version + (lib.optionalString (!versionInfo.released) "-dirty");
+
+ diskoLib = import ./lib {
+ inherit (nixpkgs) lib;
+ };
+ in
+ {
+ nixosModules.default = self.nixosModules.disko; # convention
+ nixosModules.disko.imports = [ ./module.nix ];
+ lib = diskoLib;
+ packages = forAllSystems (system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in
+ {
+ disko = pkgs.callPackage ./package.nix { diskoVersion = version; };
+ # alias to make `nix run` more convenient
+ disko-install = self.packages.${system}.disko.overrideAttrs (_old: {
+ name = "disko-install";
+ });
+ default = self.packages.${system}.disko;
+
+ create-release = pkgs.callPackage ./scripts/create-release.nix { };
+ } // pkgs.lib.optionalAttrs (!pkgs.stdenv.buildPlatform.isRiscV64) {
+ disko-doc = pkgs.callPackage ./doc.nix { };
+ });
+ # TODO: disable bios-related tests on aarch64...
+ # Run checks: nix flake check -L
+ checks = forAllSystems (system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ # FIXME: aarch64-linux seems to hang on boot
+ nixosTests = lib.optionalAttrs pkgs.stdenv.hostPlatform.isx86_64 (import ./tests {
+ inherit pkgs;
+ makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix");
+ eval-config = import (pkgs.path + "/nixos/lib/eval-config.nix");
+ });
+
+ disko-install = pkgs.callPackage ./tests/disko-install {
+ inherit self;
+ diskoVersion = version;
+ };
+
+ shellcheck = pkgs.runCommand "shellcheck" { nativeBuildInputs = [ pkgs.shellcheck ]; } ''
+ cd ${./.}
+ shellcheck disk-deactivate/disk-deactivate disko
+ touch $out
+ '';
+
+ jsonTypes = pkgs.writeTextFile { name = "jsonTypes"; text = (builtins.toJSON diskoLib.jsonTypes); };
+ in
+ # FIXME: aarch64-linux seems to hang on boot
+ lib.optionalAttrs pkgs.stdenv.hostPlatform.isx86_64 (nixosTests // { inherit disko-install; }) //
+ pkgs.lib.optionalAttrs (!pkgs.stdenv.buildPlatform.isRiscV64 && !pkgs.stdenv.hostPlatform.isx86_32) {
+ inherit shellcheck jsonTypes;
+ inherit (self.packages.${system}) disko-doc;
+ });
+
+ nixosConfigurations.testmachine = lib.nixosSystem {
+ system = "x86_64-linux";
+ modules = [
+ ./tests/disko-install/configuration.nix
+ ./example/hybrid.nix
+ ./module.nix
+ ];
+ };
+ formatter = forAllSystems (system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in
+ pkgs.writeShellApplication {
+ name = "format";
+ runtimeInputs = with pkgs; [
+ nixpkgs-fmt
+ deno
+ deadnix
+ ];
+ text = ''
+ showUsage() {
+ cat <
+, rootMountPoint ? "/mnt"
+, makeTest ? import
+, eval-config ? import
+}:
+let
+ outputs = import ../default.nix { inherit lib diskoLib; };
+ diskoLib = {
+ testLib = import ./tests.nix { inherit lib makeTest eval-config; };
+ # like lib.types.oneOf but instead of a list takes an attrset
+ # uses the field "type" to find the correct type in the attrset
+ subType = { types, extraArgs ? { parent = { type = "rootNode"; name = "root"; }; } }: lib.mkOptionType {
+ name = "subType";
+ description = "one of ${lib.concatStringsSep "," (lib.attrNames types)}";
+ check = x: if x ? type then types.${x.type}.check x else throw "No type option set in:\n${lib.generators.toPretty {} x}";
+ merge = loc: lib.foldl'
+ (_res: def: types.${def.value.type}.merge loc [
+ # we add a dummy root parent node to render documentation
+ (lib.recursiveUpdate { value._module.args = extraArgs; } def)
+ ])
+ { };
+ nestedTypes = types;
+ };
+
+ # option for valid contents of partitions (basically like devices, but without tables)
+ _partitionTypes = { inherit (diskoLib.types) btrfs filesystem zfs mdraid luks lvm_pv swap; };
+ partitionType = extraArgs: lib.mkOption {
+ type = lib.types.nullOr (diskoLib.subType {
+ types = diskoLib._partitionTypes;
+ inherit extraArgs;
+ });
+ default = null;
+ description = "The type of partition";
+ };
+
+ # option for valid contents of devices
+ _deviceTypes = { inherit (diskoLib.types) table gpt btrfs filesystem zfs mdraid luks lvm_pv swap; };
+ deviceType = extraArgs: lib.mkOption {
+ type = lib.types.nullOr (diskoLib.subType {
+ types = diskoLib._deviceTypes;
+ inherit extraArgs;
+ });
+ default = null;
+ description = "The type of device";
+ };
+
+ /* deepMergeMap takes a function and a list of attrsets and deep merges them
+
+ deepMergeMap :: (AttrSet -> AttrSet ) -> [ AttrSet ] -> Attrset
+
+ Example:
+ deepMergeMap (x: x.t = "test") [ { x = { y = 1; z = 3; }; } { x = { bla = 234; }; } ]
+ => { x = { y = 1; z = 3; bla = 234; t = "test"; }; }
+ */
+ deepMergeMap = f: lib.foldr (attr: acc: (lib.recursiveUpdate acc (f attr))) { };
+
+ /* get a device and an index to get the matching device name
+
+ deviceNumbering :: str -> int -> str
+
+ Example:
+ deviceNumbering "/dev/sda" 3
+ => "/dev/sda3"
+
+ deviceNumbering "/dev/disk/by-id/xxx" 2
+ => "/dev/disk/by-id/xxx-part2"
+ */
+ deviceNumbering = dev: index:
+ let inherit (lib) match; in
+ if match "/dev/([vs]|(xv)d).+" dev != null then
+ dev + toString index # /dev/{s,v,xv}da style
+ else if match "/dev/(disk|zvol)/.+" dev != null then
+ "${dev}-part${toString index}" # /dev/disk/by-id/xxx style, also used by zfs's zvolumes
+ else if match "/dev/((nvme|mmcblk).+|md/.*[[:digit:]])" dev != null then
+ "${dev}p${toString index}" # /dev/nvme0n1p1 style
+ else if match "/dev/md/.+" dev != null then
+ "${dev}${toString index}" # /dev/md/raid1 style
+ else if match "/dev/mapper/.+" dev != null then
+ "${dev}${toString index}" # /dev/mapper/vg-lv1 style
+ else if match "/dev/loop[[:digit:]]+" dev != null
+ then "${dev}p${toString index}" # /dev/mapper/vg-lv1 style
+ else
+ abort ''
+ ${dev} seems not to be a supported disk format. Please add this to disko in https://github.com/nix-community/disko/blob/master/lib/default.nix
+ '';
+
+ /* Escape a string as required to be used in udev symlinks
+
+ The allowed characters are "0-9A-Za-z#+-.:=@_/", valid UTF-8 character sequences, and "\x00" hex encoding.
+ Everything else is escaped as "\xXX" where XX is the hex value of the character.
+
+ The source of truth for the list of allowed characters is the udev documentation:
+ https://www.freedesktop.org/software/systemd/man/latest/udev.html#SYMLINK1
+
+ This function is implemented as a best effort. It is not guaranteed to be 100% in line
+ with the udev implementation, and we hope that you're not crazy enough to try to break it.
+
+ hexEscapeUdevSymlink :: str -> str
+
+ Example:
+ hexEscapeUdevSymlink "Boot data partition"
+ => "Boot\x20data\x20partition"
+
+ hexEscapeUdevSymlink "Even(crazier)par&titi^onName"
+ => "Even\x28crazier\x29par\x26titi\x5EonName"
+
+ hexEscapeUdevSymlink "all0these@char#acters+_are-allow.ed"
+ => "all0these@char#acters+_are-allow.ed"
+ */
+ hexEscapeUdevSymlink =
+ let
+ allowedChars = "[0-9A-Za-z#+-.:=@_/]";
+ charToHex = c: lib.toHexString (lib.strings.charToInt c);
+ in
+ lib.stringAsChars
+ (c: if lib.match allowedChars c != null || c == "" then c else "\\x" + charToHex c);
+
+ /* get the index an item in a list
+
+ indexOf :: (a -> bool) -> [a] -> int -> int
+
+ Example:
+ indexOf (x: x == 2) [ 1 2 3 ] 0
+ => 2
+
+ indexOf (x: x == "x") [ 1 2 3 ] 0
+ => 0
+ */
+ indexOf = f: list: fallback:
+ let
+ iter = index: list:
+ if list == [ ] then
+ fallback
+ else if f (lib.head list) then
+ index
+ else
+ iter (index + 1) (lib.tail list);
+ in
+ iter 1 list;
+
+
+ /* indent takes a multiline string and indents it by 2 spaces starting on the second line
+
+ indent :: str -> str
+
+ Example:
+ indent "test\nbla"
+ => "test\n bla"
+ */
+ indent = lib.replaceStrings [ "\n" ] [ "\n " ];
+
+ /* A nix option type representing a json datastructure, vendored from nixpkgs to avoid dependency on pkgs */
+ jsonType =
+ let
+ valueType = lib.types.nullOr
+ (lib.types.oneOf [
+ lib.types.bool
+ lib.types.int
+ lib.types.float
+ lib.types.str
+ lib.types.path
+ (lib.types.attrsOf valueType)
+ (lib.types.listOf valueType)
+ ]) // {
+ description = "JSON value";
+ };
+ in
+ valueType;
+
+ /* Given a attrset of deviceDependencies and a devices attrset
+ returns a sorted list by deviceDependencies. aborts if a loop is found
+
+ sortDevicesByDependencies :: AttrSet -> AttrSet -> [ [ str str ] ]
+ */
+ sortDevicesByDependencies = deviceDependencies: devices:
+ let
+ dependsOn = a: b:
+ lib.elem a (lib.attrByPath b [ ] deviceDependencies);
+ maybeSortedDevices = lib.toposort dependsOn (diskoLib.deviceList devices);
+ in
+ if (lib.hasAttr "cycle" maybeSortedDevices) then
+ abort "detected a cycle in your disk setup: ${maybeSortedDevices.cycle}"
+ else
+ maybeSortedDevices.result;
+
+ /* Takes a devices attrSet and returns it as a list
+
+ deviceList :: AttrSet -> [ [ str str ] ]
+
+ Example:
+ deviceList { zfs.pool1 = {}; zfs.pool2 = {}; mdadm.raid1 = {}; }
+ => [ [ "zfs" "pool1" ] [ "zfs" "pool2" ] [ "mdadm" "raid1" ] ]
+ */
+ deviceList = devices:
+ lib.concatLists (lib.mapAttrsToList (n: v: (map (x: [ n x ]) (lib.attrNames v))) devices);
+
+ /* Takes either a string or null and returns the string or an empty string
+
+ maybeStr :: Either (str null) -> str
+
+ Example:
+ maybeStr null
+ => ""
+ maybeSTr "hello world"
+ => "hello world"
+ */
+ maybeStr = x: lib.optionalString (x != null) x;
+
+ /* Takes a Submodules config and options argument and returns a serializable
+ subset of config variables as a shell script snippet.
+ */
+ defineHookVariables = { options }:
+ let
+ sanitizeName = lib.replaceStrings [ "-" ] [ "_" ];
+ isAttrsOfSubmodule = o: o.type.name == "attrsOf" && o.type.nestedTypes.elemType.name == "submodule";
+ isSerializable = n: o: !(
+ lib.hasPrefix "_" n
+ || lib.hasSuffix "Hook" n
+ || isAttrsOfSubmodule o
+ # TODO don't hardcode diskoLib.subType options.
+ || n == "content" || n == "partitions" || n == "datasets" || n == "swap"
+ || n == "mode"
+ );
+ in
+ lib.toShellVars
+ (lib.mapAttrs'
+ (n: o: lib.nameValuePair (sanitizeName n) o.value)
+ (lib.filterAttrs isSerializable options));
+
+ mkHook = description: lib.mkOption {
+ inherit description;
+ type = lib.types.lines;
+ default = "";
+ };
+
+ mkSubType = module: lib.types.submodule [
+ module
+
+ {
+ options = {
+ preCreateHook = diskoLib.mkHook "shell commands to run before create";
+ postCreateHook = diskoLib.mkHook "shell commands to run after create";
+ preMountHook = diskoLib.mkHook "shell commands to run before mount";
+ postMountHook = diskoLib.mkHook "shell commands to run after mount";
+ preUnmountHook = diskoLib.mkHook "shell commands to run before unmount";
+ postUnmountHook = diskoLib.mkHook "shell commands to run after unmount";
+ };
+ config._module.args = {
+ inherit diskoLib rootMountPoint;
+ };
+ }
+ ];
+
+ mkCreateOption = { config, options, default }@attrs:
+ lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.str;
+ default = ''
+ ( # ${config.type} ${lib.concatMapStringsSep " " (n: toString (config.${n} or "")) ["name" "device" "format" "mountpoint"]} #
+ ${diskoLib.indent (diskoLib.defineHookVariables { inherit options; })}
+ ${diskoLib.indent config.preCreateHook}
+ ${diskoLib.indent attrs.default}
+ ${diskoLib.indent config.postCreateHook}
+ )
+ '';
+ description = "Creation script";
+ };
+
+ mkMountOption = { config, options, default }@attrs:
+ lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = diskoLib.jsonType;
+ default = lib.mapAttrsRecursive
+ (_name: value:
+ if builtins.isString value then ''
+ (
+ ${diskoLib.indent (diskoLib.defineHookVariables { inherit options; })}
+ ${diskoLib.indent config.preMountHook}
+ ${diskoLib.indent value}
+ ${diskoLib.indent config.postMountHook}
+ )
+ '' else value)
+ attrs.default;
+ description = "Mount script";
+ };
+
+ mkUnmountOption = { config, options, default }@attrs:
+ lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = diskoLib.jsonType;
+ default = lib.mapAttrsRecursive
+ (_name: value:
+ if builtins.isString value then ''
+ (
+ ${diskoLib.indent (diskoLib.defineHookVariables { inherit options; })}
+ ${diskoLib.indent config.preUnmountHook}
+ ${diskoLib.indent value}
+ ${diskoLib.indent config.postUnmountHook}
+ )
+ '' else value)
+ attrs.default;
+ description = "Unmount script";
+ };
+
+ /* Writer for optionally checking bash scripts before writing them to the store
+
+ writeCheckedBash :: AttrSet -> str -> str -> derivation
+ */
+ writeCheckedBash = { pkgs, checked ? false, noDeps ? false }: pkgs.writers.makeScriptWriter {
+ interpreter = if noDeps then "/usr/bin/env bash" else "${pkgs.bash}/bin/bash";
+ check = lib.optionalString (checked && !pkgs.stdenv.hostPlatform.isRiscV64 && !pkgs.stdenv.hostPlatform.isx86_32) (pkgs.writeScript "check" ''
+ set -efu
+ # SC2054: our toShellVars function doesn't quote list elements with commas
+ # SC2034: We don't use all variables exported by hooks.
+ ${pkgs.shellcheck}/bin/shellcheck -e SC2034,SC2054 "$1"
+ '');
+ };
+
+
+ /* Takes a disko device specification, returns an attrset with metadata
+
+ meta :: lib.types.devices -> AttrSet
+ */
+ meta = toplevel: toplevel._meta;
+
+ /* Takes a disko device specification and returns a string which formats the disks
+
+ create :: lib.types.devices -> str
+ */
+ create = toplevel: toplevel._create;
+ /* Takes a disko device specification and returns a string which mounts the disks
+
+ mount :: lib.types.devices -> str
+ */
+ mount = toplevel: toplevel._mount;
+
+ /* takes a disko device specification and returns a string which unmounts, destroys all disks and then runs create and mount
+
+ zapCreateMount :: lib.types.devices -> str
+ */
+ zapCreateMount = toplevel:
+ ''
+ set -efux
+ ${toplevel._disko}
+ '';
+ /* Takes a disko device specification and returns a nixos configuration
+
+ config :: lib.types.devices -> nixosConfig
+ */
+ config = toplevel: toplevel._config;
+
+ /* Takes a disko device specification and returns a function to get the needed packages to format/mount the disks
+
+ packages :: lib.types.devices -> pkgs -> [ derivation ]
+ */
+ packages = toplevel: toplevel._packages;
+
+ /* Checks whether nixpkgs is recent enough for vmTools to support the customQemu argument.
+
+ Returns false, which is technically incorrect, for a few commits on 2024-07-08, but we can't be more accurate.
+ Make sure to pass lib, not pkgs.lib! See https://github.com/nix-community/disko/issues/904
+
+ vmToolsSupportsCustomQemu :: final_lib -> bool
+ */
+ vmToolsSupportsCustomQemu = final_lib: lib.versionAtLeast final_lib.version "24.11.20240709";
+
+ optionTypes = rec {
+ filename = lib.mkOptionType {
+ name = "filename";
+ check = lib.isString;
+ merge = lib.mergeOneOption;
+ description = "A filename";
+ };
+
+ absolute-pathname = lib.mkOptionType {
+ name = "absolute pathname";
+ check = x: lib.isString x && lib.substring 0 1 x == "/" && pathname.check x;
+ merge = lib.mergeOneOption;
+ description = "An absolute path";
+ };
+
+ pathname = lib.mkOptionType {
+ name = "pathname";
+ check = x:
+ with lib; let
+ # The filter is used to normalize paths, i.e. to remove duplicated and
+ # trailing slashes. It also removes leading slashes, thus we have to
+ # check for "/" explicitly below.
+ xs = filter (s: stringLength s > 0) (splitString "/" x);
+ in
+ isString x && (x == "/" || (length xs > 0 && all filename.check xs));
+ merge = lib.mergeOneOption;
+ description = "A path name";
+ };
+ };
+
+ /* topLevel type of the disko config, takes attrsets of disks, mdadms, zpools, nodevs, and lvm vgs.
+ */
+ toplevel = lib.types.submodule (cfg:
+ let
+ devices = { inherit (cfg.config) disk mdadm zpool lvm_vg nodev; };
+ in
+ {
+ options = {
+ disk = lib.mkOption {
+ type = lib.types.attrsOf diskoLib.types.disk;
+ default = { };
+ description = "Block device";
+ };
+ mdadm = lib.mkOption {
+ type = lib.types.attrsOf diskoLib.types.mdadm;
+ default = { };
+ description = "mdadm device";
+ };
+ zpool = lib.mkOption {
+ type = lib.types.attrsOf diskoLib.types.zpool;
+ default = { };
+ description = "ZFS pool device";
+ };
+ lvm_vg = lib.mkOption {
+ type = lib.types.attrsOf diskoLib.types.lvm_vg;
+ default = { };
+ description = "LVM VG device";
+ };
+ nodev = lib.mkOption {
+ type = lib.types.attrsOf diskoLib.types.nodev;
+ default = { };
+ description = "A non-block device";
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ description = ''
+ meta informationen generated by disko
+ currently used for building a dependency list so we know in which order to create the devices
+ '';
+ default = diskoLib.deepMergeMap (dev: dev._meta) (lib.flatten (map lib.attrValues (lib.attrValues devices)));
+ };
+ _packages = lib.mkOption {
+ internal = true;
+ description = ''
+ packages required by the disko configuration
+ coreutils is always included
+ '';
+ default = pkgs: with lib; unique ((flatten (map (dev: dev._pkgs pkgs) (flatten (map attrValues (attrValues devices))))) ++ [ pkgs.coreutils-full ]);
+ };
+ _scripts = lib.mkOption {
+ internal = true;
+ description = ''
+ The scripts generated by disko
+ '';
+ default = { pkgs, checked ? false }:
+ let
+ throwIfNoDisksDetected = _: v: if devices.disk == { } then throw "No disks defined, did you forget to import your disko config?" else v;
+ destroyDependencies = with pkgs; [
+ util-linux
+ e2fsprogs
+ mdadm
+ zfs
+ lvm2
+ bash
+ jq
+ gnused
+ gawk
+ coreutils-full
+ ];
+ in
+ lib.mapAttrs throwIfNoDisksDetected {
+ destroy = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-destroy" ''
+ export PATH=${lib.makeBinPath destroyDependencies}:$PATH
+ ${cfg.config._destroy}
+ '';
+ format = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-format" ''
+ export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
+ ${cfg.config._create}
+ '';
+ mount = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-mount" ''
+ export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
+ ${cfg.config._mount}
+ '';
+ unmount = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-unmount" ''
+ export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
+ ${cfg.config._unmount}
+ '';
+ formatMount = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-format-mount" ''
+ export PATH=${lib.makeBinPath ((cfg.config._packages pkgs) ++ [ pkgs.bash ])}:$PATH
+ ${cfg.config._formatMount}
+ '';
+ destroyFormatMount = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-destroy-format-mount" ''
+ export PATH=${lib.makeBinPath ((cfg.config._packages pkgs) ++ [ pkgs.bash ] ++ destroyDependencies)}:$PATH
+ ${cfg.config._destroyFormatMount}
+ '';
+
+ # These are useful to skip copying executables uploading a script to an in-memory installer
+ destroyNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-destroy" ''
+ ${cfg.config._destroy}
+ '';
+ formatNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-format" ''
+ ${cfg.config._create}
+ '';
+ mountNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-mount" ''
+ ${cfg.config._mount}
+ '';
+ unmountNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-unmount" ''
+ ${cfg.config._unmount}
+ '';
+ formatMountNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-format-mount" ''
+ ${cfg.config._formatMount}
+ '';
+ destroyFormatMountNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-destroy-format-mount" ''
+ ${cfg.config._destroyFormatMount}
+ '';
+
+
+ # Legacy scripts, to be removed in version 2.0.0
+ # They are generally less useful, because the scripts are directly written to their $out path instead of
+ # into the $out/bin directory, which makes them incompatible with `nix run`
+ # (see https://github.com/nix-community/disko/pull/78), `lib.buildEnv` and thus `environment.systemPackages`,
+ # `user.users..packages` and `home.packages`, see https://github.com/nix-community/disko/issues/454
+ destroyScript = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "disko-destroy" ''
+ export PATH=${lib.makeBinPath destroyDependencies}:$PATH
+ ${cfg.config._legacyDestroy}
+ '';
+
+ formatScript = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "disko-format" ''
+ export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
+ ${cfg.config._create}
+ '';
+
+ mountScript = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "disko-mount" ''
+ export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
+ ${cfg.config._mount}
+ '';
+
+ diskoScript = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "disko" ''
+ export PATH=${lib.makeBinPath ((cfg.config._packages pkgs) ++ [ pkgs.bash ] ++ destroyDependencies)}:$PATH
+ ${cfg.config._disko}
+ '';
+
+ # These are useful to skip copying executables uploading a script to an in-memory installer
+ destroyScriptNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "disko-destroy" ''
+ ${cfg.config._legacyDestroy}
+ '';
+
+ formatScriptNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "disko-format" ''
+ ${cfg.config._create}
+ '';
+
+ mountScriptNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "disko-mount" ''
+ ${cfg.config._mount}
+ '';
+
+ diskoScriptNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "disko" ''
+ ${cfg.config._disko}
+ '';
+ };
+ };
+ _legacyDestroy = lib.mkOption {
+ internal = true;
+ type = lib.types.str;
+ description = ''
+ The script to unmount (& destroy) all devices defined by disko.devices
+ Does not ask for confirmation! Depracated in favor of _destroy
+ '';
+ default = ''
+ umount -Rv "${rootMountPoint}" || :
+
+ # shellcheck disable=SC2043
+ for dev in ${toString (lib.catAttrs "device" (lib.attrValues devices.disk))}; do
+ $BASH ${../disk-deactivate}/disk-deactivate "$dev"
+ done
+ '';
+ };
+ _destroy = lib.mkOption {
+ internal = true;
+ type = lib.types.str;
+ description = ''
+ The script to unmount (& destroy) all devices defined by disko.devices
+ '';
+ default =
+ let
+ selectedDisks = lib.escapeShellArgs (lib.catAttrs "device" (lib.attrValues devices.disk));
+ in
+ ''
+ if [ "$1" != "--yes-wipe-all-disks" ]; then
+ echo "WARNING: This will destroy all data on the disks defined in disko.devices, which are:"
+ echo
+ # shellcheck disable=SC2043,2041
+ for dev in ${selectedDisks}; do
+ echo " - $dev"
+ done
+ echo
+ echo " (If you want to skip this dialogue, pass --yes-wipe-all-disks)"
+ echo
+ echo "Are you sure you want to wipe the devices listed above?"
+ read -rp "Type 'yes' to continue, anything else to abort: " confirmation
+
+ if [ "$confirmation" != "yes" ]; then
+ echo "Aborted."
+ exit 1
+ fi
+ fi
+
+ umount -Rv "${rootMountPoint}" || :
+
+ # shellcheck disable=SC2043
+ for dev in ${selectedDisks}; do
+ $BASH ${../disk-deactivate}/disk-deactivate "$dev"
+ done
+ '';
+ };
+ _create = lib.mkOption {
+ internal = true;
+ type = lib.types.str;
+ description = ''
+ The script to create all devices defined by disko.devices
+ '';
+ default =
+ with lib; let
+ sortedDeviceList = diskoLib.sortDevicesByDependencies (cfg.config._meta.deviceDependencies or { }) devices;
+ in
+ ''
+ set -efux
+
+ disko_devices_dir=$(mktemp -d)
+ trap 'rm -rf "$disko_devices_dir"' EXIT
+ mkdir -p "$disko_devices_dir"
+
+ ${concatMapStrings (dev: (attrByPath (dev ++ [ "_create" ]) {} devices)) sortedDeviceList}
+ '';
+ };
+ _mount = lib.mkOption {
+ internal = true;
+ type = lib.types.str;
+ description = ''
+ The script to mount all devices defined by disko.devices
+ '';
+ default =
+ with lib; let
+ fsMounts = diskoLib.deepMergeMap (dev: dev._mount.fs or { }) (flatten (map attrValues (attrValues devices)));
+ sortedDeviceList = diskoLib.sortDevicesByDependencies (cfg.config._meta.deviceDependencies or { }) devices;
+ in
+ ''
+ set -efux
+ # first create the necessary devices
+ ${concatMapStrings (dev: (attrByPath (dev ++ [ "_mount" ]) {} devices).dev or "") sortedDeviceList}
+
+ # and then mount the filesystems in alphabetical order
+ ${concatStrings (attrValues fsMounts)}
+ '';
+ };
+ _unmount = lib.mkOption {
+ internal = true;
+ type = lib.types.str;
+ description = ''
+ The script to unmount all devices defined by disko.devices
+ '';
+ default =
+ with lib; let
+ fsMounts = diskoLib.deepMergeMap (dev: dev._unmount.fs or { }) (flatten (map attrValues (attrValues devices)));
+ sortedDeviceList = diskoLib.sortDevicesByDependencies (cfg.config._meta.deviceDependencies or { }) devices;
+ in
+ ''
+ set -efux
+ # first unmount the filesystems in reverse alphabetical order
+ ${concatStrings (lib.reverseList (attrValues fsMounts))}
+
+ # Than close the devices
+ ${concatMapStrings (dev: (attrByPath (dev ++ [ "_unmount" ]) {} devices).dev or "") (lib.reverseList sortedDeviceList)}
+ '';
+ };
+ _disko = lib.mkOption {
+ internal = true;
+ type = lib.types.str;
+ description = ''
+ The script to umount, create and mount all devices defined by disko.devices
+ Deprecated in favor of _destroyFormatMount
+ '';
+ default = ''
+ ${cfg.config._legacyDestroy}
+ ${cfg.config._create}
+ ${cfg.config._mount}
+ '';
+ };
+ _destroyFormatMount = lib.mkOption {
+ internal = true;
+ type = lib.types.str;
+ description = ''
+ The script to unmount, create and mount all devices defined by disko.devices
+ '';
+ default = ''
+ ${cfg.config._destroy}
+ ${cfg.config._create}
+ ${cfg.config._mount}
+ '';
+ };
+ _formatMount = lib.mkOption {
+ internal = true;
+ type = lib.types.str;
+ description = ''
+ The script to create and mount all devices defined by disko.devices, without wiping the disks first
+ '';
+ default = ''
+ ${cfg.config._create}
+ ${cfg.config._mount}
+ '';
+ };
+ _config = lib.mkOption {
+ internal = true;
+ description = ''
+ The NixOS config generated by disko
+ '';
+ default =
+ with lib; let
+ configKeys = flatten (map attrNames (flatten (map (dev: dev._config) (flatten (map attrValues (attrValues devices))))));
+ collectedConfigs = flatten (map (dev: dev._config) (flatten (map attrValues (attrValues devices))));
+ in
+ genAttrs configKeys (key: mkMerge (catAttrs key collectedConfigs));
+ };
+ };
+ });
+
+ # import all the types from the types directory
+ types = lib.listToAttrs (
+ map
+ (file: lib.nameValuePair
+ (lib.removeSuffix ".nix" file)
+ (diskoLib.mkSubType (./types + "/${file}"))
+ )
+ (lib.attrNames (builtins.readDir ./types))
+ );
+
+
+ # render types into an json serializable format
+ serializeType = type:
+ let
+ options = lib.filter (x: !lib.hasPrefix "_" x) (lib.attrNames type.options);
+ in
+ lib.listToAttrs (
+ map
+ (option: lib.nameValuePair
+ option
+ type.options.${option}
+ )
+ options
+ );
+
+ typesSerializerLib = {
+ rootMountPoint = "";
+ options = null;
+ config = {
+ _module = {
+ args.name = "";
+ args._parent.name = "";
+ args._parent.type = "";
+ };
+ name = "";
+ };
+ parent = { };
+ device = "/dev/";
+ # Spoof part of nixpkgs/lib to analyze the types
+ lib = lib // {
+ mkOption = option: {
+ inherit (option) type;
+ description = option.description or null;
+ default = option.defaultText or option.default or null;
+ };
+ types = {
+ attrsOf = subType: {
+ type = "attrsOf";
+ inherit subType;
+ };
+ listOf = subType: {
+ type = "listOf";
+ inherit subType;
+ };
+ nullOr = subType: {
+ type = "nullOr";
+ inherit subType;
+ };
+ oneOf = types: {
+ type = "oneOf";
+ inherit types;
+ };
+ either = t1: t2: {
+ type = "oneOf";
+ types = [ t1 t2 ];
+ };
+ enum = choices: {
+ type = "enum";
+ inherit choices;
+ };
+ anything = "anything";
+ nonEmptyStr = "str";
+ strMatching = _: "str";
+ str = "str";
+ bool = "bool";
+ int = "int";
+ submodule = x: x {
+ inherit (diskoLib.typesSerializerLib) lib config options;
+ name = "";
+ };
+ };
+ };
+ diskoLib = {
+ optionTypes.absolute-pathname = "absolute-pathname";
+ # Spoof these types to avoid infinite recursion
+ deviceType = _: "";
+ partitionType = _: "";
+ subType = { types, ... }: {
+ type = "oneOf";
+ types = lib.attrNames types;
+ };
+ mkCreateOption = option: "_create";
+ };
+ };
+
+ jsonTypes = lib.listToAttrs
+ (
+ map
+ (file: lib.nameValuePair
+ (lib.removeSuffix ".nix" file)
+ (diskoLib.serializeType (import (./types + "/${file}") diskoLib.typesSerializerLib))
+ )
+ (lib.filter (name: lib.hasSuffix ".nix" name) (lib.attrNames (builtins.readDir ./types)))
+ ) // {
+ partitionType = {
+ type = "oneOf";
+ types = lib.attrNames diskoLib._partitionTypes;
+ };
+ deviceType = {
+ type = "oneOf";
+ types = lib.attrNames diskoLib._deviceTypes;
+ };
+ };
+
+ } // outputs;
+in
+diskoLib
diff --git a/pkgs/disko/lib/interactive-vm.nix b/pkgs/disko/lib/interactive-vm.nix
new file mode 100644
index 0000000..67d5eb8
--- /dev/null
+++ b/pkgs/disko/lib/interactive-vm.nix
@@ -0,0 +1,81 @@
+{ diskoLib, modulesPath, config, pkgs, lib, ... }:
+
+let
+ vm_disko = (diskoLib.testLib.prepareDiskoConfig config diskoLib.testLib.devices).disko;
+ cfg_ = (lib.evalModules {
+ modules = lib.singleton {
+ # _file = toString input;
+ imports = lib.singleton { disko.devices = vm_disko.devices; };
+ options = {
+ disko.devices = lib.mkOption {
+ type = diskoLib.toplevel;
+ };
+ disko.testMode = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ };
+ };
+ };
+ }).config;
+ disks = lib.attrValues cfg_.disko.devices.disk;
+ rootDisk = {
+ name = "root";
+ file = ''"$tmp"/${lib.escapeShellArg (builtins.head disks).name}.qcow2'';
+ driveExtraOpts.cache = "writeback";
+ driveExtraOpts.werror = "report";
+ deviceExtraOpts.bootindex = "1";
+ deviceExtraOpts.serial = "root";
+ };
+ otherDisks = map
+ (disk: {
+ name = disk.name;
+ file = ''"$tmp"/${lib.escapeShellArg disk.name}.qcow2'';
+ driveExtraOpts.werror = "report";
+ })
+ (builtins.tail disks);
+
+ diskoBasedConfiguration = {
+ # generated from disko config
+ virtualisation.fileSystems = cfg_.disko.devices._config.fileSystems;
+ boot = cfg_.disko.devices._config.boot or { };
+ swapDevices = cfg_.disko.devices._config.swapDevices or [ ];
+ };
+
+ hostPkgs = config.virtualisation.host.pkgs;
+in
+{
+ imports = [
+ (modulesPath + "/virtualisation/qemu-vm.nix")
+ diskoBasedConfiguration
+ ];
+
+ disko.testMode = true;
+
+ disko.imageBuilder.copyNixStore = false;
+ disko.imageBuilder.extraConfig = {
+ disko.devices = cfg_.disko.devices;
+ };
+ disko.imageBuilder.imageFormat = "qcow2";
+
+ virtualisation.useEFIBoot = config.disko.tests.efi;
+ virtualisation.memorySize = lib.mkDefault config.disko.memSize;
+ virtualisation.useDefaultFilesystems = false;
+ virtualisation.diskImage = null;
+ virtualisation.qemu.drives = [ rootDisk ] ++ otherDisks;
+ boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms
+ boot.zfs.forceImportAll = true;
+ boot.zfs.forceImportRoot = lib.mkForce true;
+
+ system.build.vmWithDisko = hostPkgs.writers.writeDashBin "disko-vm" ''
+ set -efux
+ export tmp=$(${hostPkgs.coreutils}/bin/mktemp -d)
+ trap 'rm -rf "$tmp"' EXIT
+ ${lib.concatMapStringsSep "\n" (disk: ''
+ ${hostPkgs.qemu}/bin/qemu-img create -f qcow2 \
+ -b ${config.system.build.diskoImages}/${lib.escapeShellArg disk.name}.qcow2 \
+ -F qcow2 "$tmp"/${lib.escapeShellArg disk.name}.qcow2
+ '') disks}
+ set +f
+ ${config.system.build.vm}/bin/run-*-vm
+ '';
+}
diff --git a/pkgs/disko/lib/make-disk-image.nix b/pkgs/disko/lib/make-disk-image.nix
new file mode 100644
index 0000000..39b55f0
--- /dev/null
+++ b/pkgs/disko/lib/make-disk-image.nix
@@ -0,0 +1,228 @@
+{ config
+, diskoLib
+, lib
+, extendModules
+, options
+, ...
+}:
+let
+ diskoCfg = config.disko;
+ cfg = diskoCfg.imageBuilder;
+ inherit (cfg) pkgs imageFormat;
+ checked = diskoCfg.checkScripts;
+
+ configSupportsZfs = config.boot.supportedFilesystems.zfs or false;
+ vmTools = pkgs.vmTools.override
+ {
+ rootModules = [
+ "9p" "9pnet_virtio" # we can drop those in future if we stop supporting 24.11
+
+ "virtiofs"
+ "virtio_pci"
+ "virtio_blk"
+ "virtio_balloon"
+ "virtio_rng"
+ ]
+ ++ (lib.optional configSupportsZfs "zfs")
+ ++ cfg.extraRootModules;
+ kernel = pkgs.aggregateModules
+ (with cfg.kernelPackages; [ kernel ]
+ ++ lib.optional (lib.elem "zfs" cfg.extraRootModules || configSupportsZfs) zfs);
+ }
+ // lib.optionalAttrs (diskoLib.vmToolsSupportsCustomQemu lib)
+ {
+ customQemu = cfg.qemu;
+ };
+ cleanedConfig = diskoLib.testLib.prepareDiskoConfig config diskoLib.testLib.devices;
+ systemToInstall = extendModules {
+ modules = [
+ cfg.extraConfig
+ {
+ disko.testMode = true;
+ disko.devices = lib.mkForce cleanedConfig.disko.devices;
+ boot.loader.grub.devices = lib.mkForce cleanedConfig.boot.loader.grub.devices;
+ }
+ ];
+ };
+ dependencies = with pkgs; [
+ bash
+ coreutils
+ gnused
+ parted # for partprobe
+ systemdMinimal
+ nix
+ util-linux
+ findutils
+ kmod
+ ] ++ cfg.extraDependencies;
+ preVM = ''
+ # shellcheck disable=SC2154
+ mkdir -p "$out"
+ ${lib.concatMapStringsSep "\n" (disk:
+ # shellcheck disable=SC2154
+ "${pkgs.qemu}/bin/qemu-img create -f ${imageFormat} \"$out/${disk.imageName}.${imageFormat}\" ${disk.imageSize}"
+ ) (lib.attrValues diskoCfg.devices.disk)}
+ # This makes disko work, when canTouchEfiVariables is set to true.
+ # Technically these boot entries will no be persisted this way, but
+ # in most cases this is OK, because we can rely on the standard location for UEFI executables.
+ install -m600 ${pkgs.OVMF.variables} efivars.fd
+ '';
+
+ closureInfo = pkgs.closureInfo {
+ rootPaths = [ systemToInstall.config.system.build.toplevel ];
+ };
+ partitioner = ''
+ set -efux
+ # running udev, stolen from stage-1.sh
+ echo "running udev..."
+ ln -sfn /proc/self/fd /dev/fd
+ ln -sfn /proc/self/fd/0 /dev/stdin
+ ln -sfn /proc/self/fd/1 /dev/stdout
+ ln -sfn /proc/self/fd/2 /dev/stderr
+ mkdir -p /etc/udev
+ mount -t efivarfs none /sys/firmware/efi/efivars
+ ln -sfn ${systemToInstall.config.system.build.etc}/etc/udev/rules.d /etc/udev/rules.d
+ mkdir -p /dev/.mdadm
+ ${pkgs.systemdMinimal}/lib/systemd/systemd-udevd --daemon
+ partprobe
+ udevadm trigger --action=add
+ udevadm settle
+
+ ${lib.optionalString diskoCfg.testMode ''
+ export IN_DISKO_TEST=1
+ ''}
+ ${lib.getExe systemToInstall.config.system.build.destroyFormatMount} --yes-wipe-all-disks
+ '';
+
+ installer = lib.optionalString cfg.copyNixStore ''
+ # populate nix db, so nixos-install doesn't complain
+ export NIX_STATE_DIR=${systemToInstall.config.disko.rootMountPoint}/nix/var/nix
+ nix-store --load-db < "${closureInfo}/registration"
+
+ # We copy files with cp because `nix copy` seems to have a large memory leak
+ mkdir -p ${systemToInstall.config.disko.rootMountPoint}/nix/store
+ time xargs cp --recursive --target ${systemToInstall.config.disko.rootMountPoint}/nix/store < ${closureInfo}/store-paths
+
+ ${systemToInstall.config.system.build.nixos-install}/bin/nixos-install --root ${systemToInstall.config.disko.rootMountPoint} --system ${systemToInstall.config.system.build.toplevel} --keep-going --no-channel-copy -v --no-root-password --option binary-caches ""
+ umount -Rv ${systemToInstall.config.disko.rootMountPoint}
+ '';
+
+ QEMU_OPTS = lib.concatStringsSep " " ([
+ "-drive if=pflash,format=raw,unit=0,readonly=on,file=${pkgs.OVMF.firmware}"
+ "-drive if=pflash,format=raw,unit=1,file=efivars.fd"
+ ] ++ builtins.map
+ (disk:
+ "-drive file=\"$out\"/${disk.imageName}.${imageFormat},if=virtio,cache=unsafe,werror=report,format=${imageFormat}"
+ )
+ (lib.attrValues diskoCfg.devices.disk));
+in
+{
+ system.build.diskoImages = vmTools.runInLinuxVM (pkgs.runCommand cfg.name
+ {
+ buildInputs = dependencies;
+ inherit preVM QEMU_OPTS;
+ postVm = cfg.extraPostVM;
+ inherit (diskoCfg) memSize;
+ }
+ (partitioner + installer));
+
+ system.build.diskoImagesScript = diskoLib.writeCheckedBash { inherit checked pkgs; } cfg.name ''
+ set -efu
+ export PATH=${lib.makeBinPath dependencies}
+ showUsage() {
+ cat <<\USAGE
+ Usage: $script [options]
+
+ Options:
+ * --pre-format-files
+ copies the src to the dst on the VM, before disko is run
+ This is useful to provide secrets like LUKS keys, or other files you need for formatting
+ * --post-format-files
+ copies the src to the dst on the finished image
+ These end up in the images later and is useful if you want to add some extra stateful files
+ They will have the same permissions but will be owned by root:root
+ * --build-memory
+ specify the amount of memory in MiB that gets allocated to the build VM
+ This can be useful if you want to build images with a more involed NixOS config
+ The default is disko.memSize which defaults to ${builtins.toString options.disko.memSize.default} MiB
+ USAGE
+ }
+
+ export out=$PWD
+ TMPDIR=$(mktemp -d); export TMPDIR
+ trap 'rm -rf "$TMPDIR"' EXIT
+ cd "$TMPDIR"
+
+ mkdir copy_before_disko copy_after_disko
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --pre-format-files)
+ src=$2
+ dst=$3
+ cp --reflink=auto -r "$src" copy_before_disko/"$(echo "$dst" | base64)"
+ shift 2
+ ;;
+ --post-format-files)
+ src=$2
+ dst=$3
+ cp --reflink=auto -r "$src" copy_after_disko/"$(echo "$dst" | base64)"
+ shift 2
+ ;;
+ --build-memory)
+ regex="^[0-9]+$"
+ if ! [[ $2 =~ $regex ]]; then
+ echo "'$2' is not a number"
+ exit 1
+ fi
+ build_memory=$2
+ shift 1
+ ;;
+ *)
+ showUsage
+ exit 1
+ ;;
+ esac
+ shift
+ done
+
+ export preVM=${diskoLib.writeCheckedBash { inherit pkgs checked; } "preVM.sh" ''
+ set -efu
+ mv copy_before_disko copy_after_disko xchg/
+ origBuilder=${pkgs.writeScript "disko-builder" ''
+ set -eu
+ export PATH=${lib.makeBinPath dependencies}
+ for src in /tmp/xchg/copy_before_disko/*; do
+ [ -e "$src" ] || continue
+ dst=$(basename "$src" | base64 -d)
+ mkdir -p "$(dirname "$dst")"
+ cp -r "$src" "$dst"
+ done
+ set -f
+ ${partitioner}
+ set +f
+ for src in /tmp/xchg/copy_after_disko/*; do
+ [ -e "$src" ] || continue
+ dst=/mnt/$(basename "$src" | base64 -d)
+ mkdir -p "$(dirname "$dst")"
+ cp -r "$src" "$dst"
+ done
+ ${installer}
+ ''}
+ echo "export origBuilder=$origBuilder" >> xchg/saved-env
+ ${preVM}
+ ''}
+ export postVM=${diskoLib.writeCheckedBash { inherit pkgs checked; } "postVM.sh" cfg.extraPostVM}
+
+ build_memory=''${build_memory:-${builtins.toString diskoCfg.memSize}}
+ # shellcheck disable=SC2016
+ QEMU_OPTS=${lib.escapeShellArg QEMU_OPTS}
+ # replace quoted $out with the actual path
+ QEUM_OPTS=''${QEMU_OPTS//\$out/$out}
+ QEMU_OPTS+=" -m $build_memory"
+ export QEMU_OPTS
+
+ ${pkgs.bash}/bin/sh -e ${vmTools.vmRunCommand vmTools.qemuCommandLinux}
+ cd /
+ '';
+}
diff --git a/pkgs/disko/lib/tests.nix b/pkgs/disko/lib/tests.nix
new file mode 100644
index 0000000..3cef657
--- /dev/null
+++ b/pkgs/disko/lib/tests.nix
@@ -0,0 +1,337 @@
+{ lib
+, makeTest
+, eval-config
+, ...
+}:
+
+let
+ testLib = {
+ # this takes a nixos config and changes the disk devices so we can run them inside the qemu test runner
+ # basically changes all the disk.*.devices to something like /dev/vda or /dev/vdb etc.
+ prepareDiskoConfig = cfg: devices:
+ let
+ cleanedTopLevel = lib.filterAttrsRecursive (n: _: !lib.hasPrefix "_" n) cfg;
+
+ preparedDisks = lib.foldlAttrs
+ (acc: n: v: {
+ devices = lib.tail acc.devices;
+ grub-devices = acc.grub-devices ++ (lib.optional (lib.any (part: (part.type or "") == "EF02") (lib.attrValues (v.content.partitions or { }))) (lib.head acc.devices));
+ disks = acc.disks // {
+ "${n}" = v // {
+ device = lib.head acc.devices;
+ content = v.content // { device = lib.head acc.devices; };
+ };
+ };
+ })
+ {
+ inherit devices;
+ grub-devices = [ ];
+ disks = { };
+ }
+ cleanedTopLevel.disko.devices.disk;
+ in
+ cleanedTopLevel // {
+ boot.loader.grub.devices = if (preparedDisks.grub-devices != [ ]) then preparedDisks.grub-devices else [ "nodev" ];
+ disko.devices = cleanedTopLevel.disko.devices // {
+ disk = preparedDisks.disks;
+ };
+ };
+
+ # list of devices generated inside qemu
+ devices = [
+ "/dev/vda"
+ "/dev/vdb"
+ "/dev/vdc"
+ "/dev/vdd"
+ "/dev/vde"
+ "/dev/vdf"
+ "/dev/vdg"
+ "/dev/vdh"
+ "/dev/vdi"
+ "/dev/vdj"
+ "/dev/vdk"
+ "/dev/vdl"
+ "/dev/vdm"
+ "/dev/vdn"
+ "/dev/vdo"
+ ];
+
+ # This is the test generator for a disko test
+ makeDiskoTest =
+ { name
+ , disko-config
+ , extendModules ? null
+ , pkgs ? import { }
+ , extraTestScript ? ""
+ , bootCommands ? ""
+ , extraInstallerConfig ? { }
+ , extraSystemConfig ? { }
+ , efi ? !pkgs.stdenv.hostPlatform.isRiscV64
+ , postDisko ? ""
+ , testMode ? "module" # can be one of direct module cli
+ , testBoot ? true # if we actually want to test booting or just create/mount
+ , enableOCR ? false
+ }:
+ let
+ makeTest' = args:
+ makeTest args {
+ inherit pkgs;
+ inherit (pkgs) system;
+ };
+ # for installation we skip /dev/vda because it is the test runner disk
+
+ importedDiskoConfig =
+ if builtins.isPath disko-config then
+ import disko-config
+ else
+ disko-config;
+
+ diskoConfigWithArgs =
+ if builtins.isFunction importedDiskoConfig then
+ importedDiskoConfig { inherit lib; }
+ else
+ importedDiskoConfig;
+ testConfigInstall = testLib.prepareDiskoConfig diskoConfigWithArgs (lib.tail testLib.devices);
+ # we need to shift the disks by one because the first disk is the /dev/vda of the test runner
+ # so /dev/vdb becomes /dev/vda etc.
+ testConfigBooted = testLib.prepareDiskoConfig diskoConfigWithArgs testLib.devices;
+
+ tsp-generator = pkgs.callPackage ../. { checked = true; };
+ tsp-format = (tsp-generator._cliFormat testConfigInstall) pkgs;
+ tsp-mount = (tsp-generator._cliMount testConfigInstall) pkgs;
+ tsp-unmount = (tsp-generator._cliUnmount testConfigInstall) pkgs;
+ tsp-disko = (tsp-generator._cliDestroyFormatMount testConfigInstall) pkgs;
+ tsp-config = tsp-generator.config testConfigBooted;
+ num-disks = builtins.length (lib.attrNames testConfigBooted.disko.devices.disk);
+
+ installed-system = { config, ... }: {
+ imports = [
+ (lib.optionalAttrs (testMode == "direct") tsp-config)
+ (lib.optionalAttrs (testMode == "module") {
+ disko.enableConfig = true;
+ imports = [
+ ../module.nix
+ testConfigBooted
+ ];
+ })
+ ];
+
+ # config for tests to make them run faster or work at all
+ documentation.enable = false;
+ hardware.enableAllFirmware = lib.mkForce false;
+ # FIXME: we don't have an systemd in stage-1 equialvent for this
+ boot.initrd.preDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
+ echo -n 'secretsecret' > /tmp/secret.key
+ '';
+ boot.consoleLogLevel = lib.mkForce 100;
+ boot.loader.systemd-boot.enable = lib.mkDefault efi;
+ };
+
+ installed-system-eval = eval-config {
+ modules = [ installed-system ];
+ inherit (pkgs) system;
+ };
+
+ installedTopLevel = ((if extendModules != null then extendModules else installed-system-eval.extendModules) {
+ modules = [
+ ({ config, ... }: {
+ imports = [
+ extraSystemConfig
+ ({ modulesPath, ... }: {
+ imports = [
+ (modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests
+ (modulesPath + "/profiles/qemu-guest.nix")
+ ];
+ disko.devices = lib.mkForce testConfigBooted.disko.devices;
+ })
+ ];
+
+ # since we boot on a different machine, the efi payload needs to be portable
+ boot.loader.grub.efiInstallAsRemovable = efi;
+ boot.loader.grub.efiSupport = efi;
+ boot.loader.systemd-boot.graceful = true;
+
+ # we always want the bind-mounted nix store. otherwise tests take forever
+ fileSystems."/nix/store" = lib.mkForce {
+ device = "nix-store";
+ fsType = "9p";
+ neededForBoot = true;
+ options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
+ };
+ boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms
+
+ # grub will install to these devices, we need to force those or we are offset by 1
+ # we use mkOveride 70, so that users can override this with mkForce in case they are testing grub mirrored boots
+ boot.loader.grub.devices = lib.mkOverride 70 testConfigInstall.boot.loader.grub.devices;
+
+ assertions = [
+ {
+ assertion = builtins.length config.boot.loader.grub.mirroredBoots > 1 -> config.boot.loader.grub.devices == [ ];
+ message = ''
+ When using `--vm-test` in combination with `mirroredBoots`,
+ it is necessary to configure `boot.loader.grub.devices` as an empty list by setting `boot.loader.grub.devices = lib.mkForce [];`.
+ This adjustment is crucial because the `--vm-test` mechanism automatically overrides the grub boot devices as part of the virtual machine test.
+ '';
+ }
+ ];
+ })
+ ];
+ }).config.system.build.toplevel;
+
+ in
+ makeTest' {
+ name = "disko-${name}";
+ meta.timeout = 600; # 10 minutes
+ inherit enableOCR;
+
+ nodes.machine = { pkgs, ... }: {
+ imports = [
+ (lib.optionalAttrs (testMode == "module") {
+ imports = [
+ ../module.nix
+ ];
+ disko = {
+ enableConfig = false;
+ checkScripts = true;
+ devices = testConfigInstall.disko.devices;
+ };
+ })
+ extraInstallerConfig
+
+ # from base.nix
+ ({ config, ... }: {
+ boot.supportedFilesystems =
+ [ "btrfs" "cifs" "f2fs" "jfs" "ntfs" "reiserfs" "vfat" "xfs" ] ++
+ lib.optional (config.networking.hostId != null && lib.meta.availableOn pkgs.stdenv.hostPlatform config.boot.zfs.package) "zfs";
+ })
+
+ (if lib.versionAtLeast (lib.versions.majorMinor lib.version) "23.11" then {
+ boot.swraid.enable = true;
+ } else {
+ boot.initrd.services.swraid.enable = true;
+ })
+ ];
+
+ systemd.services.mdmonitor.enable = false; # silence some weird warnings
+
+ environment.systemPackages = [
+ pkgs.jq
+ ];
+
+ # speed-up eval
+ documentation.enable = false;
+
+ nix.settings = {
+ substituters = lib.mkForce [ ];
+ hashed-mirrors = null;
+ connect-timeout = 1;
+ };
+
+ networking.hostId = lib.mkIf
+ (
+ (testConfigInstall ? networking.hostId) && (testConfigInstall.networking.hostId != null)
+ )
+ testConfigInstall.networking.hostId;
+
+ virtualisation.emptyDiskImages = builtins.genList (_: 4096) num-disks;
+
+ # useful for debugging via repl
+ system.build.systemToInstall = installed-system-eval;
+ };
+
+ testScript = { nodes, ... }: ''
+ def disks(oldmachine, num_disks):
+ disk_flags = []
+ for i in range(num_disks):
+ disk_flags += [
+ '-drive',
+ f"file={oldmachine.state_dir}/empty{i}.qcow2,id=drive{i + 1},if=none,index={i + 1},werror=report",
+ '-device',
+ f"virtio-blk-pci,drive=drive{i + 1}"
+ ]
+ return disk_flags
+
+ def create_test_machine(
+ oldmachine=None, **kwargs
+ ): # taken from
+ start_command = [
+ "${pkgs.qemu_test}/bin/qemu-kvm",
+ "-cpu",
+ "max",
+ "-m",
+ "1024",
+ "-virtfs",
+ "local,path=/nix/store,security_model=none,mount_tag=nix-store",
+ *disks(oldmachine, ${toString num-disks})
+ ]
+ ${lib.optionalString efi ''
+ start_command += ["-drive",
+ "if=pflash,format=raw,unit=0,readonly=on,file=${pkgs.OVMF.firmware}",
+ "-drive",
+ "if=pflash,format=raw,unit=1,readonly=on,file=${pkgs.OVMF.variables}"
+ ]
+ ''}
+ machine = create_machine(start_command=" ".join(start_command), **kwargs)
+ driver.machines.append(machine)
+ return machine
+
+ machine.start()
+ machine.succeed("echo -n 'additionalSecret' > /tmp/additionalSecret.key")
+ machine.succeed("echo -n 'secretsecret' > /tmp/secret.key")
+ ${lib.optionalString (testMode == "direct") ''
+ # running direct mode
+ machine.succeed("${lib.getExe tsp-format}")
+ machine.succeed("${lib.getExe tsp-mount}")
+ machine.succeed("${lib.getExe tsp-mount}") # verify that mount is idempotent
+ machine.succeed("${lib.getExe tsp-unmount}")
+ machine.succeed("${lib.getExe tsp-unmount}") # verify that umount is idempotent
+ machine.succeed("${lib.getExe tsp-mount}") # verify that mount is idempotent
+ machine.succeed("${lib.getExe tsp-disko} --yes-wipe-all-disks") # verify that we can destroy and recreate
+ machine.succeed("mkdir -p /mnt/home")
+ machine.succeed("touch /mnt/home/testfile")
+ machine.succeed("${lib.getExe tsp-format}") # verify that format is idempotent
+ machine.succeed("test -e /mnt/home/testfile")
+ ''}
+ ${lib.optionalString (testMode == "module") ''
+ # running module mode
+ machine.succeed("${lib.getExe nodes.machine.system.build.format}")
+ machine.succeed("${lib.getExe nodes.machine.system.build.mount}")
+ machine.succeed("${lib.getExe nodes.machine.system.build.mount}") # verify that mount is idempotent
+ machine.succeed("${lib.getExe nodes.machine.system.build.destroyFormatMount} --yes-wipe-all-disks") # verify that we can destroy and recreate again
+ machine.succeed("mkdir -p /mnt/home")
+ machine.succeed("touch /mnt/home/testfile")
+ machine.succeed("${lib.getExe nodes.machine.system.build.format}") # verify that format is idempotent
+ machine.succeed("test -e /mnt/home/testfile")
+ ''}
+
+ ${postDisko}
+
+ ${lib.optionalString testBoot ''
+ # mount nix-store in /mnt
+ machine.succeed("mkdir -p /mnt/nix/store")
+ machine.succeed("mount --bind /nix/store /mnt/nix/store")
+
+ machine.succeed("nix-store --load-db < ${pkgs.closureInfo {rootPaths = [installedTopLevel];}}/registration")
+
+ # fix "this is not a NixOS installation"
+ machine.succeed("mkdir -p /mnt/etc")
+ machine.succeed("touch /mnt/etc/NIXOS")
+
+ machine.succeed("mkdir -p /mnt/nix/var/nix/profiles")
+ machine.succeed("nix-env -p /mnt/nix/var/nix/profiles/system --set ${installedTopLevel}")
+ machine.succeed("NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root /mnt -- ${installedTopLevel}/bin/switch-to-configuration boot")
+ machine.succeed("sync")
+ machine.shutdown()
+
+ machine = create_test_machine(oldmachine=machine, name="booted_machine")
+ machine.start()
+ ${bootCommands}
+ machine.wait_for_unit("local-fs.target")
+ ''}
+
+ ${extraTestScript}
+ '';
+ };
+ };
+in
+testLib
diff --git a/pkgs/disko/lib/types/btrfs.nix b/pkgs/disko/lib/types/btrfs.nix
new file mode 100644
index 0000000..d9cff2c
--- /dev/null
+++ b/pkgs/disko/lib/types/btrfs.nix
@@ -0,0 +1,272 @@
+{ config, options, diskoLib, lib, rootMountPoint, parent, device, ... }:
+let
+ swapType = lib.mkOption {
+ type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
+ options = {
+ size = lib.mkOption {
+ type = lib.types.strMatching "^([0-9]+[KMGTP])?$";
+ description = "Size of the swap file (e.g. 2G)";
+ };
+
+ path = lib.mkOption {
+ type = lib.types.str;
+ default = name;
+ description = "Path to the swap file (relative to the mountpoint)";
+ };
+
+ priority = lib.mkOption {
+ type = lib.types.nullOr lib.types.int;
+ default = null;
+ description = ''
+ Specify the priority of the swap file. Priority is a value between 0 and 32767.
+ Higher numbers indicate higher priority.
+ null lets the kernel choose a priority, which will show up as a negative value.
+ '';
+ };
+
+ options = lib.mkOption {
+ type = lib.types.listOf lib.types.nonEmptyStr;
+ default = [ "defaults" ];
+ example = [ "nofail" ];
+ description = "Options used to mount the swap.";
+ };
+ };
+ }));
+ default = { };
+ description = "Swap files";
+ };
+
+ swapConfig = { mountpoint, swap }:
+ {
+ swapDevices = builtins.map
+ (file: {
+ device = "${mountpoint}/${file.path}";
+ inherit (file) priority options;
+ })
+ (lib.attrValues swap);
+ };
+
+ swapCreate = mountpoint: swap:
+ lib.concatMapStringsSep
+ "\n"
+ (file: ''
+ if ! test -e "${mountpoint}/${file.path}"; then
+ btrfs filesystem mkswapfile --size ${file.size} "${mountpoint}/${file.path}"
+ fi
+ '')
+ (lib.attrValues swap);
+
+in
+{
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "btrfs" ];
+ internal = true;
+ description = "Type";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ default = device;
+ description = "Device to use";
+ };
+ extraArgs = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Extra arguments";
+ };
+ mountOptions = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ "defaults" ];
+ description = "A list of options to pass to mount.";
+ };
+ subvolumes = lib.mkOption {
+ type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
+ options = {
+ name = lib.mkOption {
+ type = lib.types.str;
+ default = config._module.args.name;
+ description = "Name of the BTRFS subvolume.";
+ };
+ type = lib.mkOption {
+ type = lib.types.enum [ "btrfs_subvol" ];
+ default = "btrfs_subvol";
+ internal = true;
+ description = "Type";
+ };
+ extraArgs = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Extra arguments";
+ };
+ mountOptions = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ "defaults" ];
+ description = "Options to pass to mount";
+ };
+ mountpoint = lib.mkOption {
+ type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
+ default = null;
+ description = "Location to mount the subvolume to.";
+ };
+ swap = swapType;
+ };
+ }));
+ default = { };
+ description = "Subvolumes to define for BTRFS.";
+ };
+ mountpoint = lib.mkOption {
+ type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
+ default = null;
+ description = "A path to mount the BTRFS filesystem to.";
+ };
+ swap = swapType;
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = _dev: { };
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ # create the filesystem only if the device seems empty
+ if ! (blkid "${config.device}" -o export | grep -q '^TYPE='); then
+ mkfs.btrfs "${config.device}" ${toString config.extraArgs}
+ fi
+ ${lib.optionalString (config.swap != {} || config.subvolumes != {}) ''
+ if (blkid "${config.device}" -o export | grep -q '^TYPE=btrfs$'); then
+ ${lib.optionalString (config.swap != {}) ''
+ (
+ MNTPOINT=$(mktemp -d)
+ mount ${device} "$MNTPOINT" -o subvol=/
+ trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
+ ${swapCreate "$MNTPOINT" config.swap}
+ )
+ ''}
+ ${lib.concatMapStrings (subvol: ''
+ (
+ MNTPOINT=$(mktemp -d)
+ mount "${config.device}" "$MNTPOINT" -o subvol=/
+ trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
+ SUBVOL_ABS_PATH="$MNTPOINT/${subvol.name}"
+ mkdir -p "$(dirname "$SUBVOL_ABS_PATH")"
+ if ! btrfs subvolume show "$SUBVOL_ABS_PATH" > /dev/null 2>&1; then
+ btrfs subvolume create "$SUBVOL_ABS_PATH" ${toString subvol.extraArgs}
+ fi
+ ${swapCreate "$SUBVOL_ABS_PATH" subvol.swap}
+ )
+ '') (lib.attrValues config.subvolumes)}
+ fi
+ ''}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default =
+ let
+ subvolMounts = lib.concatMapAttrs
+ (_: subvol:
+ lib.warnIf (subvol.mountOptions != (options.subvolumes.type.getSubOptions [ ]).mountOptions.default && subvol.mountpoint == null)
+ "Subvolume ${subvol.name} has mountOptions but no mountpoint. See upgrade guide (2023-07-09 121df48)."
+ lib.optionalAttrs
+ (subvol.mountpoint != null)
+ {
+ ${subvol.mountpoint} = ''
+ if ! findmnt "${config.device}" "${rootMountPoint}${subvol.mountpoint}" > /dev/null 2>&1; then
+ mount "${config.device}" "${rootMountPoint}${subvol.mountpoint}" \
+ ${lib.concatMapStringsSep " " (opt: "-o ${opt}") (subvol.mountOptions ++ [ "subvol=${subvol.name}" ])} \
+ -o X-mount.mkdir
+ fi
+ '';
+ }
+ )
+ config.subvolumes;
+ in
+ {
+ fs = subvolMounts // lib.optionalAttrs (config.mountpoint != null) {
+ ${config.mountpoint} = ''
+ if ! findmnt "${config.device}" "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then
+ mount "${config.device}" "${rootMountPoint}${config.mountpoint}" \
+ ${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \
+ -o X-mount.mkdir
+ fi
+ '';
+ };
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default =
+ let
+ subvolMounts = lib.concatMapAttrs
+ (_: subvol:
+ lib.optionalAttrs
+ (subvol.mountpoint != null)
+ {
+ ${subvol.mountpoint} = ''
+ if findmnt "${config.device}" "${rootMountPoint}${subvol.mountpoint}" > /dev/null 2>&1; then
+ umount "${rootMountPoint}${subvol.mountpoint}"
+ fi
+ '';
+ }
+ )
+ config.subvolumes;
+ in
+ {
+ fs = subvolMounts // lib.optionalAttrs (config.mountpoint != null) {
+ ${config.mountpoint} = ''
+ if findmnt "${config.device}" "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then
+ umount "${rootMountPoint}${config.mountpoint}"
+ fi
+ '';
+ };
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = [
+ (map
+ (subvol:
+ lib.optional (subvol.mountpoint != null) {
+ fileSystems.${subvol.mountpoint} = {
+ device = config.device;
+ fsType = "btrfs";
+ options = subvol.mountOptions ++ [ "subvol=${subvol.name}" ];
+ };
+ }
+ )
+ (lib.attrValues config.subvolumes))
+ (lib.optional (config.mountpoint != null) {
+ fileSystems.${config.mountpoint} = {
+ device = config.device;
+ fsType = "btrfs";
+ options = config.mountOptions;
+ };
+ })
+ (map
+ (subvol: swapConfig {
+ inherit (subvol) mountpoint swap;
+ })
+ (lib.attrValues config.subvolumes))
+ (swapConfig {
+ inherit (config) mountpoint swap;
+ })
+ ];
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs:
+ [ pkgs.btrfs-progs pkgs.gnugrep ];
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/disk.nix b/pkgs/disko/lib/types/disk.nix
new file mode 100644
index 0000000..343f968
--- /dev/null
+++ b/pkgs/disko/lib/types/disk.nix
@@ -0,0 +1,71 @@
+{ config, options, lib, diskoLib, ... }:
+{
+ options = {
+ name = lib.mkOption {
+ type = lib.types.str;
+ default = lib.replaceStrings [ "/" ] [ "_" ] config._module.args.name;
+ description = "Device name";
+ };
+ type = lib.mkOption {
+ type = lib.types.enum [ "disk" ];
+ default = "disk";
+ internal = true;
+ description = "Type";
+ };
+ device = lib.mkOption {
+ type = diskoLib.optionTypes.absolute-pathname; # TODO check if subpath of /dev ? - No! eg: /.swapfile
+ description = "Device path";
+ };
+ imageName = lib.mkOption {
+ type = lib.types.str;
+ default = config.name;
+ description = ''
+ name of the image when disko images are created
+ is used as an argument to "qemu-img create ..."
+ '';
+ };
+ imageSize = lib.mkOption {
+ type = lib.types.strMatching "[0-9]+[KMGTP]?";
+ description = ''
+ size of the image when disko images are created
+ is used as an argument to "qemu-img create ..."
+ '';
+ default = "2G";
+ };
+ content = diskoLib.deviceType { parent = config; device = config.device; };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = diskoLib.jsonType;
+ default =
+ lib.optionalAttrs (config.content != null) (config.content._meta [ "disk" config.device ]);
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = config.content._create;
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default = lib.optionalAttrs (config.content != null) config.content._mount;
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = lib.optionalAttrs (config.content != null) config.content._unmount;
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default =
+ lib.optional (config.content != null) config.content._config;
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [ pkgs.jq ] ++ lib.optionals (config.content != null) (config.content._pkgs pkgs);
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/filesystem.nix b/pkgs/disko/lib/types/filesystem.nix
new file mode 100644
index 0000000..e59e186
--- /dev/null
+++ b/pkgs/disko/lib/types/filesystem.nix
@@ -0,0 +1,109 @@
+{ config, options, lib, diskoLib, rootMountPoint, parent, device, ... }:
+{
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "filesystem" ];
+ internal = true;
+ description = "Type";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ default = device;
+ description = "Device to use";
+ };
+ extraArgs = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Extra arguments";
+ };
+ mountOptions = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ "defaults" ];
+ description = "Options to pass to mount";
+ };
+ mountpoint = lib.mkOption {
+ type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
+ default = null;
+ description = "Path to mount the filesystem to";
+ };
+ format = lib.mkOption {
+ type = lib.types.str;
+ description = "Format of the filesystem";
+ };
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = _dev: { };
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ if ! (blkid "${config.device}" | grep -q 'TYPE='); then
+ mkfs.${config.format} \
+ ${lib.escapeShellArgs config.extraArgs} \
+ "${config.device}"
+ fi
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default = lib.optionalAttrs (config.mountpoint != null) {
+ fs.${config.mountpoint} = ''
+ if ! findmnt "${config.device}" "${rootMountPoint}${config.mountpoint}" >/dev/null 2>&1; then
+ mount "${config.device}" "${rootMountPoint}${config.mountpoint}" \
+ -t "${config.format}" \
+ ${lib.concatMapStringsSep " " (opt: "-o ${lib.escapeShellArg opt}") config.mountOptions} \
+ -o X-mount.mkdir
+ fi
+ '';
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = lib.optionalAttrs (config.mountpoint != null) {
+ fs.${config.mountpoint} = ''
+ if findmnt "${config.device}" "${rootMountPoint}${config.mountpoint}" >/dev/null 2>&1; then
+ umount "${rootMountPoint}${config.mountpoint}"
+ fi
+ '';
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = lib.optional (config.mountpoint != null) {
+ fileSystems.${config.mountpoint} = {
+ device = config.device;
+ fsType = config.format;
+ options = config.mountOptions;
+ };
+ };
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ # type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs:
+ [ pkgs.util-linux pkgs.gnugrep ] ++ (
+ # TODO add many more
+ if (config.format == "xfs") then [ pkgs.xfsprogs ]
+ else if (config.format == "btrfs") then [ pkgs.btrfs-progs ]
+ else if (config.format == "vfat") then [ pkgs.dosfstools ]
+ else if (config.format == "ext2") then [ pkgs.e2fsprogs ]
+ else if (config.format == "ext3") then [ pkgs.e2fsprogs ]
+ else if (config.format == "ext4") then [ pkgs.e2fsprogs ]
+ else if (config.format == "bcachefs") then [ pkgs.bcachefs-tools ]
+ else if (config.format == "f2fs") then [ pkgs.f2fs-tools ]
+ else [ ]
+ );
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/gpt.nix b/pkgs/disko/lib/types/gpt.nix
new file mode 100644
index 0000000..27a87fc
--- /dev/null
+++ b/pkgs/disko/lib/types/gpt.nix
@@ -0,0 +1,297 @@
+{ config, options, lib, diskoLib, parent, device, ... }:
+let
+ sortedPartitions = lib.sort (x: y: x.priority < y.priority) (lib.attrValues config.partitions);
+ sortedHybridPartitions = lib.filter (p: p.hybrid != null) sortedPartitions;
+in
+{
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "gpt" ];
+ internal = true;
+ description = "Partition table";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ default = device;
+ description = "Device to use for the partition table";
+ };
+ partitions = lib.mkOption {
+ type = lib.types.attrsOf (lib.types.submodule ({ name, ... }@partition: {
+ options = {
+ type = lib.mkOption {
+ type =
+ let
+ hexPattern = len: "[A-Fa-f0-9]{${toString len}}";
+ in
+ lib.types.either
+ (lib.types.strMatching (hexPattern 4))
+ (lib.types.strMatching (lib.concatMapStringsSep "-" hexPattern [ 8 4 4 4 12 ]));
+ default = if partition.config.content != null && partition.config.content.type == "swap" then "8200" else "8300";
+ defaultText = ''8300 (Linux filesystem) normally, 8200 (Linux swap) if content.type is "swap"'';
+ description = ''
+ Filesystem type to use.
+ This can either be an sgdisk-specific short code (run sgdisk -L to see what is available),
+ or a fully specified GUID (see https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs).
+ '';
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ default =
+ if config._parent.type == "mdadm" then
+ # workaround because mdadm partlabel do not appear in /dev/disk/by-partlabel
+ "/dev/disk/by-id/md-name-any:${config._parent.name}-part${toString partition.config._index}"
+ else
+ "/dev/disk/by-partlabel/${diskoLib.hexEscapeUdevSymlink partition.config.label}";
+ defaultText = ''
+ if the parent is an mdadm device:
+ /dev/disk/by-id/md-name-any:''${config._parent.name}-part''${toString partition.config._index}
+
+ otherwise:
+ /dev/disk/by-partlabel/''${diskoLib.hexEscapeUdevSymlink partition.config.label}
+ '';
+ description = "Device to use for the partition";
+ };
+ priority = lib.mkOption {
+ type = lib.types.int;
+ default =
+ if partition.config.size or "" == "100%" then
+ 9001
+ else if partition.config.type == "EF02" then
+ # Boot partition should be created first, because some BIOS implementations require it.
+ # Priority defaults to 100 here to support any potential use-case for placing partitions prior to EF02
+ 100
+ else
+ 1000;
+ defaultText = ''
+ 1000: normal partitions
+ 9001: partitions with 100% size
+ 100: boot partitions (EF02)
+ '';
+ description = "Priority of the partition, smaller values are created first";
+ };
+ name = lib.mkOption {
+ type = lib.types.str;
+ description = "Name of the partition";
+ default = name;
+ };
+ label = lib.mkOption {
+ type = lib.types.str;
+ default =
+ let
+ # 72 bytes is the maximum length of a GPT partition name
+ # the labels seem to be in UTF-16, so 2 bytes per character
+ limit = 36;
+ label = "${config._parent.type}-${config._parent.name}-${partition.config.name}";
+ in
+ if (lib.stringLength label) > limit then
+ builtins.substring 0 limit (builtins.hashString "sha256" label)
+ else
+ label;
+ defaultText = ''
+ ''${config._parent.type}-''${config._parent.name}-''${partition.config.name}
+
+ or a truncated hash of the above if it is longer than 36 characters
+ '';
+ };
+ size = lib.mkOption {
+ type = lib.types.either (lib.types.enum [ "100%" ]) (lib.types.strMatching "[0-9]+[KMGTP]?");
+ default = "0";
+ description = ''
+ Size of the partition, in sgdisk format.
+ sets end automatically with the + prefix
+ can be 100% for the whole remaining disk, will be done last in that case.
+ '';
+ };
+ alignment = lib.mkOption {
+ type = lib.types.int;
+ default = if (builtins.substring (builtins.stringLength partition.config.start - 1) 1 partition.config.start == "s" || (builtins.substring (builtins.stringLength partition.config.end - 1) 1 partition.config.end == "s")) then 1 else 0;
+ defaultText = "1 if the unit of start or end is sectors, 0 otherwise";
+ description = "Alignment of the partition, if sectors are used as start or end it can be aligned to 1";
+ };
+ start = lib.mkOption {
+ type = lib.types.str;
+ default = "0";
+ description = "Start of the partition, in sgdisk format, use 0 for next available range";
+ };
+ end = lib.mkOption {
+ type = lib.types.str;
+ default = if partition.config.size == "100%" then "-0" else "+${partition.config.size}";
+ defaultText = ''
+ if partition.config.size == "100%" then "-0" else "+''${partition.config.size}";
+ '';
+ description = ''
+ End of the partition, in sgdisk format.
+ Use + for relative sizes from the partitions start
+ or - for relative sizes from the disks end
+ '';
+ };
+ content = diskoLib.partitionType { parent = config; device = partition.config.device; };
+ hybrid = lib.mkOption {
+ type = lib.types.nullOr (lib.types.submodule ({ ... } @ hp: {
+ options = {
+ mbrPartitionType = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "MBR type code";
+ };
+ mbrBootableFlag = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Set the bootable flag (aka the active flag) on any or all of your hybridized partitions";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ ${lib.optionalString (hp.config.mbrPartitionType != null) ''
+ sfdisk --label-nested dos --part-type "${parent.device}" ${(toString partition.config._index)} ${hp.config.mbrPartitionType}
+ udevadm trigger --subsystem-match=block
+ udevadm settle
+ ''}
+ ${lib.optionalString hp.config.mbrBootableFlag ''
+ sfdisk --label-nested dos --activate "${parent.device}" ${(toString partition.config._index)}
+ ''}
+ '';
+ };
+ };
+ }));
+ default = null;
+ description = "Entry to add to the Hybrid MBR table";
+ };
+ _index = lib.mkOption {
+ type = lib.types.int;
+ internal = true;
+ default = diskoLib.indexOf (x: x.name == partition.config.name) sortedPartitions 0;
+ defaultText = null;
+ };
+ };
+ }));
+ default = { };
+ description = "Attrs of partitions to add to the partition table";
+ };
+ efiGptPartitionFirst = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Place EFI GPT (0xEE) partition first in MBR (good for GRUB)";
+ };
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = dev:
+ lib.foldr lib.recursiveUpdate { } (map
+ (partition:
+ lib.optionalAttrs (partition.content != null) (partition.content._meta dev)
+ )
+ (lib.attrValues config.partitions));
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ if ! blkid "${config.device}" >&2; then
+ sgdisk --clear "${config.device}"
+ fi
+ ${lib.concatStrings (map (partition: ''
+ # try to create the partition, if it fails, try to change the type and name
+ if ! sgdisk \
+ --align-end ${lib.optionalString (partition.alignment != 0) ''--set-alignment=${builtins.toString partition.alignment}''} \
+ --new=${toString partition._index}:${partition.start}:${partition.end} \
+ --change-name="${toString partition._index}:${partition.label}" \
+ --typecode=${toString partition._index}:${partition.type} \
+ "${config.device}"
+ then sgdisk \
+ --change-name="${toString partition._index}:${partition.label}" \
+ --typecode=${toString partition._index}:${partition.type} \
+ "${config.device}"
+ fi
+ # ensure /dev/disk/by-path/..-partN exists before continuing
+ partprobe "${config.device}" || : # sometimes partprobe fails, but the partitions are still up2date
+ udevadm trigger --subsystem-match=block
+ udevadm settle
+ '') sortedPartitions)}
+
+ ${
+ lib.optionalString (sortedHybridPartitions != [])
+ ("sgdisk -h "
+ + (lib.concatStringsSep ":" (map (p: (toString p._index)) sortedHybridPartitions))
+ + (
+ lib.optionalString (!config.efiGptPartitionFirst) ":EE "
+ )
+ + ''"${parent.device}"'')
+ }
+ ${lib.concatMapStrings (p:
+ p.hybrid._create
+ )
+ sortedHybridPartitions
+ }
+
+ ${lib.concatStrings (map (partition: ''
+ ${lib.optionalString (partition.content != null) partition.content._create}
+ '') sortedPartitions)}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default =
+ let
+ partMounts = lib.foldr lib.recursiveUpdate { } (map
+ (partition:
+ lib.optionalAttrs (partition.content != null) partition.content._mount
+ )
+ (lib.attrValues config.partitions));
+ in
+ {
+ dev = partMounts.dev or "";
+ fs = partMounts.fs or { };
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default =
+ let
+ partMounts = lib.foldr lib.recursiveUpdate { } (map
+ (partition:
+ lib.optionalAttrs (partition.content != null) partition.content._unmount
+ )
+ (lib.attrValues config.partitions));
+ in
+ {
+ dev = partMounts.dev or "";
+ fs = partMounts.fs or { };
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = (map
+ (partition:
+ lib.optional (partition.content != null) partition.content._config
+ )
+ (lib.attrValues config.partitions))
+ ++ (lib.optional (lib.any (part: part.type == "EF02") (lib.attrValues config.partitions)) {
+ boot.loader.grub.devices = [ config.device ];
+ });
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs:
+ [
+ pkgs.gptfdisk
+ pkgs.systemdMinimal
+ pkgs.parted # for partprobe
+ ] ++ lib.flatten (map
+ (partition:
+ lib.optional (partition.content != null) (partition.content._pkgs pkgs)
+ )
+ (lib.attrValues config.partitions));
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/luks.nix b/pkgs/disko/lib/types/luks.nix
new file mode 100644
index 0000000..738acd1
--- /dev/null
+++ b/pkgs/disko/lib/types/luks.nix
@@ -0,0 +1,210 @@
+{ config, options, lib, diskoLib, parent, device, ... }:
+let
+ keyFile =
+ if config.settings ? "keyFile"
+ then config.settings.keyFile
+ else if config.askPassword
+ then ''<(set +x; echo -n "$password"; set -x)''
+ else if config.passwordFile != null
+ # do not print the password to the console
+ then ''<(set +x; echo -n "$(cat ${config.passwordFile})"; set -x)''
+ else if config.keyFile != null
+ then
+ lib.warn
+ ("The option `keyFile` is deprecated."
+ + "Use passwordFile instead if you want to use interactive login or settings.keyFile if you want to use key file login")
+ config.keyFile
+ else null;
+ keyFileArgs = ''
+ ${lib.optionalString (keyFile != null) "--key-file ${keyFile}"} \
+ ${lib.optionalString (lib.hasAttr "keyFileSize" config.settings) "--keyfile-size ${builtins.toString config.settings.keyFileSize}"} \
+ ${lib.optionalString (lib.hasAttr "keyFileOffset" config.settings) "--keyfile-offset ${builtins.toString config.settings.keyFileOffset}"} \
+ '';
+ cryptsetupOpen = ''
+ cryptsetup open "${config.device}" "${config.name}" \
+ ${lib.optionalString (config.settings.allowDiscards or false) "--allow-discards"} \
+ ${lib.optionalString (config.settings.bypassWorkqueues or false) "--perf-no_read_workqueue --perf-no_write_workqueue"} \
+ ${toString config.extraOpenArgs} \
+ ${keyFileArgs} \
+ '';
+in
+{
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "luks" ];
+ internal = true;
+ description = "Type";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ description = "Device to encrypt";
+ default = device;
+ };
+ name = lib.mkOption {
+ type = lib.types.str;
+ description = "Name of the LUKS";
+ };
+ keyFile = lib.mkOption {
+ type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
+ default = null;
+ description = "DEPRECATED use passwordFile or settings.keyFile. Path to the key for encryption";
+ example = "/tmp/disk.key";
+ };
+ passwordFile = lib.mkOption {
+ type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
+ default = null;
+ description = "Path to the file which contains the password for initial encryption";
+ example = "/tmp/disk.key";
+ };
+ askPassword = lib.mkOption {
+ type = lib.types.bool;
+ default = config.keyFile == null && config.passwordFile == null && (! config.settings ? "keyFile");
+ defaultText = "true if neither keyFile nor passwordFile are set";
+ description = "Whether to ask for a password for initial encryption";
+ };
+ settings = lib.mkOption {
+ type = lib.types.attrsOf lib.types.anything;
+ default = { };
+ description = "LUKS settings (as defined in configuration.nix in boot.initrd.luks.devices.)";
+ example = ''{
+ keyFile = "/tmp/disk.key";
+ keyFileSize = 2048;
+ keyFileOffset = 1024;
+ fallbackToPassword = true;
+ allowDiscards = true;
+ };
+ '';
+ };
+ additionalKeyFiles = lib.mkOption {
+ type = lib.types.listOf diskoLib.optionTypes.absolute-pathname;
+ default = [ ];
+ description = "Path to additional key files for encryption";
+ example = [ "/tmp/disk2.key" ];
+ };
+ initrdUnlock = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Whether to add a boot.initrd.luks.devices entry for the specified disk.";
+ };
+ extraFormatArgs = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Extra arguments to pass to `cryptsetup luksFormat` when formatting";
+ example = [ "--pbkdf argon2id" ];
+ };
+ extraOpenArgs = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Extra arguments to pass to `cryptsetup luksOpen` when opening";
+ example = [ "--timeout 10" ];
+ };
+ content = diskoLib.deviceType { parent = config; device = "/dev/mapper/${config.name}"; };
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = dev:
+ lib.optionalAttrs (config.content != null) (config.content._meta dev);
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ if ! blkid "${config.device}" >/dev/null || ! (blkid "${config.device}" -o export | grep -q '^TYPE='); then
+ ${lib.optionalString config.askPassword ''
+ askPassword() {
+ if [ -z ''${IN_DISKO_TEST+x} ]; then
+ set +x
+ echo "Enter password for ${config.device}: "
+ IFS= read -r -s password
+ echo "Enter password for ${config.device} again to be safe: "
+ IFS= read -r -s password_check
+ export password
+ [ "$password" = "$password_check" ]
+ set -x
+ else
+ export password=disko
+ fi
+ }
+ until askPassword; do
+ echo "Passwords did not match, please try again."
+ done
+ ''}
+ cryptsetup -q luksFormat "${config.device}" ${toString config.extraFormatArgs} ${keyFileArgs}
+ ${cryptsetupOpen} --persistent
+ ${toString (lib.forEach config.additionalKeyFiles (keyFile: ''
+ cryptsetup luksAddKey "${config.device}" ${keyFile} ${keyFileArgs}
+ ''))}
+ fi
+ ${lib.optionalString (config.content != null) config.content._create}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default =
+ let
+ contentMount = config.content._mount;
+ in
+ {
+ dev = ''
+ if ! cryptsetup status "${config.name}" >/dev/null 2>/dev/null; then
+ ${lib.optionalString config.askPassword ''
+ if [ -z ''${IN_DISKO_TEST+x} ]; then
+ set +x
+ echo "Enter password for ${config.device}"
+ IFS= read -r -s password
+ export password
+ set -x
+ else
+ export password=disko
+ fi
+ ''}
+ ${cryptsetupOpen}
+ fi
+ ${lib.optionalString (config.content != null) contentMount.dev or ""}
+ '';
+ fs = lib.optionalAttrs (config.content != null) contentMount.fs or { };
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default =
+ let
+ contentUnmount = config.content._unmount;
+ in
+ {
+ dev = ''
+ ${lib.optionalString (config.content != null) contentUnmount.dev or ""}
+ if cryptsetup status "${config.name}" >/dev/null 2>/dev/null; then
+ cryptsetup close "${config.name}"
+ fi
+ '';
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = [ ]
+ # If initrdUnlock is true, then add a device entry to the initrd.luks.devices config.
+ ++ (lib.optional config.initrdUnlock [
+ {
+ boot.initrd.luks.devices.${config.name} = {
+ inherit (config) device;
+ } // config.settings;
+ }
+ ]) ++ (lib.optional (config.content != null) config.content._config);
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [ pkgs.gnugrep pkgs.cryptsetup ] ++ (lib.optionals (config.content != null) (config.content._pkgs pkgs));
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/lvm_pv.nix b/pkgs/disko/lib/types/lvm_pv.nix
new file mode 100644
index 0000000..6b0ae41
--- /dev/null
+++ b/pkgs/disko/lib/types/lvm_pv.nix
@@ -0,0 +1,62 @@
+{ config, options, lib, diskoLib, parent, device, ... }:
+{
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "lvm_pv" ];
+ internal = true;
+ description = "Type";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ description = "Device";
+ default = device;
+ };
+ vg = lib.mkOption {
+ type = lib.types.str;
+ description = "Volume group";
+ };
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = dev: {
+ deviceDependencies.lvm_vg.${config.vg} = [ dev ];
+ };
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ if ! (blkid "${config.device}" | grep -q 'TYPE='); then
+ pvcreate "${config.device}"
+ fi
+ echo "${config.device}" >>"$disko_devices_dir"/lvm_${lib.escapeShellArg config.vg}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default = { };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = { };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = [ ];
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [ pkgs.gnugrep pkgs.lvm2 ];
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/lvm_vg.nix b/pkgs/disko/lib/types/lvm_vg.nix
new file mode 100644
index 0000000..b396fee
--- /dev/null
+++ b/pkgs/disko/lib/types/lvm_vg.nix
@@ -0,0 +1,183 @@
+{ config, options, lib, diskoLib, ... }:
+let
+ # Load kernel modules to ensure device mapper types are available
+ kernelModules =
+ [
+ # Prevent unbootable systems if LVM snapshots are present at boot time.
+ "dm-snapshot"
+ ] ++
+ lib.filter (x: x != "") (map
+ (lv: lib.optionalString (lv.lvm_type != null && lv.lvm_type != "thinlv") "dm-${lv.lvm_type}")
+ (lib.attrValues config.lvs));
+in
+{
+ options = {
+ name = lib.mkOption {
+ type = lib.types.str;
+ default = config._module.args.name;
+ description = "Name of the volume group";
+ };
+ type = lib.mkOption {
+ type = lib.types.enum [ "lvm_vg" ];
+ internal = true;
+ description = "Type";
+ };
+ lvs = lib.mkOption {
+ type = lib.types.attrsOf (lib.types.submodule ({ name, ... }@lv: {
+ options = {
+ name = lib.mkOption {
+ type = lib.types.str;
+ default = name;
+ description = "Name of the logical volume";
+ };
+ priority = lib.mkOption {
+ type = lib.types.int;
+ default = (if lv.config.lvm_type == "thin-pool" then 501 else 1000) + (if lib.hasInfix "100%" lv.config.size then 251 else 0);
+ defaultText = lib.literalExpression ''
+ if (lib.hasInfix "100%" lv.config.size) then 9001 else 1000
+ '';
+ description = "Priority of the logical volume, smaller values are created first";
+ };
+ size = lib.mkOption {
+ type = lib.types.str; # TODO lvm size type
+ description = "Size of the logical volume";
+ };
+ lvm_type = lib.mkOption {
+ # TODO: add raid10
+ type = lib.types.nullOr (lib.types.enum [ "mirror" "raid0" "raid1" "raid4" "raid5" "raid6" "thin-pool" "thinlv" ]); # TODO add all lib.types
+ default = null; # maybe there is always a default type?
+ description = "LVM type";
+ };
+ extraArgs = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Extra arguments";
+ };
+ pool = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Name of pool LV that this LV belongs to";
+ };
+ content = diskoLib.partitionType { parent = config; device = "/dev/${config.name}/${lv.config.name}"; };
+ };
+ }));
+ default = { };
+ description = "LVS for the volume group";
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = diskoLib.jsonType;
+ default =
+ diskoLib.deepMergeMap
+ (lv:
+ lib.optionalAttrs (lv.content != null) (lv.content._meta [ "lvm_vg" config.name ])
+ )
+ (lib.attrValues config.lvs);
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default =
+ let
+ sortedLvs = lib.sort (a: b: a.priority < b.priority) (lib.attrValues config.lvs);
+ in
+ ''
+ ${lib.concatMapStringsSep "\n" (k: ''modprobe "${k}"'') kernelModules}
+ readarray -t lvm_devices < <(cat "$disko_devices_dir"/lvm_${lib.escapeShellArg config.name})
+ if ! vgdisplay "${config.name}" >/dev/null; then
+ vgcreate "${config.name}" \
+ "''${lvm_devices[@]}"
+ fi
+ ${lib.concatMapStrings (lv: ''
+ if ! lvdisplay "${config.name}/${lv.name}"; then
+ lvcreate \
+ --yes \
+ ${if (lv.lvm_type == "thinlv") then "-V"
+ else if lib.hasInfix "%" lv.size then "-l" else "-L"} \
+ ${if lib.hasSuffix "%" lv.size then "${lv.size}FREE" else lv.size} \
+ -n "${lv.name}" \
+ ${lib.optionalString (lv.lvm_type == "thinlv") "--thinpool=${lv.pool}"} \
+ ${lib.optionalString (lv.lvm_type != null && lv.lvm_type != "thinlv") "--type=${lv.lvm_type}"} \
+ ${toString lv.extraArgs} \
+ "${config.name}"
+ fi
+ '') sortedLvs}
+
+ ${lib.concatMapStrings (lv: ''
+ ${lib.optionalString (lv.content != null) lv.content._create}
+ '') sortedLvs}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default =
+ let
+ lvMounts = diskoLib.deepMergeMap
+ (lv:
+ lib.optionalAttrs (lv.content != null) lv.content._mount
+ )
+ (lib.attrValues config.lvs);
+ in
+ {
+ dev = ''
+ vgchange -a y
+ ${lib.concatMapStrings (x: x.dev or "") (lib.attrValues lvMounts)}
+ '';
+ fs = lvMounts.fs or { };
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default =
+ let
+ lvMounts = diskoLib.deepMergeMap
+ (lv:
+ lib.optionalAttrs (lv.content != null) lv.content._unmount
+ )
+ (lib.attrValues config.lvs);
+ in {
+ dev = ''
+ ${lib.concatMapStrings (x: x.dev or "") (lib.attrValues lvMounts)}
+ vgchange -a n
+ '';
+ fs = lvMounts.fs or { };
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = [{ boot.initrd.kernelModules = kernelModules; }] ++
+ map
+ (lv: [
+ (lib.optional (lv.content != null) lv.content._config)
+ (lib.optional (lv.lvm_type != null) {
+ boot.initrd.kernelModules = [
+ (if lv.lvm_type == "mirror" then "dm-mirror" else "dm-raid")
+ ]
+ ++ lib.optional (lv.lvm_type == "raid0") "raid0"
+ ++ lib.optional (lv.lvm_type == "raid1") "raid1"
+ # ++ lib.optional (lv.lvm_type == "raid10") "raid10"
+ ++ lib.optional
+ (lv.lvm_type == "raid4" ||
+ lv.lvm_type == "raid5" ||
+ lv.lvm_type == "raid6") "raid456";
+
+ })
+ ])
+ (lib.attrValues config.lvs);
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: lib.flatten (map
+ (lv:
+ lib.optional (lv.content != null) (lv.content._pkgs pkgs)
+ )
+ (lib.attrValues config.lvs));
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/mdadm.nix b/pkgs/disko/lib/types/mdadm.nix
new file mode 100644
index 0000000..f29d6a1
--- /dev/null
+++ b/pkgs/disko/lib/types/mdadm.nix
@@ -0,0 +1,99 @@
+{ config, options, lib, diskoLib, ... }:
+{
+ options = {
+ name = lib.mkOption {
+ type = lib.types.str;
+ default = config._module.args.name;
+ description = "Name";
+ };
+ type = lib.mkOption {
+ type = lib.types.enum [ "mdadm" ];
+ default = "mdadm";
+ internal = true;
+ description = "Type";
+ };
+ level = lib.mkOption {
+ type = lib.types.int;
+ default = 1;
+ description = "mdadm level";
+ };
+ metadata = lib.mkOption {
+ type = lib.types.enum [ "1" "1.0" "1.1" "1.2" "default" "ddf" "imsm" ];
+ default = "default";
+ description = "Metadata";
+ };
+ content = diskoLib.deviceType { parent = config; device = "/dev/md/${config.name}"; };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = diskoLib.jsonType;
+ default =
+ lib.optionalAttrs (config.content != null) (config.content._meta [ "mdadm" config.name ]);
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ if ! test -e "/dev/md/${config.name}"; then
+ readarray -t disk_devices < <(cat "$disko_devices_dir"/raid_${lib.escapeShellArg config.name})
+ echo 'y' | mdadm --create "/dev/md/${config.name}" \
+ --level=${toString config.level} \
+ --raid-devices="$(wc -l "$disko_devices_dir"/raid_${lib.escapeShellArg config.name} | cut -f 1 -d " ")" \
+ --metadata=${config.metadata} \
+ --force \
+ --homehost=any \
+ "''${disk_devices[@]}"
+ partprobe "/dev/md/${config.name}"
+ udevadm trigger --subsystem-match=block
+ udevadm settle
+ # for some reason mdadm devices spawn with an existing partition table, so we need to wipe it
+ sgdisk --zap-all "/dev/md/${config.name}"
+ fi
+ ${lib.optionalString (config.content != null) config.content._create}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default =
+ lib.optionalAttrs (config.content != null) config.content._mount;
+ # TODO we probably need to assemble the mdadm somehow
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = let
+ content = lib.optionalAttrs (config.content != null) config.content._unmount;
+ in {
+ fs = content.fs;
+ dev = ''
+ ${content.dev or ""}
+ if [ -e "/dev/md/${config.name}" ]; then
+ mdadm --stop "/dev/md/${config.name}"
+ fi
+ '';
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default =
+ [
+ (if lib.versionAtLeast (lib.versions.majorMinor lib.version) "23.11" then {
+ boot.swraid.enable = true;
+ } else {
+ boot.initrd.services.swraid.enable = true;
+ })
+ ] ++
+ lib.optional (config.content != null) config.content._config;
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [
+ pkgs.parted # for partprobe
+ ] ++ (lib.optionals (config.content != null) (config.content._pkgs pkgs));
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/mdraid.nix b/pkgs/disko/lib/types/mdraid.nix
new file mode 100644
index 0000000..b00626a
--- /dev/null
+++ b/pkgs/disko/lib/types/mdraid.nix
@@ -0,0 +1,60 @@
+{ config, options, lib, diskoLib, parent, device, ... }:
+{
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "mdraid" ];
+ internal = true;
+ description = "Type";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ description = "Device";
+ default = device;
+ };
+
+ name = lib.mkOption {
+ type = lib.types.str;
+ description = "Name";
+ };
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = dev: {
+ deviceDependencies.mdadm.${config.name} = [ dev ];
+ };
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ echo "${config.device}" >>"$disko_devices_dir"/raid_${lib.escapeShellArg config.name}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default = { };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = { };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = [ ];
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [ pkgs.mdadm ];
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/nodev.nix b/pkgs/disko/lib/types/nodev.nix
new file mode 100644
index 0000000..6a45865
--- /dev/null
+++ b/pkgs/disko/lib/types/nodev.nix
@@ -0,0 +1,82 @@
+{ lib, config, options, diskoLib, rootMountPoint, ... }:
+{
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "nodev" ];
+ default = "nodev";
+ internal = true;
+ description = "Device type";
+ };
+ fsType = lib.mkOption {
+ type = lib.types.str;
+ description = "File system type";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ default = "none";
+ description = "Device to use";
+ };
+ mountpoint = lib.mkOption {
+ type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
+ default = config._module.args.name;
+ description = "Location to mount the file system at";
+ };
+ mountOptions = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ "defaults" ];
+ description = "Options to pass to mount";
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = diskoLib.jsonType;
+ default = { };
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = "";
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default = lib.optionalAttrs (config.mountpoint != null) {
+ fs.${config.mountpoint} = ''
+ if ! findmnt ${config.fsType} "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then
+ mount -t ${config.fsType} "${config.device}" "${rootMountPoint}${config.mountpoint}" \
+ ${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \
+ -o X-mount.mkdir
+ fi
+ '';
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = lib.optionalAttrs (config.mountpoint != null) {
+ fs.${config.mountpoint} = ''
+ if findmnt ${config.fsType} "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then
+ umount "${rootMountPoint}${config.mountpoint}"
+ fi
+ '';
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = lib.optional (config.mountpoint != null) {
+ fileSystems.${config.mountpoint} = {
+ device = config.device;
+ fsType = config.fsType;
+ options = config.mountOptions;
+ };
+ };
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = _pkgs: [ ];
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/swap.nix b/pkgs/disko/lib/types/swap.nix
new file mode 100644
index 0000000..83a6a74
--- /dev/null
+++ b/pkgs/disko/lib/types/swap.nix
@@ -0,0 +1,134 @@
+{ diskoLib, config, options, lib, parent, device, ... }:
+{
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "swap" ];
+ internal = true;
+ description = "Type";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ default = device;
+ description = "Device";
+ };
+ discardPolicy = lib.mkOption {
+ default = null;
+ example = "once";
+ type = lib.types.nullOr (lib.types.enum [ "once" "pages" "both" ]);
+ description = ''
+ Specify the discard policy for the swap device. If "once", then the
+ whole swap space is discarded at swapon invocation. If "pages",
+ asynchronous discard on freed pages is performed, before returning to
+ the available pages pool. With "both", both policies are activated.
+ See swapon(8) for more information.
+ '';
+ };
+ extraArgs = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Extra arguments";
+ };
+ mountOptions = lib.mkOption {
+ type = lib.types.listOf lib.types.nonEmptyStr;
+ default = [ "defaults" ];
+ example = [ "nofail" ];
+ description = "Options used to mount the swap.";
+ };
+ priority = lib.mkOption {
+ type = lib.types.nullOr lib.types.int;
+ default = null;
+ description = ''
+ Specify the priority of the swap device. Priority is a value between 0 and 32767.
+ Higher numbers indicate higher priority.
+ null lets the kernel choose a priority, which will show up as a negative value.
+ '';
+ };
+ randomEncryption = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Whether to randomly encrypt the swap";
+ };
+ resumeDevice = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Whether to use this as a boot.resumeDevice";
+ };
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = _dev: { };
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ # TODO: we don't support encrypted swap yet
+ default = lib.optionalString (!config.randomEncryption) ''
+ if ! blkid "${config.device}" -o export | grep -q '^TYPE='; then
+ mkswap \
+ ${toString config.extraArgs} \
+ "${config.device}"
+ fi
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ # TODO: we don't support encrypted swap yet
+ default = lib.optionalAttrs (!config.randomEncryption) {
+ fs.${config.device} = ''
+ if test "''${DISKO_SKIP_SWAP:-}" != 1 && ! swapon --show | grep -q "^$(readlink -f "${config.device}") "; then
+ swapon ${
+ lib.optionalString (config.discardPolicy != null)
+ "--discard${lib.optionalString (config.discardPolicy != "both")
+ "=${config.discardPolicy}"
+ }"} ${
+ lib.optionalString (config.priority != null)
+ "--priority=${toString config.priority}"
+ } \
+ --options=${lib.concatStringsSep "," config.mountOptions} \
+ "${config.device}"
+ fi
+ '';
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = lib.optionalAttrs (!config.randomEncryption) {
+ fs.${config.device} = ''
+ if swapon --show | grep -q "^$(readlink -f "${config.device}") "; then
+ swapoff "${config.device}"
+ fi
+ '';
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = [{
+ swapDevices = [{
+ device = config.device;
+ inherit (config) discardPolicy priority;
+ randomEncryption = {
+ enable = config.randomEncryption;
+ # forward discard/TRIM attempts through dm-crypt
+ allowDiscards = config.discardPolicy != null;
+ };
+ options = config.mountOptions;
+ }];
+ boot.resumeDevice = lib.mkIf config.resumeDevice config.device;
+ }];
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [ pkgs.gnugrep pkgs.util-linux ];
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/table.nix b/pkgs/disko/lib/types/table.nix
new file mode 100644
index 0000000..6640da5
--- /dev/null
+++ b/pkgs/disko/lib/types/table.nix
@@ -0,0 +1,179 @@
+{ config, options, lib, diskoLib, parent, device, ... }:
+{
+ options = lib.warn ''
+ The legacy table is outdated and should not be used. We recommend using the gpt type instead.
+ Please note that certain features, such as the test framework, may not function properly with the legacy table type.
+ If you encounter errors similar to:
+ "error: The option `disko.devices.disk.disk1.content.partitions."[definition 1-entry 1]".content._config` is read-only, but it's set multiple times,"
+ this is likely due to the use of the legacy table type.
+ for a migration you can follow the guide at https://github.com/nix-community/disko/blob/master/docs/table-to-gpt.md
+ ''
+ {
+ type = lib.mkOption {
+ type = lib.types.enum [ "table" ];
+ internal = true;
+ description = "Partition table";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ default = device;
+ description = "Device to partition";
+ };
+ format = lib.mkOption {
+ type = lib.types.enum [ "gpt" "msdos" ];
+ default = "gpt";
+ description = "The kind of partition table";
+ };
+ partitions = lib.mkOption {
+ type = lib.types.listOf (lib.types.submodule ({ name, ... }@partition: {
+ options = {
+ part-type = lib.mkOption {
+ type = lib.types.enum [ "primary" "logical" "extended" ];
+ default = "primary";
+ description = "Partition type";
+ };
+ fs-type = lib.mkOption {
+ type = lib.types.nullOr (lib.types.enum [ "btrfs" "ext2" "ext3" "ext4" "fat16" "fat32" "hfs" "hfs+" "linux-swap" "ntfs" "reiserfs" "udf" "xfs" ]);
+ default = null;
+ description = "Filesystem type to use";
+ };
+ name = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ description = "Name of the partition";
+ };
+ start = lib.mkOption {
+ type = lib.types.str;
+ default = "0%";
+ description = "Start of the partition";
+ };
+ end = lib.mkOption {
+ type = lib.types.str;
+ default = "100%";
+ description = "End of the partition";
+ };
+ flags = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Partition flags";
+ };
+ bootable = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Whether to make the partition bootable";
+ };
+ content = diskoLib.partitionType { parent = config; device = diskoLib.deviceNumbering config.device partition.config._index; };
+ _index = lib.mkOption {
+ type = lib.types.int;
+ internal = true;
+ default = lib.toInt (lib.head (builtins.match ".*entry ([[:digit:]]+)]" name));
+ defaultText = null;
+ };
+ };
+ }));
+ default = [ ];
+ description = "List of partitions to add to the partition table";
+ };
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = dev:
+ lib.foldr lib.recursiveUpdate { } (lib.imap
+ (_index: partition:
+ lib.optionalAttrs (partition.content != null) (partition.content._meta dev)
+ )
+ config.partitions);
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ if ! blkid "${config.device}" >/dev/null; then
+ parted -s "${config.device}" -- mklabel ${config.format}
+ ${lib.concatStrings (map (partition: ''
+ ${lib.optionalString (config.format == "gpt") ''
+ parted -s "${config.device}" -- mkpart "${diskoLib.hexEscapeUdevSymlink partition.name}" ${diskoLib.maybeStr partition.fs-type} ${partition.start} ${partition.end}
+ ''}
+ ${lib.optionalString (config.format == "msdos") ''
+ parted -s "${config.device}" -- mkpart ${partition.part-type} ${diskoLib.maybeStr partition.fs-type} ${partition.start} ${partition.end}
+ ''}
+ # ensure /dev/disk/by-path/..-partN exists before continuing
+ partprobe "${config.device}"
+ udevadm trigger --subsystem-match=block
+ udevadm settle
+ ${lib.optionalString partition.bootable ''
+ parted -s "${config.device}" -- set ${toString partition._index} boot on
+ ''}
+ ${lib.concatMapStringsSep "" (flag: ''
+ parted -s "${config.device}" -- set ${toString partition._index} ${flag} on
+ '') partition.flags}
+ # ensure further operations can detect new partitions
+ partprobe "${config.device}"
+ udevadm trigger --subsystem-match=block
+ udevadm settle
+ '') config.partitions)}
+ fi
+ ${lib.concatStrings (map (partition: ''
+ ${lib.optionalString (partition.content != null) partition.content._create}
+ '') config.partitions)}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default =
+ let
+ partMounts = lib.foldr lib.recursiveUpdate { } (map
+ (partition:
+ lib.optionalAttrs (partition.content != null) partition.content._mount
+ )
+ config.partitions);
+ in
+ {
+ dev = partMounts.dev or "";
+ fs = partMounts.fs or { };
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default =
+ let
+ partMounts = lib.foldr lib.recursiveUpdate { } (map
+ (partition:
+ lib.optionalAttrs (partition.content != null) partition.content._unmount
+ )
+ config.partitions);
+ in
+ {
+ dev = partMounts.dev or "";
+ fs = partMounts.fs or { };
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default =
+ map
+ (partition:
+ lib.optional (partition.content != null) partition.content._config
+ )
+ config.partitions;
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs:
+ [ pkgs.parted pkgs.systemdMinimal ] ++ lib.flatten (map
+ (partition:
+ lib.optional (partition.content != null) (partition.content._pkgs pkgs)
+ )
+ config.partitions);
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/zfs.nix b/pkgs/disko/lib/types/zfs.nix
new file mode 100644
index 0000000..f9e9778
--- /dev/null
+++ b/pkgs/disko/lib/types/zfs.nix
@@ -0,0 +1,59 @@
+{ config, options, lib, diskoLib, parent, device, ... }:
+{
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "zfs" ];
+ internal = true;
+ description = "Type";
+ };
+ device = lib.mkOption {
+ type = lib.types.str;
+ default = device;
+ description = "Device";
+ };
+ pool = lib.mkOption {
+ type = lib.types.str;
+ description = "Name of the ZFS pool";
+ };
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = dev: {
+ deviceDependencies.zpool.${config.pool} = [ dev ];
+ };
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ echo "${config.device}" >>"$disko_devices_dir"/zfs_${lib.escapeShellArg config.pool}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default = { };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = { };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = [ ];
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [ pkgs.zfs ];
+ description = "Packages";
+ };
+ };
+}
diff --git a/pkgs/disko/lib/types/zfs_fs.nix b/pkgs/disko/lib/types/zfs_fs.nix
new file mode 100644
index 0000000..1399ce9
--- /dev/null
+++ b/pkgs/disko/lib/types/zfs_fs.nix
@@ -0,0 +1,160 @@
+{ config, options, lib, diskoLib, rootMountPoint, parent, ... }:
+{
+ options = {
+ name = lib.mkOption {
+ type = lib.types.str;
+ default = config._module.args.name;
+ description = "Name of the dataset";
+ };
+
+ _name = lib.mkOption {
+ type = lib.types.str;
+ default = "${config._parent.name}/${config.name}";
+ internal = true;
+ description = "Fully quantified name for dataset";
+ };
+
+ type = lib.mkOption {
+ type = lib.types.enum [ "zfs_fs" ];
+ default = "zfs_fs";
+ internal = true;
+ description = "Type";
+ };
+
+ options = lib.mkOption {
+ type = lib.types.attrsOf lib.types.str;
+ default = { };
+ description = "Options to set for the dataset";
+ };
+
+ mountOptions = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ "defaults" ];
+ description = "Mount options";
+ };
+
+ mountpoint = lib.mkOption {
+ type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
+ default = null;
+ description = "Path to mount the dataset to";
+ };
+
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = _dev: { };
+ description = "Metadata";
+ };
+
+ _createFilesystem = lib.mkOption {
+ internal = true;
+ type = lib.types.bool;
+ default = true;
+ };
+
+ _create = diskoLib.mkCreateOption
+ {
+ inherit config options;
+ # -u prevents mounting newly created datasets, which is
+ # important to prevent accidental shadowing of mount points
+ # since (create order != mount order)
+ # -p creates parents automatically
+ default =
+ let
+ createOptions = (lib.optionalAttrs (config.mountpoint != null) { mountpoint = config.mountpoint; }) // config.options;
+ # All options defined as PROP_ONETIME or PROP_ONETIME_DEFAULT in https://github.com/openzfs/zfs/blob/master/module/zcommon/zfs_prop.c
+ onetimeProperties = [
+ "encryption"
+ "casesensitivity"
+ "utf8only"
+ "normalization"
+ "volblocksize"
+ "pbkdf2iters"
+ "pbkdf2salt"
+ "keyformat"
+ ];
+ updateOptions = builtins.removeAttrs createOptions onetimeProperties;
+ in
+ ''
+ if ! zfs get type ${config._name} >/dev/null 2>&1; then
+ ${if config._createFilesystem then ''
+ zfs create -up ${config._name} \
+ ${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-o ${n}=${v}") (createOptions))}
+ '' else ''
+ # don't create anything for root dataset of zpools
+ true
+ ''}
+ ${lib.optionalString (updateOptions != {}) ''
+ else
+ zfs set -u ${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${v}") updateOptions)} ${config._name}
+ ''}
+ fi
+ '';
+ };
+
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default =
+ (lib.optionalAttrs (config.options.keylocation or "none" != "none") {
+ dev = ''
+ if [ "$(zfs get keystatus ${config._name} -H -o value)" == "unavailable" ]; then
+ zfs load-key ${config._name}
+ fi
+ '';
+ }) // lib.optionalAttrs (config.options.mountpoint or "" != "none" && config.options.canmount or "" != "off") {
+ fs.${config.mountpoint} = ''
+ if ! findmnt ${config._name} "${rootMountPoint}${config.mountpoint}" >/dev/null 2>&1; then
+ mount ${config._name} "${rootMountPoint}${config.mountpoint}" \
+ -o X-mount.mkdir \
+ ${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \
+ ${lib.optionalString ((config.options.mountpoint or "") != "legacy") "-o zfsutil"} \
+ -t zfs
+ fi
+ '';
+ };
+ };
+
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default =
+ (lib.optionalAttrs (config.options.keylocation or "none" != "none") {
+ dev = "zfs unload-key ${config.name}";
+ }) // lib.optionalAttrs (config.options.mountpoint or "" != "none" && config.options.canmount or "" != "off") {
+ fs.${config.mountpoint} = ''
+ if findmnt ${config._name} "${rootMountPoint}${config.mountpoint}" >/dev/null 2>&1; then
+ umount "${rootMountPoint}${config.mountpoint}"
+ fi
+ '';
+ };
+ };
+
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default =
+ lib.optional (config.options.mountpoint or "" != "none" && config.options.canmount or "" != "off") {
+ fileSystems.${config.mountpoint} = {
+ device = "${config._name}";
+ fsType = "zfs";
+ options = config.mountOptions ++ lib.optional ((config.options.mountpoint or "") != "legacy") "zfsutil";
+ };
+ };
+ description = "NixOS configuration";
+ };
+
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [ pkgs.util-linux ];
+ description = "Packages";
+ };
+ };
+}
+
diff --git a/pkgs/disko/lib/types/zfs_volume.nix b/pkgs/disko/lib/types/zfs_volume.nix
new file mode 100644
index 0000000..e4c1e90
--- /dev/null
+++ b/pkgs/disko/lib/types/zfs_volume.nix
@@ -0,0 +1,113 @@
+{ config, options, lib, diskoLib, parent, ... }:
+{
+ options = {
+ name = lib.mkOption {
+ type = lib.types.str;
+ default = config._module.args.name;
+ description = "Name of the dataset";
+ };
+ type = lib.mkOption {
+ type = lib.types.enum [ "zfs_volume" ];
+ default = "zfs_volume";
+ internal = true;
+ description = "Type";
+ };
+ options = lib.mkOption {
+ type = lib.types.attrsOf lib.types.str;
+ default = { };
+ description = "Options to set for the dataset";
+ };
+ extraArgs = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Extra arguments passed to `zfs create`";
+ };
+ mountOptions = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ "defaults" ];
+ description = "Mount options";
+ };
+
+ # volume options
+ size = lib.mkOption {
+ type = lib.types.nullOr lib.types.str; # TODO size
+ default = null;
+ description = "Size of the dataset";
+ };
+
+ content = diskoLib.partitionType { parent = config; device = "/dev/zvol/${config._parent.name}/${config.name}"; };
+
+ _parent = lib.mkOption {
+ internal = true;
+ default = parent;
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo diskoLib.jsonType;
+ default = dev:
+ lib.optionalAttrs (config.content != null) (config.content._meta dev);
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default = ''
+ if ! zfs get type "${config._parent.name}/${config.name}" >/dev/null 2>&1; then
+ zfs create "${config._parent.name}/${config.name}" \
+ ${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-o ${n}=${v}") config.options)} \
+ -V ${config.size} ${toString (builtins.map lib.escapeShellArg config.extraArgs)}
+ zvol_wait
+ partprobe "/dev/zvol/${config._parent.name}/${config.name}"
+ udevadm trigger --subsystem-match=block
+ udevadm settle
+ fi
+ ${lib.optionalString (config.content != null) config.content._create}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default = {
+ dev = ''
+ ${lib.optionalString (config.options.keylocation or "none" != "none") ''
+ if [ "$(zfs get keystatus ${config.name} -H -o value)" == "unavailable" ]; then
+ zfs load-key ${config.name}
+ fi
+ ''}
+
+ ${config.content._mount.dev or ""}
+ '';
+ fs = config.content._mount.fs or { };
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = {
+ dev = ''
+ ${lib.optionalString (config.options.keylocation or "none" != "none") "zfs unload-key ${config.name}"}
+
+ ${config.content._unmount.dev or ""}
+ '';
+
+ fs = config.content._unmount.fs;
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default =
+ lib.optional (config.content != null) config.content._config;
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [
+ pkgs.util-linux
+ pkgs.parted # for partprobe
+ ] ++ lib.optionals (config.content != null) (config.content._pkgs pkgs);
+ description = "Packages";
+ };
+ };
+}
+
diff --git a/pkgs/disko/lib/types/zpool.nix b/pkgs/disko/lib/types/zpool.nix
new file mode 100644
index 0000000..8034048
--- /dev/null
+++ b/pkgs/disko/lib/types/zpool.nix
@@ -0,0 +1,390 @@
+{ config, options, lib, diskoLib, rootMountPoint, ... }:
+let
+ # TODO: Consider expanding to handle `file` and `draid` mode options.
+ modeOptions = [
+ ""
+ "mirror"
+ "raidz"
+ "raidz1"
+ "raidz2"
+ "raidz3"
+ ];
+in
+{
+ options = {
+ name = lib.mkOption {
+ type = lib.types.str;
+ default = config._module.args.name;
+ description = "Name of the ZFS pool";
+ };
+ type = lib.mkOption {
+ type = lib.types.enum [ "zpool" ];
+ default = "zpool";
+ internal = true;
+ description = "Type";
+ };
+ mode = lib.mkOption {
+ default = "";
+ example = {
+ mode = {
+ topology = {
+ type = "topology";
+ vdev = [
+ {
+ # Members can be either specified by a full path or by a disk name
+ # This is example uses the full path
+ members = [ "/dev/disk/by-id/wwn-0x5000c500af8b2a14" ];
+ }
+ ];
+ log = [
+ {
+ # Example using gpt partition labels
+ # This expects an disk called `ssd` with a gpt partition called `zfs`
+ # disko.devices.disk.ssd = {
+ # type = "disk";
+ # device = "/dev/nvme0n1";
+ # content = {
+ # type = "gpt";
+ # partitions = {
+ # zfs = {
+ # size = "100%";
+ # content = {
+ # type = "zfs";
+ # # use your own pool name here
+ # pool = "zroot";
+ # };
+ # };
+ # };
+ # };
+ # };
+ members = [ "ssd" ];
+ }
+ ];
+ };
+ };
+ };
+ type = (lib.types.oneOf [
+ (lib.types.enum modeOptions)
+ (lib.types.attrsOf (diskoLib.subType {
+ types = {
+ topology =
+ let
+ vdev = lib.types.submodule ({ ... }: {
+ options = {
+ mode = lib.mkOption {
+ type = lib.types.enum modeOptions;
+ default = "";
+ description = "Mode of the zfs vdev";
+ };
+ members = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ description = "Members of the vdev";
+ };
+ };
+ });
+ in
+ lib.types.submodule
+ ({ ... }: {
+ options = {
+ type = lib.mkOption {
+ type = lib.types.enum [ "topology" ];
+ default = "topology";
+ internal = true;
+ description = "Type";
+ };
+ # zfs device types
+ vdev = lib.mkOption {
+ type = lib.types.listOf vdev;
+ default = [ ];
+ description = ''
+ A list of storage vdevs. See
+ https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Virtual_Devices_(vdevs)
+ for details.
+ '';
+ example = [
+ {
+ mode = "mirror";
+ members = [ "x" "y" ];
+ }
+ {
+ members = [ "z" ];
+ }
+ ];
+ };
+ spare = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = ''
+ A list of devices to use as hot spares. See
+ https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Hot_Spares
+ for details.
+ '';
+ example = [ "x" "y" ];
+ };
+ log = lib.mkOption {
+ type = lib.types.listOf vdev;
+ default = [ ];
+ description = ''
+ A list of vdevs used for the zfs intent log (ZIL). See
+ https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Intent_Log
+ for details.
+ '';
+ example = [
+ {
+ mode = "mirror";
+ members = [ "x" "y" ];
+ }
+ {
+ members = [ "z" ];
+ }
+ ];
+ };
+ dedup = lib.mkOption {
+ type = lib.types.listOf vdev;
+ default = [ ];
+ description = ''
+ A list of vdevs used for the deduplication table. See
+ https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#dedup
+ for details.
+ '';
+ example = [
+ {
+ mode = "mirror";
+ members = [ "x" "y" ];
+ }
+ {
+ members = [ "z" ];
+ }
+ ];
+ };
+ special = lib.mkOption {
+ type = lib.types.either (lib.types.listOf vdev) (lib.types.nullOr vdev);
+ default = [ ];
+ description = ''
+ A list of vdevs used as special devices. See
+ https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#special
+ for details.
+ '';
+ example = [
+ {
+ mode = "mirror";
+ members = [ "x" "y" ];
+ }
+ {
+ members = [ "z" ];
+ }
+ ];
+ };
+ cache = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = ''
+ A dedicated zfs cache device (L2ARC). See
+ https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Cache_Devices
+ for details.
+ '';
+ example = [ "x" "y" ];
+ };
+ };
+ });
+ };
+ extraArgs.parent = config;
+ }))
+ ]);
+ description = "Mode of the ZFS pool";
+ };
+ options = lib.mkOption {
+ type = lib.types.attrsOf lib.types.str;
+ default = { };
+ description = "Options for the ZFS pool";
+ };
+ rootFsOptions = lib.mkOption {
+ type = lib.types.attrsOf lib.types.str;
+ default = { };
+ description = "Options for the root filesystem";
+ };
+ mountpoint = lib.mkOption {
+ type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
+ default = null;
+ description = "The mountpoint of the pool";
+ };
+ mountOptions = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ "defaults" ];
+ description = "Options to pass to mount";
+ };
+ datasets = lib.mkOption {
+ type = lib.types.attrsOf (diskoLib.subType {
+ types = { inherit (diskoLib.types) zfs_fs zfs_volume; };
+ extraArgs.parent = config;
+ });
+ description = "List of datasets to define";
+ };
+ _meta = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = diskoLib.jsonType;
+ default =
+ diskoLib.deepMergeMap (dataset: dataset._meta [ "zpool" config.name ]) (lib.attrValues config.datasets);
+ description = "Metadata";
+ };
+ _create = diskoLib.mkCreateOption {
+ inherit config options;
+ default =
+ let
+ formatOutput = type: mode: members: ''
+ entries+=("${type} ${mode}=${
+ lib.concatMapStringsSep " "
+ (d: if lib.strings.hasPrefix "/" d then d else "/dev/disk/by-partlabel/disk-${d}-zfs") members
+ }")
+ '';
+ formatVdev = type: vdev: formatOutput type vdev.mode vdev.members;
+ formatVdevList = type: vdevs: lib.concatMapStrings
+ (formatVdev type)
+ (builtins.sort (a: _: a.mode == "") vdevs);
+ hasTopology = !(builtins.isString config.mode);
+ mode = if hasTopology then "prescribed" else config.mode;
+ topology = lib.optionalAttrs hasTopology config.mode.topology;
+ in
+ ''
+ readarray -t zfs_devices < <(cat "$disko_devices_dir/zfs_${config.name}")
+ if [ ''${#zfs_devices[@]} -eq 0 ]; then
+ echo "no devices found for zpool ${config.name}. Did you misspell the pool name?" >&2
+ exit 1
+ fi
+ # Try importing the pool without mounting anything if it exists.
+ # This allows us to set mounpoints.
+ if zpool import -N -f "${config.name}" || zpool list "${config.name}"; then
+ echo "not creating zpool ${config.name} as a pool with that name already exists" >&2
+ else
+ continue=1
+ for dev in "''${zfs_devices[@]}"; do
+ if ! blkid "$dev" >/dev/null; then
+ # blkid fails, so device seems empty
+ :
+ elif (blkid "$dev" -o export | grep '^PTUUID='); then
+ echo "device $dev already has a partuuid, skipping creating zpool ${config.name}" >&2
+ continue=0
+ elif (blkid "$dev" -o export | grep '^TYPE=zfs_member'); then
+ # zfs_member is a zfs partition, so we try to add the device to the pool
+ :
+ elif (blkid "$dev" -o export | grep '^TYPE='); then
+ echo "device $dev already has a partition, skipping creating zpool ${config.name}" >&2
+ continue=0
+ fi
+ done
+ if [ $continue -eq 1 ]; then
+ topology=""
+ # For shell check
+ mode="${mode}"
+ if [ "$mode" != "prescribed" ]; then
+ topology="${mode} ''${zfs_devices[*]}"
+ else
+ entries=()
+ ${lib.optionalString (hasTopology && topology.vdev != null)
+ (formatVdevList "" topology.vdev)}
+ ${lib.optionalString (hasTopology && topology.spare != [])
+ (formatOutput "spare" "" topology.spare)}
+ ${lib.optionalString (hasTopology && topology.log != [])
+ (formatVdevList "log" topology.log)}
+ ${lib.optionalString (hasTopology && topology.dedup != [])
+ (formatVdevList "dedup" topology.dedup)}
+ ${lib.optionalString (hasTopology && topology.special != null && topology.special != [])
+ (formatVdevList "special" (lib.lists.toList topology.special))}
+ ${lib.optionalString (hasTopology && topology.cache != [])
+ (formatOutput "cache" "" topology.cache)}
+ all_devices=()
+ last_type=
+ for line in "''${entries[@]}"; do
+ # lineformat is type mode=device1 device2 device3
+ mode="''${line%%=*}"
+ type="''${mode%% *}"
+ mode="''${mode#"$type "}"
+ devs="''${line#*=}"
+ IFS=' ' read -r -a devices <<< "$devs"
+ all_devices+=("''${devices[@]}")
+ if ! [ "$type" = "$last_type" ]; then
+ topology+=" $type"
+ last_type="$type"
+ fi
+ topology+=" ''${mode} ''${devices[*]}"
+ done
+ # all_devices sorted should equal zfs_devices sorted
+ all_devices_list=$(echo "''${all_devices[*]}" | tr ' ' '\n' | sort)
+ zfs_devices_list=$(echo "''${zfs_devices[*]}" | tr ' ' '\n' | sort)
+ if [[ "$all_devices_list" != "$zfs_devices_list" ]]; then
+ echo "not all disks accounted for, skipping creating zpool ${config.name}" >&2
+ diff <(echo "$all_devices_list" ) <(echo "$zfs_devices_list") >&2
+ continue=0
+ fi
+ fi
+ fi
+ if [ $continue -eq 1 ]; then
+ zpool create -f "${config.name}" \
+ -R ${rootMountPoint} \
+ ${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-o ${n}=${v}") config.options)} \
+ ${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-O ${n}=${v}") config.rootFsOptions)} \
+ ''${topology:+ $topology}
+ if [[ $(zfs get -H mounted "${config.name}" | cut -f3) == "yes" ]]; then
+ zfs unmount "${config.name}"
+ fi
+ fi
+ fi
+ ${lib.concatMapStrings (dataset: dataset._create) (lib.attrValues config.datasets)}
+ '';
+ };
+ _mount = diskoLib.mkMountOption {
+ inherit config options;
+ default =
+ let
+ datasetFilesystemsMounts = diskoLib.deepMergeMap (dataset: dataset._mount.fs or {}) (lib.attrValues config.datasets);
+ in
+ {
+ dev = ''
+ zpool list "${config.name}" >/dev/null 2>/dev/null ||
+ zpool import -l -R ${rootMountPoint} "${config.name}"
+
+ ${lib.concatMapStrings (x: x._mount.dev or "") (lib.attrValues config.datasets)}
+ '';
+ fs = datasetFilesystemsMounts;
+ };
+ };
+ _unmount = diskoLib.mkUnmountOption {
+ inherit config options;
+ default = {
+ dev = ''
+ ${lib.concatMapStrings (dataset: dataset._unmount.dev or "") (lib.attrValues config.datasets)}
+
+ if zpool list "${config.name}" >/dev/null 2>/dev/null; then
+ zpool export "${config.name}"
+ fi
+ '';
+ fs = diskoLib.deepMergeMap (dataset: dataset._unmount.fs or {}) (lib.attrValues config.datasets);
+ };
+ };
+ _config = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ default = map (dataset: dataset._config) (lib.attrValues config.datasets);
+ description = "NixOS configuration";
+ };
+ _pkgs = lib.mkOption {
+ internal = true;
+ readOnly = true;
+ type = lib.types.functionTo (lib.types.listOf lib.types.package);
+ default = pkgs: [ pkgs.gnugrep pkgs.util-linux ] ++ lib.flatten (map (dataset: dataset._pkgs pkgs) (lib.attrValues config.datasets));
+ description = "Packages";
+ };
+ };
+
+ config = {
+ datasets."__root" = {
+ _name = config.name;
+ _createFilesystem = false;
+ type = "zfs_fs";
+ mountpoint = config.mountpoint;
+ options = config.rootFsOptions;
+ mountOptions = config.mountOptions;
+ };
+ };
+}
diff --git a/pkgs/disko/module.nix b/pkgs/disko/module.nix
new file mode 100644
index 0000000..6543e11
--- /dev/null
+++ b/pkgs/disko/module.nix
@@ -0,0 +1,266 @@
+{ config, lib, pkgs, extendModules, diskoLib, ... }:
+let
+ cfg = config.disko;
+
+ vmVariantWithDisko = extendModules {
+ modules = [
+ ./lib/interactive-vm.nix
+ config.disko.tests.extraConfig
+ ];
+ };
+in
+{
+ imports = [ ./lib/make-disk-image.nix ];
+
+ options.disko = {
+ imageBuilder = {
+ qemu = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ description = ''
+ the qemu emulator string used when building disk images via make-disk-image.nix.
+ Useful when using binfmt on your build host, and wanting to build disk
+ images for a foreign architecture
+ '';
+ default = null;
+ example = lib.literalExpression "\${pkgs.qemu_kvm}/bin/qemu-system-aarch64";
+ };
+
+ pkgs = lib.mkOption {
+ type = lib.types.attrs;
+ description = ''
+ the pkgs instance used when building disk images via make-disk-image.nix.
+ Useful when the config's kernel won't boot in the image-builder.
+ '';
+ default = pkgs;
+ defaultText = lib.literalExpression "pkgs";
+ example = lib.literalExpression "pkgs";
+ };
+
+ kernelPackages = lib.mkOption {
+ type = lib.types.attrs;
+ description = ''
+ the kernel used when building disk images via make-disk-image.nix.
+ Useful when the config's kernel won't boot in the image-builder.
+ '';
+ default = config.boot.kernelPackages;
+ defaultText = lib.literalExpression "config.boot.kernelPackages";
+ example = lib.literalExpression "pkgs.linuxPackages_testing";
+ };
+
+ extraRootModules = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ description = ''
+ extra kernel modules to pass to the vmTools.runCommand invocation in the make-disk-image.nix builder
+ '';
+ default = [ ];
+ example = [ "bcachefs" ];
+ };
+
+ extraPostVM = lib.mkOption {
+ type = lib.types.lines;
+ description = ''
+ extra shell code to execute once the disk image(s) have been succesfully created and moved to $out
+ '';
+ default = "";
+ example = lib.literalExpression ''
+ ''${pkgs.zstd}/bin/zstd --compress $out/*raw
+ rm $out/*raw
+ '';
+ };
+
+ extraDependencies = lib.mkOption {
+ type = lib.types.listOf lib.types.package;
+ description = ''
+ list of extra packages to make available in the make-disk-image.nix VM builder, an example might be f2fs-tools
+ '';
+ default = [ ];
+ };
+
+ name = lib.mkOption {
+ type = lib.types.str;
+ description = "name for the disk images";
+ default = "${config.networking.hostName}-disko-images";
+ defaultText = "\${config.networking.hostName}-disko-images";
+ };
+
+ copyNixStore = lib.mkOption {
+ type = lib.types.bool;
+ description = "whether to copy the nix store into the disk images we just created";
+ default = true;
+ };
+
+ extraConfig = lib.mkOption {
+ description = ''
+ Extra NixOS config for your test. Can be used to specify a different luks key for tests.
+ A dummy key is in /tmp/secret.key
+ '';
+ default = { };
+ };
+
+ imageFormat = lib.mkOption {
+ type = lib.types.enum [ "raw" "qcow2" ];
+ description = "QEMU image format to use for the disk images";
+ default = "raw";
+ };
+ };
+
+ memSize = lib.mkOption {
+ type = lib.types.int;
+ description = ''
+ size of the memory passed to runInLinuxVM, in megabytes
+ '';
+ default = 1024;
+ };
+
+ devices = lib.mkOption {
+ type = diskoLib.toplevel;
+ default = { };
+ description = "The devices to set up";
+ };
+
+ rootMountPoint = lib.mkOption {
+ type = lib.types.str;
+ default = "/mnt";
+ description = "Where the device tree should be mounted by the mountScript";
+ };
+
+ enableConfig = lib.mkOption {
+ description = ''
+ configure nixos with the specified devices
+ should be true if the system is booted with those devices
+ should be false on an installer image etc.
+ '';
+ type = lib.types.bool;
+ default = true;
+ };
+
+ checkScripts = lib.mkOption {
+ description = ''
+ Whether to run shellcheck on script outputs
+ '';
+ type = lib.types.bool;
+ default = false;
+ };
+
+ testMode = lib.mkOption {
+ internal = true;
+ description = ''
+ this is true if the system is being run in test mode.
+ like a vm test or an interactive vm
+ '';
+ type = lib.types.bool;
+ default = false;
+ };
+
+ tests = {
+ bootCommands = lib.mkOption {
+ description = ''
+ NixOS test script commands to run after the machine has started. Can
+ be used to enter an interactive password.
+ '';
+ type = lib.types.lines;
+ default = "";
+ };
+
+ efi = lib.mkOption {
+ description = ''
+ Whether efi is enabled for the `system.build.installTest`.
+ We try to automatically detect efi based on the configured bootloader.
+ '';
+ type = lib.types.bool;
+ defaultText = "config.boot.loader.systemd-boot.enable || config.boot.loader.grub.efiSupport";
+ default = config.boot.loader.systemd-boot.enable || config.boot.loader.grub.efiSupport;
+ };
+
+ enableOCR = lib.mkOption {
+ description = ''
+ Sets the enableOCR option in the NixOS VM test driver.
+ '';
+ type = lib.types.bool;
+ default = false;
+ };
+
+ extraChecks = lib.mkOption {
+ description = ''
+ extra checks to run in the `system.build.installTest`.
+ '';
+ type = lib.types.lines;
+ default = "";
+ example = ''
+ machine.succeed("test -e /var/secrets/my.secret")
+ '';
+ };
+
+ extraConfig = lib.mkOption {
+ description = ''
+ Extra NixOS config for your test. Can be used to specify a different luks key for tests.
+ A dummy key is in /tmp/secret.key
+ '';
+ default = { };
+ };
+ };
+ };
+
+ options.virtualisation.vmVariantWithDisko = lib.mkOption {
+ description = ''
+ Machine configuration to be added for the vm script available at `.system.build.vmWithDisko`.
+ '';
+ inherit (vmVariantWithDisko) type;
+ default = { };
+ visible = "shallow";
+ };
+
+ config = {
+ assertions = [
+ {
+ assertion = config.disko.imageBuilder.qemu != null -> diskoLib.vmToolsSupportsCustomQemu lib;
+ message = ''
+ You have set config.disko.imageBuild.qemu, but vmTools in your nixpkgs version "${lib.version}"
+ does not support overriding the qemu package with the customQemu option yet.
+ Please upgrade nixpkgs so that `lib.version` is at least "24.11.20240709".
+ '';
+ }
+ ];
+
+ _module.args.diskoLib = import ./lib {
+ inherit lib;
+ rootMountPoint = config.disko.rootMountPoint;
+ makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix");
+ eval-config = import (pkgs.path + "/nixos/lib/eval-config.nix");
+ };
+
+ system.build = (cfg.devices._scripts { inherit pkgs; checked = cfg.checkScripts; }) // (
+ let
+ throwIfNoDisksDetected = _: v: if cfg.devices.disk == { } then throw "No disks defined, did you forget to import your disko config?" else v;
+ in
+ lib.mapAttrs throwIfNoDisksDetected {
+ # we keep these old outputs for compatibility
+ disko = builtins.trace "the .disko output is deprecated, please use .diskoScript instead" (cfg.devices._scripts { inherit pkgs; }).diskoScript;
+ diskoNoDeps = builtins.trace "the .diskoNoDeps output is deprecated, please use .diskoScriptNoDeps instead" (cfg.devices._scripts { inherit pkgs; }).diskoScriptNoDeps;
+
+ installTest = diskoLib.testLib.makeDiskoTest {
+ inherit extendModules pkgs;
+ name = "${config.networking.hostName}-disko";
+ disko-config = builtins.removeAttrs config [ "_module" ];
+ testMode = "direct";
+ bootCommands = cfg.tests.bootCommands;
+ efi = cfg.tests.efi;
+ enableOCR = cfg.tests.enableOCR;
+ extraSystemConfig = cfg.tests.extraConfig;
+ extraTestScript = cfg.tests.extraChecks;
+ };
+
+ vmWithDisko = lib.mkDefault config.virtualisation.vmVariantWithDisko.system.build.vmWithDisko;
+ }
+ );
+
+ # we need to specify the keys here, so we don't get an infinite recursion error
+ # Remember to add config keys here if they are added to types
+ fileSystems = lib.mkIf
+ cfg.enableConfig cfg.devices._config.fileSystems or { };
+ boot = lib.mkIf
+ cfg.enableConfig cfg.devices._config.boot or { };
+ swapDevices = lib.mkIf
+ cfg.enableConfig cfg.devices._config.swapDevices or [ ];
+ };
+}
diff --git a/pkgs/disko/package.nix b/pkgs/disko/package.nix
new file mode 100644
index 0000000..3464854
--- /dev/null
+++ b/pkgs/disko/package.nix
@@ -0,0 +1,38 @@
+{ stdenvNoCC, makeWrapper, lib, path, nix, coreutils, nixos-install-tools, binlore, diskoVersion }:
+
+let
+ self = stdenvNoCC.mkDerivation (finalAttrs: {
+ name = "disko";
+ src = ./.;
+ nativeBuildInputs = [
+ makeWrapper
+ ];
+ installPhase = ''
+ mkdir -p $out/bin $out/share/disko
+ cp -r install-cli.nix cli.nix default.nix disk-deactivate lib $out/share/disko
+
+ for i in disko disko-install; do
+ sed -e "s|libexec_dir=\".*\"|libexec_dir=\"$out/share/disko\"|" "$i" > "$out/bin/$i"
+ chmod 755 "$out/bin/$i"
+ wrapProgram "$out/bin/$i" \
+ --set DISKO_VERSION "${diskoVersion}" \
+ --prefix PATH : ${lib.makeBinPath [ nix coreutils nixos-install-tools ]} \
+ --prefix NIX_PATH : "nixpkgs=${path}"
+ done
+ '';
+ # Otherwise resholve thinks that disko and disko-install might be able to execute their arguments
+ passthru.binlore.out = binlore.synthesize self ''
+ execer cannot bin/.disko-wrapped
+ execer cannot bin/.disko-install-wrapped
+ '';
+ meta = with lib; {
+ description = "Format disks with nix-config";
+ homepage = "https://github.com/nix-community/disko";
+ license = licenses.mit;
+ maintainers = with maintainers; [ lassulus ];
+ platforms = platforms.linux;
+ mainProgram = finalAttrs.name;
+ };
+ });
+in
+self
diff --git a/pkgs/disko/scripts/create-release.nix b/pkgs/disko/scripts/create-release.nix
new file mode 100644
index 0000000..42f3319
--- /dev/null
+++ b/pkgs/disko/scripts/create-release.nix
@@ -0,0 +1,18 @@
+{
+ lib,
+ writeShellApplication,
+ bash,
+ coreutils,
+ git,
+ nix-fast-build,
+}:
+writeShellApplication {
+ name = "create-release";
+ runtimeInputs = [
+ bash
+ git
+ coreutils
+ nix-fast-build
+ ];
+ text = lib.readFile ./create-release.sh;
+}
diff --git a/pkgs/disko/scripts/create-release.sh b/pkgs/disko/scripts/create-release.sh
new file mode 100755
index 0000000..9ea37dc
--- /dev/null
+++ b/pkgs/disko/scripts/create-release.sh
@@ -0,0 +1,65 @@
+# Don't run directly! Instead, use
+# nix run .#create-release
+
+version=${1:-}
+if [[ -z "$version" ]]; then
+ echo "USAGE: nix run .#create-release -- " >&2
+ exit 1
+fi
+
+# Check if we're running from the root of the repository
+if [[ ! -f "flake.nix" || ! -f "version.nix" ]]; then
+ echo "This script must be run from the root of the repository" >&2
+ exit 1
+fi
+
+# Check if the version matches the semver pattern (without suffixes)
+semver_regex="^([0-9]+)\.([0-9]+)\.([0-9]+)$"
+if [[ ! "$version" =~ $semver_regex ]]; then
+ echo "Version must match the semver pattern (e.g., 1.0.0, 2.3.4)" >&2
+ exit 1
+fi
+
+if [[ "$(git symbolic-ref --short HEAD)" != "master" ]]; then
+ echo "must be on master branch" >&2
+ exit 1
+fi
+
+# Ensure there are no uncommitted or unpushed changes
+uncommited_changes=$(git diff --compact-summary)
+if [[ -n "$uncommited_changes" ]]; then
+ echo -e "There are uncommited changes, exiting:\n${uncommited_changes}" >&2
+ exit 1
+fi
+git pull git@github.com:nix-community/disko master
+unpushed_commits=$(git log --format=oneline origin/master..master)
+if [[ "$unpushed_commits" != "" ]]; then
+ echo -e "\nThere are unpushed changes, exiting:\n$unpushed_commits" >&2
+ exit 1
+fi
+
+# Run all tests to ensure we don't release a broken version
+# Two workers are safe on systems with at least 16GB of RAM
+nix-fast-build --no-link -j 2 --eval-workers 2 --flake .#checks
+
+# Update the version file
+echo "{ version = \"$version\"; released = true; }" > version.nix
+
+# Commit and tag the release
+git commit -am "release: v$version"
+git tag -a "v$version" -m "release: v$version"
+git tag -d "latest"
+git tag -a "latest" -m "release: v$version"
+
+# a revsion suffix when run from the tagged release commit
+echo "{ version = \"$version\"; released = false; }" > version.nix
+git commit -am "release: reset released flag"
+
+echo "Release was prepared successfully!"
+echo "To push the release, run the following command:"
+echo
+echo " git push origin master v$version && git push --force origin latest"
+echo
+echo "After that, create a release on GitHub:"
+echo
+echo " https://github.com/nix-community/disko/releases/new"
diff --git a/pkgs/disko/statix.toml b/pkgs/disko/statix.toml
new file mode 100644
index 0000000..c46ef0a
--- /dev/null
+++ b/pkgs/disko/statix.toml
@@ -0,0 +1,4 @@
+disabled = [
+ "manual_inherit", # Prefer `inherit types;` instead of `types = types;`
+ "manual_inherit_from", # Prefer `inherit (eval) options;` instead of `options = eval.options`.
+]
diff --git a/pkgs/disko/tests/bcachefs.nix b/pkgs/disko/tests/bcachefs.nix
new file mode 100644
index 0000000..f8806d5
--- /dev/null
+++ b/pkgs/disko/tests/bcachefs.nix
@@ -0,0 +1,16 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "bcachefs";
+ disko-config = ../example/bcachefs.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ machine.succeed("lsblk >&2");
+ '';
+ # so that the installer boots with a bcachefs enabled kernel
+ extraInstallerConfig = {
+ boot.supportedFilesystems = [ "bcachefs" ];
+ };
+}
diff --git a/pkgs/disko/tests/boot-raid1.nix b/pkgs/disko/tests/boot-raid1.nix
new file mode 100644
index 0000000..e020fdd
--- /dev/null
+++ b/pkgs/disko/tests/boot-raid1.nix
@@ -0,0 +1,16 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "boot-raid1";
+ disko-config = ../example/boot-raid1.nix;
+ extraTestScript = ''
+ machine.succeed("test -b /dev/md/boot");
+ machine.succeed("mountpoint /boot");
+ '';
+ extraSystemConfig = {
+ # sadly systemd-boot fails to install to a raid /boot device
+ boot.loader.systemd-boot.enable = false;
+ };
+}
diff --git a/pkgs/disko/tests/btrfs-only-root-subvolume.nix b/pkgs/disko/tests/btrfs-only-root-subvolume.nix
new file mode 100644
index 0000000..818aec7
--- /dev/null
+++ b/pkgs/disko/tests/btrfs-only-root-subvolume.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "btrfs-only-root-subvolume";
+ disko-config = ../example/btrfs-only-root-subvolume.nix;
+ extraTestScript = ''
+ machine.succeed("btrfs subvolume list /");
+ '';
+}
diff --git a/pkgs/disko/tests/btrfs-subvolumes.nix b/pkgs/disko/tests/btrfs-subvolumes.nix
new file mode 100644
index 0000000..7059c1a
--- /dev/null
+++ b/pkgs/disko/tests/btrfs-subvolumes.nix
@@ -0,0 +1,20 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "btrfs-subvolumes";
+ disko-config = ../example/btrfs-subvolumes.nix;
+ extraTestScript = ''
+ machine.succeed("test ! -e /test");
+ machine.succeed("test -e /home/user");
+ machine.succeed("btrfs subvolume list / | grep -qs 'path test$'");
+ machine.succeed("btrfs subvolume list / | grep -qs 'path nix$'");
+ machine.succeed("btrfs subvolume list / | grep -qs 'path home$'");
+ machine.succeed("test -e /.swapvol/swapfile");
+ machine.succeed("test -e /.swapvol/rel-path");
+ machine.succeed("test -e /partition-root/swapfile");
+ machine.succeed("test -e /partition-root/swapfile1");
+ '';
+}
+
diff --git a/pkgs/disko/tests/cli.nix b/pkgs/disko/tests/cli.nix
new file mode 100644
index 0000000..76cc2b1
--- /dev/null
+++ b/pkgs/disko/tests/cli.nix
@@ -0,0 +1,34 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "cli";
+ disko-config = ../example/complex.nix;
+ extraInstallerConfig.networking.hostId = "8425e349";
+ extraSystemConfig = {
+ networking.hostId = "8425e349";
+ fileSystems."/zfs_legacy_fs".options = [ "nofail" ]; # TODO find out why we need this!
+ fileSystems."/zfs_fs".options = [ "nofail" ]; # TODO find out why we need this!
+ };
+ testMode = "direct";
+ extraTestScript = ''
+ machine.succeed("test -b /dev/md/raid1p1");
+
+ machine.succeed("mountpoint /zfs_fs");
+ machine.succeed("mountpoint /zfs_legacy_fs");
+ machine.succeed("mountpoint /ext4onzfs");
+ machine.succeed("mountpoint /ext4_on_lvm");
+ '';
+ extraSystemConfig = {
+ imports = [
+ ../module.nix
+ ];
+ };
+ extraInstallerConfig = {
+ boot.kernelModules = [ "dm-raid" "dm-mirror" ];
+ imports = [
+ ../module.nix
+ ];
+ };
+}
diff --git a/pkgs/disko/tests/complex.nix b/pkgs/disko/tests/complex.nix
new file mode 100644
index 0000000..26cc5e9
--- /dev/null
+++ b/pkgs/disko/tests/complex.nix
@@ -0,0 +1,29 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "complex";
+ disko-config = ../example/complex.nix;
+ extraInstallerConfig.networking.hostId = "8425e349";
+ extraSystemConfig = {
+ networking.hostId = "8425e349";
+ fileSystems."/zfs_legacy_fs".options = [ "nofail" ]; # TODO find out why we need this!
+ fileSystems."/zfs_fs".options = [ "nofail" ]; # TODO find out why we need this!
+ };
+ extraTestScript = ''
+ machine.succeed("test -b /dev/md/raid1p1");
+
+
+ machine.succeed("mountpoint /zfs_fs");
+ machine.succeed("mountpoint /zfs_legacy_fs");
+ machine.succeed("mountpoint /ext4onzfs");
+ machine.succeed("mountpoint /ext4_on_lvm");
+
+
+ machine.succeed("test -e /ext4_on_lvm/file-from-postMountHook");
+ '';
+ extraInstallerConfig = {
+ boot.kernelModules = [ "dm-raid" "dm-mirror" ];
+ };
+}
diff --git a/pkgs/disko/tests/default.nix b/pkgs/disko/tests/default.nix
new file mode 100644
index 0000000..1ea3143
--- /dev/null
+++ b/pkgs/disko/tests/default.nix
@@ -0,0 +1,20 @@
+{ makeTest ? import
+, eval-config ? import
+, pkgs ? import { }
+}:
+let
+ lib = pkgs.lib;
+ diskoLib = import ../lib { inherit lib makeTest eval-config; };
+
+ allTestFilenames =
+ builtins.map (lib.removeSuffix ".nix") (
+ builtins.filter
+ (x: lib.hasSuffix ".nix" x && x != "default.nix")
+ (lib.attrNames (builtins.readDir ./.))
+ );
+ incompatibleTests = lib.optionals pkgs.stdenv.buildPlatform.isRiscV64 [ "zfs" "zfs-over-legacy" "cli" "module" "complex" ];
+ allCompatibleFilenames = lib.subtractLists incompatibleTests allTestFilenames;
+
+ allTests = lib.genAttrs allCompatibleFilenames (test: import (./. + "/${test}.nix") { inherit diskoLib pkgs; });
+in
+allTests
diff --git a/pkgs/disko/tests/disko-install/configuration.nix b/pkgs/disko/tests/disko-install/configuration.nix
new file mode 100644
index 0000000..41e33e0
--- /dev/null
+++ b/pkgs/disko/tests/disko-install/configuration.nix
@@ -0,0 +1,23 @@
+{ lib, pkgs, modulesPath, ... }: {
+ imports = [
+ (modulesPath + "/testing/test-instrumentation.nix")
+ (modulesPath + "/profiles/qemu-guest.nix")
+ (modulesPath + "/profiles/minimal.nix")
+ ];
+
+ networking.hostName = "disko-machine";
+
+ # do not try to fetch stuff from the internet
+ nix.settings = {
+ substituters = lib.mkForce [ ];
+ hashed-mirrors = null;
+ connect-timeout = 3;
+ flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
+ experimental-features = [ "nix-command" "flakes" ];
+ };
+ services.openssh.enable = true;
+ boot.kernelParams = [ "console=tty0" ] ++
+ (lib.optional (pkgs.stdenv.hostPlatform.isAarch) "ttyAMA0,115200") ++
+ (lib.optional (pkgs.stdenv.hostPlatform.isRiscV64) "ttySIF0,115200") ++
+ [ "console=ttyS0,115200" ];
+}
diff --git a/pkgs/disko/tests/disko-install/default.nix b/pkgs/disko/tests/disko-install/default.nix
new file mode 100644
index 0000000..af3a660
--- /dev/null
+++ b/pkgs/disko/tests/disko-install/default.nix
@@ -0,0 +1,66 @@
+{ pkgs ? import { }, self, diskoVersion }:
+let
+ disko = pkgs.callPackage ../../package.nix { inherit diskoVersion; };
+
+ dependencies = [
+ self.nixosConfigurations.testmachine.pkgs.stdenv.drvPath
+ (self.nixosConfigurations.testmachine.pkgs.closureInfo { rootPaths = [ ]; }).drvPath
+
+ # https://github.com/NixOS/nixpkgs/blob/f2fd33a198a58c4f3d53213f01432e4d88474956/nixos/modules/system/activation/top-level.nix#L342
+ self.nixosConfigurations.testmachine.pkgs.perlPackages.ConfigIniFiles
+ self.nixosConfigurations.testmachine.pkgs.perlPackages.FileSlurp
+
+ self.nixosConfigurations.testmachine.config.system.build.toplevel
+ self.nixosConfigurations.testmachine.config.system.build.diskoScript
+ ] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
+
+ closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
+in
+pkgs.nixosTest {
+ name = "disko-test";
+ nodes.machine = {
+ virtualisation.emptyDiskImages = [ 4096 ];
+ virtualisation.memorySize = 3000;
+ environment.etc."install-closure".source = "${closureInfo}/store-paths";
+ };
+
+ testScript = ''
+ def create_test_machine(
+ oldmachine=None, **kwargs
+ ): # taken from
+ start_command = [
+ "${pkgs.qemu_test}/bin/qemu-kvm",
+ "-cpu",
+ "max",
+ "-m",
+ "1024",
+ "-virtfs",
+ "local,path=/nix/store,security_model=none,mount_tag=nix-store",
+ "-drive",
+ f"file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report",
+ "-device",
+ "virtio-blk-pci,drive=drive1",
+ ]
+ machine = create_machine(start_command=" ".join(start_command), **kwargs)
+ driver.machines.append(machine)
+ return machine
+ machine.succeed("lsblk >&2")
+
+ print(machine.succeed("tty"))
+ machine.succeed("umask 066; echo > /tmp/age.key")
+ permission = machine.succeed("stat -c %a /tmp/age.key").strip()
+ assert permission == "600", f"expected permission 600 on /tmp/age.key, got {permission}"
+
+ machine.succeed("${disko}/bin/disko-install --disk main /dev/vdb --extra-files /tmp/age.key /var/lib/secrets/age.key --flake ${../..}#testmachine")
+ # test idempotency
+ machine.succeed("${disko}/bin/disko-install --mode mount --disk main /dev/vdb --flake ${../..}#testmachine")
+ machine.shutdown()
+
+ new_machine = create_test_machine(oldmachine=machine, name="after_install")
+ new_machine.start()
+ name = new_machine.succeed("hostname").strip()
+ assert name == "disko-machine", f"expected hostname 'disko-machine', got {name}"
+ permission = new_machine.succeed("stat -c %a /var/lib/secrets/age.key").strip()
+ assert permission == "600", f"expected permission 600 on /var/lib/secrets/age.key, got {permission}"
+ '';
+}
diff --git a/pkgs/disko/tests/f2fs.nix b/pkgs/disko/tests/f2fs.nix
new file mode 100644
index 0000000..ebcf8d8
--- /dev/null
+++ b/pkgs/disko/tests/f2fs.nix
@@ -0,0 +1,16 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "f2fs";
+ disko-config = ../example/f2fs.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ machine.succeed("lsblk --fs >&2");
+ '';
+ # so that the installer boots with a f2fs enabled kernel
+ extraInstallerConfig = {
+ boot.supportedFilesystems = [ "f2fs" ];
+ };
+}
diff --git a/pkgs/disko/tests/gpt-bios-compat.nix b/pkgs/disko/tests/gpt-bios-compat.nix
new file mode 100644
index 0000000..de45d3b
--- /dev/null
+++ b/pkgs/disko/tests/gpt-bios-compat.nix
@@ -0,0 +1,12 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "gpt-bios-compat";
+ disko-config = ../example/gpt-bios-compat.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ '';
+ efi = false;
+}
diff --git a/pkgs/disko/tests/gpt-name-with-special-chars.nix b/pkgs/disko/tests/gpt-name-with-special-chars.nix
new file mode 100644
index 0000000..48b4304
--- /dev/null
+++ b/pkgs/disko/tests/gpt-name-with-special-chars.nix
@@ -0,0 +1,13 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "gpt-name-with-whitespace";
+ disko-config = ../example/gpt-name-with-whitespace.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ machine.succeed("mountpoint '/name with spaces'");
+ machine.succeed("mountpoint '/name^with\\some@special#chars'");
+ '';
+}
diff --git a/pkgs/disko/tests/gpt-unformatted.nix b/pkgs/disko/tests/gpt-unformatted.nix
new file mode 100644
index 0000000..9c75f46
--- /dev/null
+++ b/pkgs/disko/tests/gpt-unformatted.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "gpt-unformatted";
+ disko-config = ../example/gpt-unformatted.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ '';
+}
diff --git a/pkgs/disko/tests/hybrid-mbr.nix b/pkgs/disko/tests/hybrid-mbr.nix
new file mode 100644
index 0000000..de68b26
--- /dev/null
+++ b/pkgs/disko/tests/hybrid-mbr.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "hybrid-mbr";
+ disko-config = ../example/hybrid-mbr.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ '';
+}
diff --git a/pkgs/disko/tests/hybrid-tmpfs-on-root.nix b/pkgs/disko/tests/hybrid-tmpfs-on-root.nix
new file mode 100644
index 0000000..09d15d6
--- /dev/null
+++ b/pkgs/disko/tests/hybrid-tmpfs-on-root.nix
@@ -0,0 +1,12 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "hybrid-tmpfs-on-root";
+ disko-config = ../example/hybrid-tmpfs-on-root.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ machine.succeed("findmnt / --types tmpfs");
+ '';
+}
diff --git a/pkgs/disko/tests/hybrid.nix b/pkgs/disko/tests/hybrid.nix
new file mode 100644
index 0000000..adf7ccd
--- /dev/null
+++ b/pkgs/disko/tests/hybrid.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "hybrid";
+ disko-config = ../example/hybrid.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ '';
+}
diff --git a/pkgs/disko/tests/legacy-table-with-whitespace.nix b/pkgs/disko/tests/legacy-table-with-whitespace.nix
new file mode 100644
index 0000000..300c641
--- /dev/null
+++ b/pkgs/disko/tests/legacy-table-with-whitespace.nix
@@ -0,0 +1,12 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "legacy-table-with-whitespace";
+ disko-config = ../example/legacy-table-with-whitespace.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ machine.succeed("mountpoint /name_with_spaces");
+ '';
+}
diff --git a/pkgs/disko/tests/legacy-table.nix b/pkgs/disko/tests/legacy-table.nix
new file mode 100644
index 0000000..2c7b646
--- /dev/null
+++ b/pkgs/disko/tests/legacy-table.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "legacy-table";
+ disko-config = ../example/legacy-table.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ '';
+}
diff --git a/pkgs/disko/tests/long-device-name.nix b/pkgs/disko/tests/long-device-name.nix
new file mode 100644
index 0000000..e328a9f
--- /dev/null
+++ b/pkgs/disko/tests/long-device-name.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "long-device-name";
+ disko-config = ../example/long-device-name.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ '';
+}
diff --git a/pkgs/disko/tests/luks-btrfs-raid.nix b/pkgs/disko/tests/luks-btrfs-raid.nix
new file mode 100644
index 0000000..6e12749
--- /dev/null
+++ b/pkgs/disko/tests/luks-btrfs-raid.nix
@@ -0,0 +1,14 @@
+{
+ pkgs ? import { },
+ diskoLib ? pkgs.callPackage ../lib { },
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "luks-btrfs-raid";
+ disko-config = ../example/luks-btrfs-raid.nix;
+ extraTestScript = ''
+ machine.succeed("cryptsetup isLuks /dev/vda2");
+ machine.succeed("cryptsetup isLuks /dev/vdb1");
+ machine.succeed("btrfs subvolume list /");
+ '';
+}
diff --git a/pkgs/disko/tests/luks-btrfs-subvolumes.nix b/pkgs/disko/tests/luks-btrfs-subvolumes.nix
new file mode 100644
index 0000000..6a4e64b
--- /dev/null
+++ b/pkgs/disko/tests/luks-btrfs-subvolumes.nix
@@ -0,0 +1,14 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "luks-btrfs-subvolumes";
+ disko-config = ../example/luks-btrfs-subvolumes.nix;
+ extraTestScript = ''
+ machine.succeed("cryptsetup isLuks /dev/vda2");
+ machine.succeed("btrfs subvolume list / | grep -qs 'path nix$'");
+ machine.succeed("btrfs subvolume list / | grep -qs 'path home$'");
+ machine.succeed("test -e /.swapvol/swapfile");
+ '';
+}
diff --git a/pkgs/disko/tests/luks-interactive-login.nix b/pkgs/disko/tests/luks-interactive-login.nix
new file mode 100644
index 0000000..a1225d0
--- /dev/null
+++ b/pkgs/disko/tests/luks-interactive-login.nix
@@ -0,0 +1,17 @@
+{
+ pkgs ? import { },
+ diskoLib ? pkgs.callPackage ../lib { },
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "luks-interactive-login";
+ disko-config = ../example/luks-interactive-login.nix;
+ enableOCR = true;
+ extraTestScript = ''
+ machine.succeed("cryptsetup isLuks /dev/vda2");
+ '';
+ bootCommands = ''
+ machine.wait_for_text("[Pp]assphrase for")
+ machine.send_chars("secretsecret\n")
+ '';
+}
diff --git a/pkgs/disko/tests/luks-lvm.nix b/pkgs/disko/tests/luks-lvm.nix
new file mode 100644
index 0000000..848a5b7
--- /dev/null
+++ b/pkgs/disko/tests/luks-lvm.nix
@@ -0,0 +1,12 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "luks-lvm";
+ disko-config = ../example/luks-lvm.nix;
+ extraTestScript = ''
+ machine.succeed("cryptsetup isLuks /dev/vda2");
+ machine.succeed("mountpoint /home");
+ '';
+}
diff --git a/pkgs/disko/tests/luks-on-mdadm.nix b/pkgs/disko/tests/luks-on-mdadm.nix
new file mode 100644
index 0000000..bd49762
--- /dev/null
+++ b/pkgs/disko/tests/luks-on-mdadm.nix
@@ -0,0 +1,16 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "luks-on-mdadm";
+ disko-config = ../example/luks-on-mdadm.nix;
+ extraTestScript = ''
+ machine.succeed("test -b /dev/md/raid1");
+ machine.succeed("mountpoint /");
+ '';
+ extraSystemConfig = {
+ # sadly systemd-boot fails to install to a raid /boot device
+ boot.loader.systemd-boot.enable = false;
+ };
+}
diff --git a/pkgs/disko/tests/lvm-raid.nix b/pkgs/disko/tests/lvm-raid.nix
new file mode 100644
index 0000000..b30332a
--- /dev/null
+++ b/pkgs/disko/tests/lvm-raid.nix
@@ -0,0 +1,18 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "lvm-raid";
+ disko-config = ../example/lvm-raid.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /home");
+ '';
+ extraInstallerConfig = {
+ boot.kernelModules = [ "dm-raid" "raid0" "dm-mirror" ];
+ };
+ extraSystemConfig = {
+ # sadly systemd-boot fails to install to a raid /boot device
+ boot.loader.systemd-boot.enable = false;
+ };
+}
diff --git a/pkgs/disko/tests/lvm-sizes-sort.nix b/pkgs/disko/tests/lvm-sizes-sort.nix
new file mode 100644
index 0000000..83ed472
--- /dev/null
+++ b/pkgs/disko/tests/lvm-sizes-sort.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "lvm-sizes-sort";
+ disko-config = ../example/lvm-sizes-sort.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /home");
+ '';
+}
diff --git a/pkgs/disko/tests/lvm-thin.nix b/pkgs/disko/tests/lvm-thin.nix
new file mode 100644
index 0000000..bfbcfc1
--- /dev/null
+++ b/pkgs/disko/tests/lvm-thin.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "lvm-thin";
+ disko-config = ../example/lvm-thin.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /home");
+ '';
+}
diff --git a/pkgs/disko/tests/make-disk-image-impure.nix b/pkgs/disko/tests/make-disk-image-impure.nix
new file mode 100644
index 0000000..1f96a3e
--- /dev/null
+++ b/pkgs/disko/tests/make-disk-image-impure.nix
@@ -0,0 +1,13 @@
+{ pkgs ? import { }
+, ...
+}:
+
+(pkgs.nixos [
+ ../module.nix
+ ../example/simple-efi.nix
+ ({ config, ... }: {
+ documentation.enable = false;
+ system.stateVersion = config.system.nixos.version;
+ disko.checkScripts = true;
+ })
+]).config.system.build.diskoImagesScript
diff --git a/pkgs/disko/tests/make-disk-image.nix b/pkgs/disko/tests/make-disk-image.nix
new file mode 100644
index 0000000..75647d1
--- /dev/null
+++ b/pkgs/disko/tests/make-disk-image.nix
@@ -0,0 +1,14 @@
+{ pkgs ? import { }
+, ...
+}:
+
+(pkgs.nixos [
+ ../module.nix
+ ../example/simple-efi.nix
+ ({ config, ... }: {
+ documentation.enable = false;
+ system.stateVersion = config.system.nixos.version;
+ disko.memSize = 2048;
+ disko.checkScripts = true;
+ })
+]).config.system.build.diskoImages
diff --git a/pkgs/disko/tests/mdadm-raid0.nix b/pkgs/disko/tests/mdadm-raid0.nix
new file mode 100644
index 0000000..7d40109
--- /dev/null
+++ b/pkgs/disko/tests/mdadm-raid0.nix
@@ -0,0 +1,13 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "mdadm-raid0";
+ disko-config = ../example/mdadm-raid0.nix;
+ extraTestScript = ''
+ machine.succeed("test -b /dev/md/raid0");
+ machine.succeed("mountpoint /");
+ '';
+ efi = false;
+}
diff --git a/pkgs/disko/tests/mdadm.nix b/pkgs/disko/tests/mdadm.nix
new file mode 100644
index 0000000..3bd9037
--- /dev/null
+++ b/pkgs/disko/tests/mdadm.nix
@@ -0,0 +1,13 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "mdadm";
+ disko-config = ../example/mdadm.nix;
+ extraTestScript = ''
+ machine.succeed("test -b /dev/md/raid1");
+ machine.succeed("mountpoint /");
+ '';
+ efi = false;
+}
diff --git a/pkgs/disko/tests/module.nix b/pkgs/disko/tests/module.nix
new file mode 100644
index 0000000..c6eb092
--- /dev/null
+++ b/pkgs/disko/tests/module.nix
@@ -0,0 +1,26 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "module";
+ disko-config = ../example/complex.nix;
+ extraInstallerConfig.networking.hostId = "8425e349";
+ extraSystemConfig = {
+ networking.hostId = "8425e349";
+ fileSystems."/zfs_legacy_fs".options = [ "nofail" ]; # TODO find out why we need this!
+ };
+ testMode = "module";
+ extraTestScript = ''
+ machine.succeed("test -b /dev/md/raid1p1");
+
+
+ machine.succeed("mountpoint /zfs_fs");
+ machine.succeed("mountpoint /zfs_legacy_fs");
+ machine.succeed("mountpoint /ext4onzfs");
+ machine.succeed("mountpoint /ext4_on_lvm");
+ '';
+ extraInstallerConfig = {
+ boot.kernelModules = [ "dm-raid" "dm-mirror" ];
+ };
+}
diff --git a/pkgs/disko/tests/multi-device-no-deps.nix b/pkgs/disko/tests/multi-device-no-deps.nix
new file mode 100644
index 0000000..77e3477
--- /dev/null
+++ b/pkgs/disko/tests/multi-device-no-deps.nix
@@ -0,0 +1,14 @@
+# this is a regression test for https://github.com/nix-community/disko/issues/52
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "multi-device-no-deps";
+ disko-config = ../example/multi-device-no-deps.nix;
+ testBoot = false;
+ extraTestScript = ''
+ machine.succeed("mountpoint /mnt/a");
+ machine.succeed("mountpoint /mnt/b");
+ '';
+}
diff --git a/pkgs/disko/tests/negative-size.nix b/pkgs/disko/tests/negative-size.nix
new file mode 100644
index 0000000..b891b08
--- /dev/null
+++ b/pkgs/disko/tests/negative-size.nix
@@ -0,0 +1,13 @@
+# this is a regression test for https://github.com/nix-community/disko/issues/52
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "negative-size";
+ disko-config = ../example/negative-size.nix;
+ testBoot = false;
+ extraTestScript = ''
+ machine.succeed("mountpoint /mnt");
+ '';
+}
diff --git a/pkgs/disko/tests/non-root-zfs.nix b/pkgs/disko/tests/non-root-zfs.nix
new file mode 100644
index 0000000..2338483
--- /dev/null
+++ b/pkgs/disko/tests/non-root-zfs.nix
@@ -0,0 +1,48 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "non-root-zfs";
+ disko-config = ../example/non-root-zfs.nix;
+ extraInstallerConfig.networking.hostId = "8425e349";
+ extraSystemConfig.networking.hostId = "8425e349";
+ postDisko = ''
+ machine.succeed("mountpoint /mnt/storage")
+ machine.succeed("mountpoint /mnt/storage/dataset")
+
+ filesystem = machine.execute("stat --file-system --format=%T /mnt/storage")[1].rstrip()
+ print(f"/mnt/storage {filesystem=}")
+ assert filesystem == "zfs", "/mnt/storage is not ZFS"
+
+ machine.fail("mountpoint /mnt/storage2")
+ machine.succeed("mountpoint /mnt/storage2/dataset")
+
+ filesystem = machine.execute("stat --file-system --format=%T /mnt/storage2")[1].rstrip()
+ print(f"/mnt/storage2 {filesystem=}")
+ assert filesystem != "zfs", "/mnt/storage should not be ZFS"
+
+ filesystem = machine.execute("stat --file-system --format=%T /mnt/storage2/dataset")[1].rstrip()
+ print(f"/mnt/storage2/dataset {filesystem=}")
+ assert filesystem == "zfs", "/mnt/storage/dataset is not ZFS"
+ '';
+ extraTestScript = ''
+ machine.succeed("mountpoint /storage")
+ machine.succeed("mountpoint /storage/dataset")
+
+ filesystem = machine.execute("stat --file-system --format=%T /storage")[1].rstrip()
+ print(f"/storage {filesystem=}")
+ assert filesystem == "zfs", "/storage is not ZFS"
+
+ machine.fail("mountpoint /storage2")
+ machine.succeed("mountpoint /storage2/dataset")
+
+ filesystem = machine.execute("stat --file-system --format=%T /storage2")[1].rstrip()
+ print(f"/storage2 {filesystem=}")
+ assert filesystem != "zfs", "/storage should not be ZFS"
+
+ filesystem = machine.execute("stat --file-system --format=%T /storage2/dataset")[1].rstrip()
+ print(f"/storage2/dataset {filesystem=}")
+ assert filesystem == "zfs", "/storage/dataset is not ZFS"
+ '';
+}
diff --git a/pkgs/disko/tests/simple-efi.nix b/pkgs/disko/tests/simple-efi.nix
new file mode 100644
index 0000000..f3b9071
--- /dev/null
+++ b/pkgs/disko/tests/simple-efi.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "simple-efi";
+ disko-config = ../example/simple-efi.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ '';
+}
diff --git a/pkgs/disko/tests/standalone.nix b/pkgs/disko/tests/standalone.nix
new file mode 100644
index 0000000..5da5cc0
--- /dev/null
+++ b/pkgs/disko/tests/standalone.nix
@@ -0,0 +1,5 @@
+{ pkgs ? import { }, ... }:
+(pkgs.nixos [
+ ../example/stand-alone/configuration.nix
+ { documentation.enable = false; }
+]).config.system.build.toplevel
diff --git a/pkgs/disko/tests/swap.nix b/pkgs/disko/tests/swap.nix
new file mode 100644
index 0000000..7d1678b
--- /dev/null
+++ b/pkgs/disko/tests/swap.nix
@@ -0,0 +1,31 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "swap";
+ disko-config = ../example/swap.nix;
+ extraTestScript = ''
+ import json
+ machine.succeed("mountpoint /");
+ machine.succeed("swapon --show >&2");
+ machine.succeed("lsblk -o +PARTTYPENAME --json /dev/vda >&2");
+ out = json.loads(machine.succeed("lsblk -o +PARTTYPENAME --json /dev/vda"))
+
+ encrypted_swap_crypt = out["blockdevices"][0]["children"][1]
+ mnt_point = encrypted_swap_crypt["children"][0]["mountpoints"][0]
+ assert mnt_point == "[SWAP]", f"Expected encrypted swap partition to be mounted as [SWAP], got {mnt_point}"
+ part_type = encrypted_swap_crypt["parttypename"]
+ # The dm-crypt partition should be labelled as swap, not dm-crypt, see https://github.com/util-linux/util-linux/issues/3238
+ assert part_type == "Linux swap", f"Expected encrypted swap container to be of type Linux swap, got {part_type}"
+
+ plain_swap_part = out["blockdevices"][0]["children"][3]
+ mnt_point = plain_swap_part["mountpoints"][0]
+ assert mnt_point == "[SWAP]", f"Expected swap partition to be mounted as [SWAP], got {mnt_point}"
+ part_type = plain_swap_part["parttypename"]
+ assert part_type == "Linux swap", f"Expected plain swap partition to be of type Linux swap, got {part_type}"
+ '';
+ extraSystemConfig = {
+ environment.systemPackages = [ pkgs.jq ];
+ };
+}
diff --git a/pkgs/disko/tests/tmpfs.nix b/pkgs/disko/tests/tmpfs.nix
new file mode 100644
index 0000000..21cb217
--- /dev/null
+++ b/pkgs/disko/tests/tmpfs.nix
@@ -0,0 +1,12 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "tmpfs";
+ disko-config = ../example/tmpfs.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ machine.succeed("mountpoint /tmp");
+ '';
+}
diff --git a/pkgs/disko/tests/with-lib.nix b/pkgs/disko/tests/with-lib.nix
new file mode 100644
index 0000000..d8274b7
--- /dev/null
+++ b/pkgs/disko/tests/with-lib.nix
@@ -0,0 +1,12 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "with-lib";
+ disko-config = ../example/with-lib.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+ '';
+ efi = false;
+}
diff --git a/pkgs/disko/tests/xfs.nix b/pkgs/disko/tests/xfs.nix
new file mode 100644
index 0000000..3c7a123
--- /dev/null
+++ b/pkgs/disko/tests/xfs.nix
@@ -0,0 +1,13 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "xfs";
+ disko-config = ../example/xfs-with-quota.nix;
+ extraTestScript = ''
+ machine.succeed("mountpoint /");
+
+ machine.succeed("xfs_quota -c 'print' / | grep -q '(pquota)'")
+ '';
+}
diff --git a/pkgs/disko/tests/zfs-over-legacy.nix b/pkgs/disko/tests/zfs-over-legacy.nix
new file mode 100644
index 0000000..af78060
--- /dev/null
+++ b/pkgs/disko/tests/zfs-over-legacy.nix
@@ -0,0 +1,15 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "zfs-over-legacy";
+ extraInstallerConfig.networking.hostId = "8425e349";
+ extraSystemConfig.networking.hostId = "8425e349";
+ disko-config = ../example/zfs-over-legacy.nix;
+ extraTestScript = ''
+ machine.succeed("test -e /zfs_fs");
+ machine.succeed("mountpoint /zfs_fs");
+ '';
+}
+
diff --git a/pkgs/disko/tests/zfs-with-vdevs.nix b/pkgs/disko/tests/zfs-with-vdevs.nix
new file mode 100644
index 0000000..6b9e2d5
--- /dev/null
+++ b/pkgs/disko/tests/zfs-with-vdevs.nix
@@ -0,0 +1,77 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "zfs-with-vdevs";
+ disko-config = ../example/zfs-with-vdevs.nix;
+ extraInstallerConfig.networking.hostId = "8425e349";
+ extraSystemConfig = {
+ networking.hostId = "8425e349";
+ # It looks like the 60s of NixOS is sometimes not enough for our virtio-based zpool.
+ # This fixes the flakeiness of the test.
+ boot.initrd.postResumeCommands = ''
+ for i in $(seq 1 120); do
+ if zpool list | grep -q zroot || zpool import -N zroot; then
+ break
+ fi
+ done
+ '';
+ };
+ extraTestScript = ''
+ def assert_property(ds, property, expected_value):
+ out = machine.succeed(f"zfs get -H {property} {ds} -o value").rstrip()
+ assert (
+ out == expected_value
+ ), f"Expected {property}={expected_value} on {ds}, got: {out}"
+
+ # These fields are 0 if l2arc is disabled
+ assert (
+ machine.succeed(
+ "cat /proc/spl/kstat/zfs/arcstats"
+ " | grep '^l2_' | tr -s ' '"
+ " | cut -s -d ' ' -f3 | uniq"
+ ).strip() != "0"
+ ), "Excepted cache to be utilized."
+
+ assert_property("zroot", "compression", "zstd")
+ assert_property("zroot/zfs_fs", "com.sun:auto-snapshot", "true")
+ assert_property("zroot/zfs_fs", "compression", "zstd")
+ machine.succeed("mountpoint /zfs_fs");
+
+ # Take the status output and flatten it so that each device is on a single line prefixed with with the group (either
+ # the pool name or a designation like log/cache/spare/dedup/special) and first portion of the vdev name (empty for a
+ # disk from a single vdev, mirror for devices in a mirror. This makes it easy to verify that the layout is as
+ # expected.
+ group = ""
+ vdev = ""
+ actual = []
+ for line in machine.succeed("zpool status -P zroot").split("\n"):
+ first_word = line.strip().split(" ", 1)[0]
+ if line.startswith("\t ") and first_word.startswith("/"):
+ actual.append(f"{group}{vdev}{first_word}")
+ elif line.startswith("\t "):
+ vdev = f"{first_word.split('-', 1)[0]} "
+ elif line.startswith("\t"):
+ group = f"{first_word} "
+ vdev = ""
+ actual.sort()
+ expected=sorted([
+ 'zroot /dev/disk/by-partlabel/disk-data1-zfs',
+ 'zroot mirror /dev/disk/by-partlabel/disk-data2-zfs',
+ 'zroot mirror /dev/disk/by-partlabel/disk-data3-zfs',
+ 'dedup /dev/disk/by-partlabel/disk-dedup3-zfs',
+ 'dedup mirror /dev/disk/by-partlabel/disk-dedup1-zfs',
+ 'dedup mirror /dev/disk/by-partlabel/disk-dedup2-zfs',
+ 'special /dev/disk/by-partlabel/disk-special3-zfs',
+ 'special mirror /dev/disk/by-partlabel/disk-special1-zfs',
+ 'special mirror /dev/disk/by-partlabel/disk-special2-zfs',
+ 'logs /dev/disk/by-partlabel/disk-log3-zfs',
+ 'logs mirror /dev/disk/by-partlabel/disk-log1-zfs',
+ 'logs mirror /dev/disk/by-partlabel/disk-log2-zfs',
+ 'cache /dev/disk/by-partlabel/disk-cache-zfs',
+ 'spares /dev/disk/by-partlabel/disk-spare-zfs',
+ ])
+ assert actual == expected, f"Incorrect pool layout. Expected:\n\t{'\n\t'.join(expected)}\nActual:\n\t{'\n\t'.join(actual)}"
+ '';
+}
diff --git a/pkgs/disko/tests/zfs.nix b/pkgs/disko/tests/zfs.nix
new file mode 100644
index 0000000..806d623
--- /dev/null
+++ b/pkgs/disko/tests/zfs.nix
@@ -0,0 +1,41 @@
+{ pkgs ? import { }
+, diskoLib ? pkgs.callPackage ../lib { }
+}:
+diskoLib.testLib.makeDiskoTest {
+ inherit pkgs;
+ name = "zfs";
+ disko-config = ../example/zfs.nix;
+ extraInstallerConfig.networking.hostId = "8425e349";
+ extraSystemConfig = {
+ networking.hostId = "8425e349";
+ fileSystems."/zfs_legacy_fs".options = [ "nofail" ]; # TODO find out why we need this!
+ };
+ extraTestScript = ''
+ machine.succeed("test -b /dev/zvol/zroot/zfs_volume");
+ machine.succeed("test -b /dev/zvol/zroot/zfs_encryptedvolume");
+
+ def assert_property(ds, property, expected_value):
+ out = machine.succeed(f"zfs get -H {property} {ds} -o value").rstrip()
+ assert (
+ out == expected_value
+ ), f"Expected {property}={expected_value} on {ds}, got: {out}"
+
+ assert_property("zroot", "compression", "zstd")
+ assert_property("zroot/zfs_fs", "compression", "zstd")
+ assert_property("zroot", "com.sun:auto-snapshot", "false")
+ assert_property("zroot/zfs_fs", "com.sun:auto-snapshot", "true")
+ assert_property("zroot/zfs_volume", "volsize", "10M")
+ assert_property("zroot/zfs_encryptedvolume", "volsize", "10M")
+ assert_property("zroot/zfs_unmounted_fs", "mountpoint", "none")
+
+ machine.succeed("zfs get name zroot@blank")
+
+ machine.succeed("mountpoint /zfs_fs");
+ machine.succeed("mountpoint /zfs_legacy_fs");
+ machine.succeed("mountpoint /ext4onzfs");
+ machine.succeed("mountpoint /ext4onzfsencrypted");
+ machine.succeed("mountpoint /zfs_crypted");
+ machine.succeed("zfs get keystatus zroot/encrypted");
+ machine.succeed("zfs get keystatus zroot/encrypted/test");
+ '';
+}
diff --git a/pkgs/disko/version.nix b/pkgs/disko/version.nix
new file mode 100644
index 0000000..fc21dc8
--- /dev/null
+++ b/pkgs/disko/version.nix
@@ -0,0 +1 @@
+{ version = "1.10.0"; released = false; }
diff --git a/size-test/.gitignore b/size-test/.gitignore
deleted file mode 100644
index 91ce5dd..0000000
--- a/size-test/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-netboot-base
-netboot-incremental
-netboot-incremental.drv
diff --git a/size-test/base.nix b/size-test/base.nix
deleted file mode 100644
index d0418c3..0000000
--- a/size-test/base.nix
+++ /dev/null
@@ -1,25 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-{
- imports = [
- ../quickly.nix
- ../installer/installer.nix
- ];
- config = {
- users.users.nixos = {
- isNormalUser = true;
- password = "password123";
- extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
- packages = with pkgs; [
- tmux
- htop
- tree
- ];
- };
- environment.systemPackages = with pkgs; [
- vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
- wget
- ];
-
- };
-}
\ No newline at end of file
diff --git a/size-test/build.sh b/size-test/build.sh
deleted file mode 100755
index ca5426c..0000000
--- a/size-test/build.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/sh
-
-set -eux
-
-cd "$(dirname "$0")"
-
-nix-build '' \
- -I nixos-config=./base.nix \
- -A config.system.build.ipxeBootDir \
- --out-link ./netboot-base
-
-nix-instantiate '' \
- -I nixos-config=./incremental.nix \
- -A config.system.build.ipxeBootDir \
- --add-root ./netboot-incremental.drv --indirect
-
-time nix-build ./netboot-incremental.drv \
- --out-link ./netboot-incremental
-
-ensureSame() (
- test "$(realpath "./netboot-base/$1")" = "$(realpath "./netboot-incremental/$1")"
-)
-
-ensureSame bzImage
-ensureSame initrd
-
-echo "ok!"
diff --git a/size-test/incremental.nix b/size-test/incremental.nix
deleted file mode 100644
index e58ac57..0000000
--- a/size-test/incremental.nix
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- imports = [ ./base.nix ];
- services.nginx.enable = true;
-}