1155 lines
36 KiB
Nix
1155 lines
36 KiB
Nix
{
|
|
lib ? import <nixpkgs/lib>,
|
|
rootMountPoint ? "/mnt",
|
|
makeTest ? import <nixpkgs/nixos/tests/make-test-python.nix>,
|
|
eval-config ? import <nixpkgs/nixos/lib/eval-config.nix>,
|
|
}:
|
|
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";
|
|
};
|
|
|
|
/**
|
|
like lib.recursiveUpdate but supports merging of lists
|
|
|
|
# Inputs:
|
|
|
|
`left`
|
|
|
|
: Left attribute set of the merge
|
|
|
|
`right`
|
|
|
|
: Right attribute set of the merge
|
|
|
|
recursiveUpdate :: AttrSet -> AttrSet -> AttrSet
|
|
|
|
# Examples
|
|
:::{.example}
|
|
```nix
|
|
recursiveUpdate {
|
|
boot.loader.grub.enable = true;
|
|
boot.loader.grub.devices = [ "/dev/hda" ];
|
|
} {
|
|
boot.loader.grub.devices = [ "/dev/hdb" ];
|
|
}
|
|
|
|
returns: {
|
|
boot.loader.grub.enable = true;
|
|
boot.loader.grub.devices = [ "/dev/hda" "/dev/hdb" ];
|
|
}
|
|
```
|
|
*
|
|
*/
|
|
recursiveUpdate =
|
|
left: right:
|
|
let
|
|
inherit (lib)
|
|
zipAttrsWith
|
|
length
|
|
elemAt
|
|
head
|
|
isAttrs
|
|
isList
|
|
concatLists
|
|
all
|
|
reverseList
|
|
;
|
|
|
|
recursiveMergeUntil =
|
|
pred: lhs: rhs:
|
|
let
|
|
f =
|
|
attrPath:
|
|
zipAttrsWith (
|
|
n: values:
|
|
let
|
|
here = attrPath ++ [ n ];
|
|
in
|
|
if length values == 1 || pred here (elemAt values 1) (head values) then
|
|
(if all isList values then concatLists (reverseList values) else head values)
|
|
else
|
|
f here values
|
|
);
|
|
in
|
|
f [ ] [ rhs lhs ];
|
|
|
|
recursiveMerge =
|
|
lhs: rhs:
|
|
recursiveMergeUntil (
|
|
_path: lhs: rhs:
|
|
!(isAttrs lhs && isAttrs rhs)
|
|
) lhs rhs;
|
|
|
|
in
|
|
recursiveMerge left right;
|
|
|
|
/*
|
|
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: (diskoLib.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.<name>.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 = "<self.name>";
|
|
args._parent.name = "<parent.name>";
|
|
args._parent.type = "<parent.type>";
|
|
};
|
|
name = "<config.name>";
|
|
};
|
|
parent = { };
|
|
device = "/dev/<device>";
|
|
# 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 = "<self.name>";
|
|
};
|
|
};
|
|
};
|
|
diskoLib = {
|
|
optionTypes.absolute-pathname = "absolute-pathname";
|
|
# Spoof these types to avoid infinite recursion
|
|
deviceType = _: "<deviceType>";
|
|
partitionType = _: "<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;
|
|
};
|
|
};
|
|
|
|
binfmt = import ./binfmt.nix;
|
|
} // outputs;
|
|
in
|
|
diskoLib
|