1
1
mirror of https://github.com/NixOS/mobile-nixos.git synced 2024-12-02 15:27:59 +03:00

image-builder: Import new implementation from celun

This commit is contained in:
Samuel Dionne-Riel 2023-01-31 21:20:23 -05:00
parent d25d3b87e7
commit 0f3ac0bef1
42 changed files with 1614 additions and 1499 deletions

View File

@ -1,47 +0,0 @@
{ lib, newScope }:
let
inherit (lib) makeScope;
in
makeScope newScope (self:
let
inherit (self) callPackage;
in
# Note: Prefer using `self.something.deep` rather than making `something` a
# recursive set. Otherwise it won't override as expected.
{
makeFilesystem = callPackage ./makeFilesystem.nix {};
# All known supported filesystems for image generation.
# Use stand-alone (outside of a disk image) is supported.
fileSystem = {
makeExt4 = callPackage ./makeExt4.nix {};
makeBtrfs = callPackage ./makeBtrfs.nix {};
makeFAT32 = callPackage ./makeFAT32.nix {};
# Specialization of `makeFAT32` with (1) filesystemType showing as ESP,
# and (2) the name defaults to ESP.
makeESP = args: self.fileSystem.makeFAT32 ({ name = "ESP"; filesystemType = "ESP"; } // args);
};
gap = length: {
inherit length;
isGap = true;
};
# All supported disk formats for image generation.
diskImage = {
makeMBR = callPackage ./makeMBR.nix {};
makeGPT = callPackage ./makeGPT.nix {};
};
# Don't do maths yourselves, just use the helpers.
# Yes, this is the bibytes family of units.
# (This is fine as rec; it won't be overriden.)
size = rec {
TiB = x: 1024 * (GiB x);
GiB = x: 1024 * (MiB x);
MiB = x: 1024 * (KiB x);
KiB = x: 1024 * x;
};
}
)

View File

@ -1,19 +0,0 @@
In-depth tests
==============
Those are rather bulky integration-type tests. They are not ran by default, but
should be used to better test the image builder infrastructure.
Changes to the infra should be passed through this test suite in addition to the
slimmer usual tests suite.
> **Tip:**
>
> From the image-builder infra directory, run the following.
>
> ```
> nix-build in-depth-tests/[...].nix -I nixpkgs-overlays=$PWD/lib/tests/test-overlay.nix
> ```
>
> This allows you to track the bigger builds more easily.

View File

@ -1,96 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
let
inherit (pkgs) imageBuilder ubootTools;
configTxt = pkgs.writeText "config.txt" ''
kernel=u-boot-rpi3.bin
# Boot in 64-bit mode.
arm_control=0x200
# Prevent the firmware from smashing the framebuffer setup done by the mainline kernel
# when attempting to show low-voltage or overtemperature warnings.
avoid_warnings=1
'';
scrTxt = pkgs.writeText "uboot.scr.txt" ''
echo
echo
echo
echo
echo " **"
echo " ** Image Builder sanity checks!"
echo " ** This will appear to freeze since the kernel is not built with the VC4 kernel built-in."
echo " ** Stay assured, the kernel should be panicking anyway since there is no initrd, no init, and no useful FS."
echo " **"
echo
load $devtype $devnum:$distro_bootpart $kernel_addr_r boot/kernel
booti $kernel_addr_r
'';
scr = pkgs.runCommand "uboot-script" {} ''
mkdir -p $out
${ubootTools}/bin/mkimage \
-A arm64 \
-O linux \
-T script \
-C none \
-n ${scrTxt} -d ${scrTxt} \
$out/boot.scr
'';
# Here, we built a fictitious system cloning the AArch64 sd-image setup.
# The chosen derivations are known to build fully when cross-compiled.
pkgsAArch64 = (if pkgs.stdenv.isAarch64 then pkgs else pkgs.pkgsCross.aarch64-multiplatform);
# The kernel for the device.
kernel = pkgsAArch64.linux_rpi;
# TODO: for completeness' sake an initrd with the vc4 driver should be built
# to show that this works as a self-contained demo.
in
with imageBuilder;
/**
* This disk image is built to be functionally compatible with the usual `sd_image`
* from NixOS, but *it is not* an actual `sd_image` compatible system.
*
* The main thing it aims to do is *minimally* create a bootable system.
*/
diskImage.makeMBR {
name = "diskimage";
diskID = "01234567";
partitions = [
(gap (size.MiB 10))
(fileSystem.makeFAT32 {
# Size-less
name = "FIRMWARE";
partitionID = "ABADF00D";
extraPadding = size.MiB 10;
populateCommands = ''
(
src=${pkgsAArch64.raspberrypifw}/share/raspberrypi/boot
cp $src/bootcode.bin $src/fixup*.dat $src/start*.elf ./
cp ${pkgsAArch64.ubootRaspberryPi3_64bit}/u-boot.bin ./u-boot-rpi3.bin
cp ${configTxt} ./config.txt
)
'';
})
(fileSystem.makeExt4 {
bootable = true;
name = "NIXOS";
partitionID = "44444444-4444-4444-8888-888888888888";
populateCommands = ''
mkdir -p ./boot
cp ${kernel}/Image ./boot/kernel
cp ${scr}/boot.scr ./boot/boot.scr
'';
})
];
}

View File

@ -1,87 +0,0 @@
# Ensures we can fit stuff in an ext4 image.
{ pkgs ? import <nixpkgs> {} }:
let
inherit (pkgs) imageBuilder;
makeNull = size: let
filename = "null.img";
filesystemType = "FAT32"; # meh, good enough
in
''
mkdir -p $out
dd if=/dev/zero of=./${toString size}.img bs=${toString size} count=1
'';
in
with imageBuilder;
{
eight = fileSystem.makeExt4 {
name = "eight";
partitionID = "44444444-4444-4444-0000-000000000008";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 8)}
'';
};
eleven = fileSystem.makeExt4 {
name = "eleven";
partitionID = "44444444-4444-4444-0000-000000000011";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 11)}
'';
};
sixteen = fileSystem.makeExt4 {
name = "sixteen";
partitionID = "44444444-4444-4444-0000-000000000016";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 16)}
'';
};
one_twenty_eight = fileSystem.makeExt4 {
name = "one_twenty_eight";
partitionID = "44444444-4444-4444-0000-000000000128";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 128)}
'';
};
two_fifty_six = fileSystem.makeExt4 {
name = "two_fifty_six";
partitionID = "44444444-4444-4444-0000-000000000256";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 256)}
'';
};
five_twelve = fileSystem.makeExt4 {
name = "five_twelve";
partitionID = "44444444-4444-4444-0000-000000000512";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 512)}
'';
};
with_space = fileSystem.makeExt4 {
name = "with_space";
partitionID = "44444444-4444-4444-0000-000000000005";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 5)}
'';
extraPadding = size.MiB 10;
};
# Fills 512 MiB (the downard slump in the high fudge factor) with 512 1MiB
# files so we ensure the filesystem overhead is accounted for.
multiple-files = fileSystem.makeExt4 {
name = "multiple-files";
partitionID = "44444444-4444-4444-0000-000000000512";
populateCommands = ''
for i in {1..512}; do
dd if=/dev/zero of=./$i.img bs=${toString (imageBuilder.size.MiB 1)} count=1
done
'';
};
}

View File

@ -1,5 +0,0 @@
[
(self: super: { imageBuilder = self.callPackage ../. {}; })
# All the software will be upstreamed with NixOS when upstreaming the library.
(import ../../../overlay/overlay.nix)
]

View File

@ -1,18 +0,0 @@
# Adds the imageBuilder overlay.
(import ../overlay.nix) ++
[
# Makes the imageBuilder build impure to force rebuilds to more easily test
# reproducibility of outputs.
(self: super:
let
inherit (self.lib.attrsets) mapAttrs;
in
{
imageBuilder = super.imageBuilder.overrideScope'(self: super: {
makeFilesystem = args: super.makeFilesystem (args // {
REBUILD = "# ${toString builtins.currentTime}";
});
});
}
)
]

View File

@ -1,61 +0,0 @@
#!/usr/bin/env ruby
require "shellwords"
require "open3"
# Test harness to validate results of a `nix-build`.
# This script will `#load()` the given script.
if ARGV.length < 2 then
abort "Usage: verify.rb <script> <result>"
end
$failures = []
$script = ARGV.shift
$result = ARGV.shift
module Helpers
def compare_output(cmd, expected, message: nil)
expected ||= ""
message ||= "Command `#{cmd.shelljoin}` has unexpected output:"
expected = expected.strip
out = `#{cmd.shelljoin}`.strip
unless out == expected then
$failures << "#{message}:\n\tGot: #{out}\n\tExpected #{expected}"
end
end
def sha256sum(filename, expected)
filename = File.join($result, filename)
compare_output(
["nix-hash", "--flat", "--type", "sha256", filename], expected,
message: "File #{filename.shellescape} has unexpected hash",
)
end
def file(filename, expected)
filename = File.join($result, filename)
compare_output(
["file", "--dereference", "--brief", filename], expected,
message: "File #{filename.shellescape} has unexpected file type",
)
end
end
include Helpers
# Executes the script
load($script)
at_exit do
if $failures.length > 0 then
puts "Verification failed:"
$failures.each do |failure|
puts "#{failure}"
end
exit 1
end
exit 0
end

View File

@ -1,20 +0,0 @@
{ lib, imageBuilder, libfaketime, btrfs-progs }:
/* */ let scope = { "fileSystem.makeBtrfs" =
{ partitionID, ... }@args:
imageBuilder.makeFilesystem (args // {
filesystemType = "btrfs";
blockSize = 4096; # dummy
nativeBuildInputs = [btrfs-progs];
copyPhase = ''
mkfs.btrfs \
-r . \
-L "$partName" \
-U "$partitionID" \
--shrink \
"$img"
'';
})
/* */ ;}; in scope."fileSystem.makeBtrfs"

View File

@ -1,122 +0,0 @@
{ lib, imageBuilder, libfaketime, e2fsprogs, make_ext4fs }:
/* */ let scope = { "fileSystem.makeExt4" =
let
inherit (lib.strings) splitString;
inherit (imageBuilder) makeFilesystem;
# Bash doesn't do floating point representations. Multiplications and divisions
# are handled with enough precision that we can multiply and divide to get a precision.
precision = 1000;
first = list: lib.lists.last (lib.lists.reverseList list);
chopDecimal = f: first (splitString "." (toString f));
makeFudge = f: toString (chopDecimal (f * precision));
# This applies only to 256MiB and greater.
# For smaller than 256MiB images the overhead from the FS is much greater.
# This will also let *some* slack space at the end at greater sizes.
# This is the value at 512MiB where it goes slightly down compared to 256MiB.
fudgeFactor = makeFudge 0.05208587646484375;
# This table was built using a script that built an image with `make_ext4fs`
# for the given size in MiB, and recorded the available size according to `df`.
smallFudgeLookup = lib.strings.concatStringsSep "\n" (lib.lists.reverseList(
lib.attrsets.mapAttrsToList (size: factor: ''
elif (( size > ${toString size} )); then
fudgeFactor=${toString factor}
'') {
"${toString (imageBuilder.size.MiB 5)}" = makeFudge 0.84609375;
"${toString (imageBuilder.size.MiB 8)}" = makeFudge 0.5419921875;
"${toString (imageBuilder.size.MiB 16)}" = makeFudge 0.288818359375;
"${toString (imageBuilder.size.MiB 32)}" = makeFudge 0.1622314453125;
"${toString (imageBuilder.size.MiB 64)}" = makeFudge 0.09893798828125;
"${toString (imageBuilder.size.MiB 128)}" = makeFudge 0.067291259765625;
"${toString (imageBuilder.size.MiB 256)}" = makeFudge 0.0518646240234375;
}
));
minimumSize = imageBuilder.size.MiB 5;
in
{ partitionID
, blockSize ? imageBuilder.size.KiB 4
, ... } @ args:
makeFilesystem (args // {
filesystemType = "ext4";
inherit blockSize minimumSize;
nativeBuildInputs = [
e2fsprogs
make_ext4fs
libfaketime
];
filesystemPhase = ''
:
'';
computeMinimalSize = ''
# `local size` is in bytes.
# We don't have a static reserved factor figured out. It is rather hard with
# ext4fs as there are multiple factors increasing the overhead.
local reservedSize=0
local fudgeFactor=${toString fudgeFactor}
# Instead we rely on a lookup table. See how it is built in the derivation file.
if (( size < ${toString (imageBuilder.size.MiB 256)} )); then
echo "$size is smaller than 256MiB; using the lookup table." 1>&2
# A bit of a hack, though allows us to build the lookup table using only
# elifs.
if false; then
:
${smallFudgeLookup}
else
# The data is smaller than 5MiB... The filesystem image size will likely
# not be able to accomodate... here we handle it in another way.
fudgeFactor=0
echo "Fudge factor skipped for extra small partition. Instead increasing by a fixed amount." 1>&2
size=$(( size + ${toString minimumSize}))
fi
fi
local reservedSize=$(( size * $fudgeFactor / ${toString precision} ))
echo "Fudge factor: $fudgeFactor / ${toString precision}" 1>&2
echo -n "Adding reservedSize: $size + $reservedSize = " 1>&2
size=$((size + reservedSize))
echo "$size" 1>&2
'';
copyPhase = ''
echo "Computing inode count..."
inodes=$(find . ! -type d -print0 | du --files0-from=- --inodes | cut -f1 | sum-lines)
echo " Min inodes: $inodes" 1>&2
inodes=$(( inodes * 2 ))
echo " Inodes reserved: $inodes" 1>&2
echo ""
faketime -f "1970-01-01 00:00:01" \
make_ext4fs \
-i $inodes \
-b $blockSize \
-L $partName \
-l $size \
-U $partitionID \
"$img" \
.
'';
checkPhase = ''
'';
# FIXME:
# Padding at end of inode bitmap is not set. Fix? no
# exit code
#EXT2FS_NO_MTAB_OK=yes fsck.ext4 -n -f $img
})
/* */ ;}; in scope."fileSystem.makeExt4"

View File

@ -1,107 +0,0 @@
{ lib, imageBuilder, dosfstools, mtools, libfaketime}:
/* */ let scope = { "fileSystem.makeFAT32" =
let
inherit (lib.strings) splitString;
inherit (imageBuilder) makeFilesystem;
# The default from `mkfs.fat`.
reservedSectors = 32;
# The default from `mkfs.fat`.
hiddenSectors = 0;
# The default from `mkfs.fat`.
numberOfFats = 2;
# Extra padding per FAT, a constant in code
fatPadding = 4;
# I have not been able to validate that it could be different from 1 for FAT32.
# It seems the different values (e.g. 4) are for FAT12 and FAT16.
# This is the only "bad" assumption here.
clusterSize = 1;
# Bash doesn't do floating point representations. Multiplications and divisions
# are handled with enough precision that we can multiply and divide to get a precision.
precision = 1000;
first = list: lib.lists.last (lib.lists.reverseList list);
chopDecimal = f: first (splitString "." (toString f));
in
{ partitionID
# These defaults are assuming small~ish FAT32 filesystems are generated.
, blockSize ? 512
, sectorSize ? 512
, ... } @ args:
makeFilesystem (args // {
# FAT32 can be used for ESP. Let's make this obvious.
filesystemType = if args ? filesystemType then args.filesystemType else "FAT32";
inherit blockSize sectorSize;
minimumSize = imageBuilder.size.KiB 500;
nativeBuildInputs = [
libfaketime
dosfstools
mtools
];
computeMinimalSize = ''
# `local size` is in bytes.
# This amount is a static amount of reserved space.
local static_reserved=${toString ( (reservedSectors + hiddenSectors) * sectorSize )}
# This is a constant representing the relative reserved space ratio.
local relative_reserved=${
chopDecimal (
precision - (
1.0 * sectorSize / ((clusterSize * sectorSize) + (numberOfFats * fatPadding))
# ^ forces floating point
) * precision
)
}
# Rounds up the likely truncated result. At worst it's a bit more space.
(( relative_reserved++ ))
echo "static_reserved=$static_reserved" 1>&2
echo "relative_reserved=$relative_reserved" 1>&2
local reservedSize=$(( (static_reserved + size) * relative_reserved / ${toString precision} + static_reserved ))
echo -n "Adding reservedSize: $size + $reservedSize = " 1>&2
size=$((size + reservedSize))
echo "$size" 1>&2
'';
filesystemPhase = ''
fatSize=16
if (( size > 1024*1024*32 )); then
fatSize=32
fi
faketime -f "1970-01-01 00:00:01" mkfs.vfat \
-F $fatSize \
-R ${toString reservedSectors} \
-h ${toString hiddenSectors} \
-s ${toString (blockSize / sectorSize)} \
-S ${toString sectorSize} \
-i $partitionID \
-n $partName \
"$img"
'';
copyPhase = ''
for f in ./* ./.*; do
if [[ "$f" != "./." && "$f" != "./.." ]]; then
faketime -f "1970-01-01 00:00:01" \
mcopy -psv -i "$img" "$f" ::
fi
done
'';
checkPhase = ''
# Always verify FS
fsck.vfat -vn "$img"
'';
})
/* */ ;}; in scope."fileSystem.makeFAT32"

View File

@ -1,156 +0,0 @@
{ stdenvNoCC, lib, writeText }:
/* */ let scope = { "fileSystem.makeFilesystem" =
let
inherit (lib) optionals optionalString assertMsg;
in
{
name
# Size (in bytes) the filesystem image will be given.
# When size is not given, it is assumed that `populateCommands` will populate
# the filesystem, and the size will be derived (see computeMinimalSize).
, size ? null
# The populate commands are executed in a subshell. The CWD at the star is the
# public API to know where to add files that will be added to the image.
, populateCommands ? null
# Used with the assumption that files are rounded up to blockSize increments.
, blockSize
# Additional commands to compute a required increase in size to fit files.
, computeMinimalSize ? null
# When automatic sizing is used, additional amount of bytes to pad the image by.
, extraPadding ? 0
, ...
} @ args:
assert lib.asserts.assertMsg
(size !=null || populateCommands != null)
"Either a size or populateCommands needs to be given to build a filesystem.";
let
partName = name;
in
stdenvNoCC.mkDerivation (args // rec {
# Do not inherit `size`; we don't want to accidentally use it. The `size` can
# be dynamic depending on the contents.
inherit partName blockSize;
name = "partition-${partName}";
filename = "${partName}.img";
img = "${placeholder "out"}/${filename}";
nativeBuildInputs = [
] ++ optionals (args ? nativeBuildInputs) args.nativeBuildInputs;
buildCommand = ''
adjust-minimal-size() {
size="$1"
echo "$size"
}
compute-minimal-size() {
local size=0
(
cd files
# Size rounded in blocks. This assumes all files are to be rounded to a
# multiple of blockSize.
# Use of `--apparent-size` is to ensure we don't get the block size of the underlying FS.
# Use of `--block-size` is to get *our* block size.
size=$(find . ! -type d -print0 | du --files0-from=- --apparent-size --block-size "$blockSize" | cut -f1 | sum-lines)
echo "Reserving $size sectors for files..." 1>&2
# Adds one blockSize per directory, they do take some place, in the end.
# FIXME: write test to confirm this assumption
local directories=$(find . -type d | wc -l)
echo "Reserving $directories sectors for directories..." 1>&2
size=$(( directories + size ))
size=$((size * blockSize))
${if computeMinimalSize == null then "" else computeMinimalSize}
size=$(( size + ${toString extraPadding} ))
echo "$size"
)
}
sum-lines() {
local acc=0
while read -r number; do
acc=$((acc+number))
done
echo "$acc"
}
# The default stdenv/generic clashes with `runHook`.
# It doesn't override as expected.
unset -f checkPhase
mkdir -p $out
mkdir -p files
${optionalString (populateCommands != null) ''
echo
echo "Populating disk image"
echo
(
cd files
${populateCommands}
)
''}
${optionalString (size == null) ''
size=$(compute-minimal-size)
''}
if (( size < minimumSize )); then
size=$minimumSize
echo "WARNING: the '$partName' partition was too small, size increased to $minimumSize bytes."
fi
echo
echo "Building partition ${partName}"
echo "With ${if size == null
then "automatic size ($size bytes)"
else "$size bytes"
}"
echo
echo " -> Allocating space"
truncate -s $size "$img"
echo " -> Making filesystem"
runHook filesystemPhase
echo " -> Copying files"
(
cd files
runHook copyPhase
)
echo " -> Checking filesystem"
echo "$checkPhase"
runHook checkPhase
if [ -n "$postProcess" ]; then
echo "-> Running post-processing"
runHook postProcess
fi
'';
})
# mkdir -p $out/nix-support
# cat ${writeText "${name}-metadata" (builtins.toJSON {
# inherit size;
# })} > $out/nix-support/partition-metadata.json
/* */ ;}; in scope."fileSystem.makeFilesystem"

View File

@ -1,190 +0,0 @@
{ stdenvNoCC, lib
, imageBuilder
, vboot_reference
}:
/* */ let scope = { "diskImage.makeGPT" =
let
inherit (lib) concatMapStringsSep optionalString;
# List of known mappings of GPT partition types to filesystems.
# This is not exhaustive, only used as a default.
# See also: https://sourceforge.net/p/gptfdisk/code/ci/master/tree/parttypes.cc
types = {
"FAT32" = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7";
"ESP" = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B";
"LUKS" = "CA7D7CCB-63ED-4C53-861C-1742536059CC";
"ext2" = "0FC63DAF-8483-4772-8E79-3D69D8477DE4";
"ext3" = "0FC63DAF-8483-4772-8E79-3D69D8477DE4";
"ext4" = "0FC63DAF-8483-4772-8E79-3D69D8477DE4";
"btrfs" = "0FC63DAF-8483-4772-8E79-3D69D8477DE4";
};
in
{
name
, partitions
, diskID
, headerHole ? 0 # in bytes
, postProcess ? null
}:
let
_name = name;
eachPart = partitions: fn: (
concatMapStringsSep "\n" (partition:
fn partition
) partitions);
# Default alignment.
alignment = toString (imageBuilder.size.MiB 1);
image = partition:
if lib.isDerivation partition then
"${partition}/${partition.filename}"
else
partition.filename
;
in
stdenvNoCC.mkDerivation rec {
name = "disk-image-${_name}";
filename = "${_name}.img";
img = "${placeholder "out"}/${filename}";
inherit
postProcess
;
nativeBuildInputs = [
vboot_reference
];
buildCommand = let
# This fragment is used to compute the (aligned) size of the partition.
# It is used *only* to track the tally of the space used, thus the starting
# offset of the next partition. The filesystem sizes are untouched.
sizeFragment = partition: ''
start=$totalSize
${
if partition ? length then
''size=$((${toString partition.length}))''
else
''size=$(($(du --apparent-size -B 512 "$input_img" | awk '{ print $1 }') * 512))''
}
size=$(( $(if (($size % ${alignment})); then echo 1; else echo 0; fi ) + size / ${alignment} ))
size=$(( size * ${alignment} ))
totalSize=$(( totalSize + size ))
echo "Partition: start $start | size $size | totalSize $totalSize"
'';
# This fragment is used to add the desired gap to `totalSize`.
# We're setting `start` and `size` only to mirror the information shown
# for partitions.
# Do note that gaps are always aligned, so two gaps sized half the alignment
# would create 2× the space expected.
# What may *instead* be done at one point is always align `start` for partitions.
gapFragment = partition: ''
start=$totalSize
size=${toString partition.length}
size=$(( $(if (($size % ${alignment})); then echo 1; else echo 0; fi ) + size / ${alignment} ))
totalSize=$(( totalSize + size ))
echo "Gap: start $start | size $size | totalSize $totalSize"
'';
in ''
mkdir -p $out
# 34 is the base GPT header size, as added to -p by cgpt.
gptSize=$((${toString headerHole} + 34*512))
touch commands.sh
cat <<EOF > commands.sh
# Zeroes the GPT
cgpt create -z $img
# Create the GPT with space if desired
cgpt create -p ${toString (headerHole / 512)} $img
# Add the PMBR
cgpt boot -p $img
EOF
totalSize=$((gptSize))
echo
echo "Gathering information about partitions."
${eachPart partitions (partition:
if partition ? isGap && partition.isGap then
(gapFragment partition)
else
''
input_img="${image partition}"
${sizeFragment partition}
echo " -> ${partition.name}: $size / ${if partition ? filesystemType then partition.filesystemType else ""}"
(
printf "cgpt add"
printf " -b %s" "$((start/512))"
printf " -s %s" "$((size/512))"
printf " -t %s" '${
if partition ? partitionType then
partition.partitionType
else
types.${partition.filesystemType}
}'
${optionalString (partition ? partitionUUID)
"printf ' -u %s' '${partition.partitionUUID}'"}
${optionalString (partition ? bootable && partition.bootable)
"printf ' -B 1'"}
${optionalString (partition ? partitionLabel)
"printf ' -l \"%s\"' '${partition.partitionLabel}'"}
printf " $img\n"
) >> commands.sh
''
)}
# Allow space for secondary partition table / header.
totalSize=$(( totalSize + 34*512 ))
echo "--- script ----"
cat commands.sh
echo "--- script ----"
echo
echo "Making image, $totalSize bytes..."
truncate -s $((totalSize)) $img
PS4=" > " sh -x commands.sh
totalSize=$((gptSize))
echo
echo "Writing partitions into image"
${eachPart partitions (partition:
if partition ? isGap && partition.isGap then
(gapFragment partition)
else
''
input_img="${image partition}"
${sizeFragment partition}
echo " -> ${partition.name}: $size / ${if partition ? filesystemType then partition.filesystemType else ""}"
echo "$start / $size"
dd conv=notrunc if=$input_img of=$img seek=$((start/512)) count=$((size/512)) bs=512
''
)}
echo
echo "Information about the image:"
ls -lh $img
cgpt show $img
if [ -n "$postProcess" ]; then
echo "-> Running post-processing"
runHook postProcess
fi
'';
}
/* */ ;}; in scope."diskImage.makeGPT"

View File

@ -1,140 +0,0 @@
{ stdenvNoCC, lib
, imageBuilder
, util-linux
}:
/* */ let scope = { "diskImage.makeMBR" =
let
inherit (lib) concatMapStringsSep optionalString;
# List of known mappings of MBR partition types to filesystems.
types = {
"FAT32" = "b";
"ESP" = "ef";
"ext2" = "83";
"ext3" = "83";
"ext4" = "83";
};
in
{
name
, partitions
# Without the prefixed `0x`
, diskID
}:
let
_name = name;
eachPart = partitions: fn: (
concatMapStringsSep "\n" (partition:
fn partition
) partitions);
# Default alignment.
alignment = toString (imageBuilder.size.MiB 1);
in
stdenvNoCC.mkDerivation rec {
name = "disk-image-${_name}";
filename = "${_name}.img";
img = "${placeholder "out"}/${filename}";
nativeBuildInputs = [
util-linux
];
buildCommand = let
# This fragment is used to compute the (aligned) size of the partition.
# It is used *only* to track the tally of the space used, thus the starting
# offset of the next partition. The filesystem sizes are untouched.
sizeFragment = ''
start=$totalSize
size=$(($(du --apparent-size -B 512 "$input_img" | awk '{ print $1 }') * 512))
size=$(( $(if (($size % ${alignment})); then echo 1; else echo 0; fi ) + size / ${alignment} ))
size=$(( size * ${alignment} ))
totalSize=$(( totalSize + size ))
echo "Partition: start $start | size $size | totalSize $totalSize"
'';
# This fragment is used to add the desired gap to `totalSize`.
# We're setting `start` and `size` only to mirror the information shown
# for partitions.
# Do note that gaps are always aligned, so two gaps sized half the alignment
# would create 2× the space expected.
# What may *instead* be done at one point is always align `start` for partitions.
gapFragment = partition: ''
start=$totalSize
size=${toString partition.length}
size=$(( $(if (($size % ${alignment})); then echo 1; else echo 0; fi ) + size / ${alignment} ))
size=$(( size * ${alignment} ))
totalSize=$(( totalSize + size ))
echo "Gap: start $start | size $size | totalSize $totalSize"
'';
in ''
mkdir -p $out
cat <<EOF > script.sfdisk
label: dos
grain: 1024
label-id: 0x${diskID}
EOF
totalSize=${alignment}
echo
echo "Gathering information about partitions."
${eachPart partitions (partition:
if partition ? isGap && partition.isGap then
(gapFragment partition)
else
''
input_img="${partition}/${partition.filename}"
${sizeFragment}
echo " -> ${partition.name}: $size / ${partition.filesystemType}"
(
# The size is /1024; otherwise it's in sectors.
echo -n 'start='"$((start/1024))"'KiB'
echo -n ', size='"$((size/1024))"'KiB'
echo -n ', type=${types."${partition.filesystemType}"}'
${optionalString (partition ? bootable && partition.bootable)
"echo -n ', bootable'"}
echo "" # Finishes the command
) >> script.sfdisk
''
)}
echo "--- script ----"
cat script.sfdisk
echo "--- script ----"
echo
echo "Making image, $totalSize bytes..."
truncate -s $((totalSize)) $img
sfdisk $img < script.sfdisk
totalSize=${alignment}
echo
echo "Writing partitions into image"
${eachPart partitions (partition:
if partition ? isGap && partition.isGap then
(gapFragment partition)
else
''
input_img="${partition}/${partition.filename}"
${sizeFragment}
echo " -> ${partition.name}: $size / ${partition.filesystemType}"
echo "$start / $size"
dd conv=notrunc if=$input_img of=$img seek=$((start/512)) count=$((size/512)) bs=512
''
)}
echo
echo "Information about the image:"
ls -lh $img
sfdisk -V --list $img
'';
}
/* */ ;}; in scope."diskImage.makeMBR"

View File

@ -1,144 +0,0 @@
#!/usr/bin/env nix-shell
# All dependencies `verify` will need too.
#!nix-shell --pure -p nix -p ruby -p file -i ruby
require "tmpdir"
require "open3"
require "json"
# require "fileutils"
prefix = File.join(__dir__, "tests")
NIX_PATH = "nixpkgs-overlays=#{__dir__}/lib/tests/test-overlay.nix:nixpkgs=channel:nixos-19.03"
# Default directives for the test.
DEFAULT_DIRECTIVES = {
# Default is to succeed.
status: 0,
# Nothing to grep for in particular.
# (Caution! Successes are likely not to have logs!)
grep: nil,
}
Env = {
"NIX_PATH" => NIX_PATH,
}
tests =
if ARGV.count > 0 then
ARGV
else
# Assumes all nix files in `./tests` are tests to `nix-build`.
Dir.glob(File.join(prefix, "**/*.nix"))
end
tests.sort!
$exit = 0
$failed_tests = []
puts ""
puts "Tests"
puts "====="
puts ""
tests.each_with_index do |file, index|
short_name = file.sub("#{prefix}/", "")
print "Running test #{(index+1).to_s.rjust(tests.length.to_s.length)}/#{tests.length} '#{short_name}' "
failures = []
# Reads the first line of the file. It may hold a json snippet
# configuring the expected test results.
directives = File.read(file).split("\n").first || ""
directives = DEFAULT_DIRECTIVES.merge(
if directives.match(/^\s*#\s*expect:/i) then
# Parse...
JSON.parse(
# Everything after the first colon as json
directives.split(":", 2).last,
symbolize_names: true,
)
else
{}
end
)
# The result symlink is in a temp directory...
Dir.mktmpdir("image-builder-test") do |dir|
result = File.join(dir, "result")
# TODO : figure out how to keep stdout/stderr synced but logged separately.
log, status = Open3.capture2e(Env, "nix-build", "--show-trace", "--out-link", result, file)
unless status.exitstatus == directives[:status]
failures << "Build exited with status #{status.exitstatus} expected #{directives[:status]}."
end
if directives[:grep] then
unless log.match(directives[:grep])
failures << "Output log did not match `#{directives[:grep]}`."
end
end
# Do we test further with the test scripts?
if failures.length == 0 and status.success? then
script = file.sub(/\.nix$/, ".rb")
if File.exists?(script) then
log, status = Open3.capture2e(Env, File.join(__dir__, "lib/tests/verify.rb"), script, result)
end
unless status.exitstatus == 0
failures << "Verification exited with status #{status.exitstatus} expected 0."
end
store_path = File.readlink(result)
end
if failures.length == 0 then
puts "[Success] (#{store_path || "no output"}) "
else
$exit = 1
$failed_tests << file
puts "[Failed] (#{store_path || "no output"}) "
puts ""
puts "Failures:"
failures.each do |failure|
puts " - #{failure}"
end
puts ""
puts "Directives:"
puts ""
puts "```"
puts "#{directives.inspect}"
puts "```"
puts ""
puts "Output from the test:"
puts ""
puts "````"
puts "#{log}"
puts "````"
end
end
end
puts ""
puts "* * *"
puts ""
puts "Test summary"
puts "============"
puts ""
puts "#{tests.length - $failed_tests.length}/#{tests.length} tests successful."
if $failed_tests.length > 0 then
puts ""
puts "Failed tests:\n"
$failed_tests.each do |filename|
puts " - #{filename.sub(/^#{__dir__}/, ".")}"
end
puts ""
end
exit $exit

View File

@ -1,50 +0,0 @@
# Verifies that filesystems sized to be aligned works.
{ pkgs ? import <nixpkgs> {} }:
let
inherit (pkgs) imageBuilder;
makeNull = size: pkgs.runCommand "filesystems-test" {
filename = "null.img";
filesystemType = "FAT32"; # meh, good enough
} ''
mkdir -p $out
dd if=/dev/zero of=$out/$filename bs=${toString size} count=1
'';
in
with imageBuilder;
{
one = diskImage.makeMBR {
name = "diskimage";
diskID = "012345678";
partitions = [
(makeNull (size.MiB 1))
(makeNull (size.MiB 1))
];
};
nine = diskImage.makeMBR {
name = "diskimage";
diskID = "012345678";
partitions = [
(makeNull (size.MiB 9))
(makeNull (size.MiB 9))
];
};
ten = diskImage.makeMBR {
name = "diskimage";
diskID = "012345678";
partitions = [
(makeNull (size.MiB 10))
(makeNull (size.MiB 10))
];
};
eleven = diskImage.makeMBR {
name = "diskimage";
diskID = "012345678";
partitions = [
(makeNull (size.MiB 11))
(makeNull (size.MiB 11))
];
};
}

View File

@ -1,42 +0,0 @@
# Verifies that filesystems sized to be unaligned will work.
{ pkgs ? import <nixpkgs> {} }:
let
inherit (pkgs) imageBuilder;
makeNull = size: pkgs.runCommand "filesystems-test" {
filename = "null.img";
filesystemType = "FAT32"; # meh, good enough
} ''
mkdir -p $out
dd if=/dev/zero of=$out/$filename bs=${toString size} count=1
'';
in
with imageBuilder;
# Through empirical testing, it was found out that the defaults from `sfdisk`
# are not as documented.
# First of all, on small disks no alignment will be made.
# Starting with 3MiB (empirically derived) alignment will be made.
# The alignment is documented as being based on the I/O limits. It seems like
# for files it ends up causing alignments at the 2MiB boundaries.
# Such, `grain: 1024` has to be set to configure sfdisk for the sane default
# documented in its manpage
#
# > grain Specify minimal size in bytes used to calculate partitions alignment.
# > The default is 1MiB and it's strongly recommended to use the default.
# > Do not modify this variable if you're not sure.
#
# The default is *not* 1MiB and will break the generation of images if it tries
# to align a small partition at the very end of the disk, when the disk is sized
# just right to fit.
#
# This is what this test validates.
diskImage.makeMBR {
name = "diskimage";
diskID = "012345678";
partitions = [
(makeNull (size.MiB 3))
(makeNull (size.MiB 1))
];
}

View File

@ -1,13 +0,0 @@
# Expect: { "status": 1, "grep": "Either a size or populateCommands needs to be given to build a filesystem." }
{ pkgs ? import <nixpkgs> {} }:
let
inherit (pkgs) imageBuilder;
in
with imageBuilder;
fileSystem.makeFAT32 {
name = "whatever";
partitionID = "0123456789ABCDEF";
}

View File

@ -1,35 +0,0 @@
# Tests all known filesystems as empty, with defaults.
# This test helps ensure the basic interface stays stable, and works.
{ pkgs ? import <nixpkgs> {} }:
let
inherit (pkgs) imageBuilder;
inherit (pkgs.lib.attrsets) mapAttrsToList;
inherit (pkgs.lib.strings) concatStringsSep removePrefix;
IDs = {
FAT32 = "0123456789ABCDEF";
ESP = "0123456789ABCDEF";
Ext4 = "44444444-4444-4444-1324-123456789098";
};
in
with imageBuilder;
let
cmds =
mapAttrsToList (fn_name: fn:
let
fs = fn rec {
name = removePrefix "make" fn_name;
size = imageBuilder.size.MiB 10;
partitionID = IDs."${name}";
};
in
''
ln -s ${fs}/${fs.filename} $out/
'') fileSystem;
in
pkgs.runCommand "filesystems-test" {} ''
mkdir -p $out/
${concatStringsSep "\n" cmds}
''

View File

@ -1,19 +0,0 @@
hashes = {
"ESP.img" => "f9b39d98bfb797b050467ca5214671b5b427b7896cae44d27d2fc8dbceaccd88",
"FAT32.img" => "79028d8af97ae4400ea2ab36d34e2a80684c9f8d31ea75e3f54d908c75adc3a4",
"Ext4.img" => "8182df3038f43cdf2aba6f3980d9fa794affab0f040f9c07450ebbf0d3d8c2ad",
}
filetypes = {
"ESP.img" => 'DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", reserved sectors 32, root entries 512, sectors 20480 (volumes <=32 MB), Media descriptor 0xf8, sectors/FAT 80, sectors/track 32, heads 64, serial number 0x89abcdef, label: "ESP ", FAT (16 bit)',
"FAT32.img" => 'DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", reserved sectors 32, root entries 512, sectors 20480 (volumes <=32 MB), Media descriptor 0xf8, sectors/FAT 80, sectors/track 32, heads 64, serial number 0x89abcdef, label: "FAT32 ", FAT (16 bit)',
"Ext4.img" => 'Linux rev 1.0 ext4 filesystem data, UUID=44444444-4444-4444-1324-123456789098, volume name "Ext4" (extents) (large files)'
}
# By globbing on the output, we can validate all built images are verified.
# The builder should have built everything under `fileSystems`.
Dir.glob(File.join($result, "**/*")) do |file|
name = File.basename(file)
sha256sum(name, hashes[name])
file(name, filetypes[name])
end

View File

@ -1,39 +0,0 @@
# Tests all known filesystems with files.
# This test helps ensure the basic interface stays stable, and works.
{ pkgs ? import <nixpkgs> {} }:
let
inherit (pkgs) imageBuilder;
inherit (pkgs.lib.attrsets) mapAttrsToList;
inherit (pkgs.lib.strings) concatStringsSep removePrefix;
IDs = {
FAT32 = "0123456789ABCDEF";
ESP = "0123456789ABCDEF";
Ext4 = "44444444-4444-4444-1324-123456789098";
};
in
with imageBuilder;
let
cmds =
mapAttrsToList (fn_name: fn:
let
fs = fn rec {
name = removePrefix "make" fn_name;
partitionID = IDs."${name}";
populateCommands = ''
echo "I am ${name}." > file
ls -lA
'';
};
in
''
ln -s ${fs}/${fs.filename} $out/
'') fileSystem;
in
pkgs.runCommand "filesystems-test" {} ''
mkdir -p $out/
${concatStringsSep "\n" cmds}
''

View File

@ -1,19 +0,0 @@
hashes = {
"ESP.img" => "d70b24594f0615b83fddb8608b4819432380359bc5c6763d7c318e0c3233ea64",
"FAT32.img" => "73ab1acd845fd0f7f5ddd30edea074f0b14bf4f2b4137f904a90939f1cb2ac8d",
"Ext4.img" => "56181a43406c4ba731ed14b3ed32ed6c169b1c2abfe385eef989faef55e3cd07",
}
filetypes = {
"ESP.img" => 'DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", reserved sectors 32, root entries 512, sectors 1000 (volumes <=32 MB), Media descriptor 0xf8, sectors/FAT 3, sectors/track 32, heads 64, serial number 0x89abcdef, label: "ESP ", FAT (12 bit)',
"FAT32.img" => 'DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", reserved sectors 32, root entries 512, sectors 1000 (volumes <=32 MB), Media descriptor 0xf8, sectors/FAT 3, sectors/track 32, heads 64, serial number 0x89abcdef, label: "FAT32 ", FAT (12 bit)',
"Ext4.img" => 'Linux rev 1.0 ext4 filesystem data, UUID=44444444-4444-4444-1324-123456789098, volume name "Ext4" (extents) (large files)'
}
# By globbing on the output, we can validate all built images are verified.
# The builder should have built everything under `fileSystems`.
Dir.glob(File.join($result, "**/*")) do |file|
name = File.basename(file)
sha256sum(name, hashes[name])
file(name, filetypes[name])
end

View File

@ -1,70 +0,0 @@
# Ensures we can fit stuff in an ext4 image.
{ pkgs ? import <nixpkgs> {} }:
let
inherit (pkgs) imageBuilder;
makeNull = size: let
filename = "null.img";
filesystemType = "FAT32"; # meh, good enough
in
''
mkdir -p $out
dd if=/dev/zero of=./${toString size}.img bs=${toString size} count=1
'';
in
with imageBuilder;
{
one = fileSystem.makeExt4 {
name = "one";
partitionID = "44444444-4444-4444-0000-000000000001";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 1)}
'';
};
two = fileSystem.makeExt4 {
name = "two";
partitionID = "44444444-4444-4444-0000-000000000002";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 2)}
'';
};
three = fileSystem.makeExt4 {
name = "three";
partitionID = "44444444-4444-4444-0000-000000000003";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 3)}
'';
};
four = fileSystem.makeExt4 {
name = "four";
partitionID = "44444444-4444-4444-0000-000000000004";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 4)}
'';
};
five = fileSystem.makeExt4 {
name = "five";
partitionID = "44444444-4444-4444-0000-000000000005";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 5)}
'';
};
# This is the boundary where otherwise it would begin to fail.
five_plus_one = fileSystem.makeExt4 {
name = "five_plus_one";
partitionID = "44444444-4444-4444-0001-000000000005";
populateCommands = ''
${makeNull ((imageBuilder.size.MiB 5) + 1)}
'';
};
six = fileSystem.makeExt4 {
name = "six";
partitionID = "44444444-4444-4444-0000-000000000006";
populateCommands = ''
${makeNull (imageBuilder.size.MiB 6)}
'';
};
# For bigger tests, see in-depth-tests
}

View File

@ -0,0 +1,10 @@
{ pkgs, lib }:
{
evaluateFilesystemImage = { config ? {}, modules ? [] }: import ./filesystem-image/eval-config.nix {
inherit pkgs config modules;
};
evaluateDiskImage = { config ? {}, modules ? [] }: import ./disk-image/eval-config.nix {
inherit pkgs config modules;
};
}

View File

@ -0,0 +1,73 @@
{ config, lib, ... }:
let
inherit (lib)
mkOption
types
;
in
{
options = {
name = mkOption {
type = types.str;
default = "disk-image";
description = "Base name of the output";
};
alignment = mkOption {
type = types.int;
default = config.helpers.size.MiB 1;
description = lib.mdDoc ''
Partitions alignment.
Automatically computed partition start position will be aligned to
multiples of this value.
The default value is most likely appropriate.
'';
};
sectorSize = mkOption {
type = types.int;
default = 512;
internal = true;
description = lib.mdDoc ''
Sector size. This is used mainly internally. Changing this should have
no effects on the actual disk image produced.
The default value is most likely appropriate.
'';
};
output = mkOption {
type = types.package;
internal = true;
description = lib.mdDoc ''
The build output for the disk image.
'';
};
additionalCommands = mkOption {
type = types.str;
default = "";
description = lib.mdDoc ''
Additional commands to run during the disk image build.
'';
};
# TODO: implement this:
# mergeDerivationScripts = mkOption {
# type = types.bool;
# default = false;
# internal = true;
# description = lib.mdDoc ''
# Whether to produce discrete derivations for each steps, or to produce
# a single derivation that builds the image from A to Z.
#
# Setting this to true may be helpful with some CI/CD environments where
# limitations in output sizes makes it impossible to produce discrete
# derivations for every steps along the way.
# '';
# };
};
}

View File

@ -0,0 +1,18 @@
/**
* Evaluates the configuration for a disk image build.
*/
{ pkgs
, modules ? []
, config ? {}
}:
let config' = config; in
rec {
module = { imports = import ./module-list.nix; };
config = (pkgs.lib.evalModules {
modules = [
{ _module.args.pkgs = pkgs; }
module
config'
] ++ modules;
}).config;
}

View File

@ -0,0 +1,8 @@
# Note: No `filesystem` modules are to be added here. Filesystems are used as
# submodules in the partitions options.
[
../helpers.nix
./basic.nix
./partitioning-scheme
./partitions.nix
]

View File

@ -0,0 +1,27 @@
{ config, lib, ... }:
let
inherit (lib)
mkOption
types
;
in
{
imports = [
./gpt
./mbr
];
options = {
availablePartitioningSchemes = mkOption {
type = with types; listOf str;
internal = true;
};
partitioningScheme = mkOption {
type = types.enum config.availablePartitioningSchemes;
description = lib.mdDoc ''
Partitioning scheme for the disk image output.
'';
};
};
}

View File

@ -0,0 +1,197 @@
{ stdenvNoCC
, lib
, fetchpatch
, gptfdisk
, buildPackages
, utillinux
, config
}:
let
inherit (lib)
concatMapStringsSep
concatStringsSep
optionalString
;
inherit (config.helpers)
each
;
inherit (config)
partitions
;
inherit (config.gpt)
hybridMBR
;
in
stdenvNoCC.mkDerivation rec {
inherit (config)
name
alignment
sectorSize
additionalCommands
;
inherit (config.gpt)
diskID
partitionEntriesCount
;
img = placeholder "out";
nativeBuildInputs = [
gptfdisk
utillinux
];
buildCommand = let
# This fragment is used to compute the (aligned) size of the partition.
# It is used *only* to track the tally of the space used, thus the starting
# offset of the next partition. The filesystem sizes are untouched.
sizeFragment = partition: ''
${if (partition ? offset && partition.offset != null) then ''
# If a partition asks to start at a specific offset, restart tally at
# that location.
offset=$((${toString partition.offset}))
if (( offset < totalSize )); then
echo "Partition '${partition.name}' wanted to start at $offset while we were already at $totalSize"
echo "As of right now, partitions need to be in order."
exit 1
else
totalSize=$offset
fi
start=$totalSize
# *by design* we're not aligning the start of the partition here if an
# offset was given.
'' else ''
# Assume we start where we left off...
start=$totalSize
# Align to the nearest alignment
if (( start % alignment )); then
start=$(( start + (alignment - start % alignment) ))
fi
''}
${
if (partition ? length && partition.length != null) then
''size=$((${toString partition.length}))''
else
''size=$(($(du --apparent-size -B 512 "$input_img" | awk '{ print $1 }') * 512))''
}
size=$(( $(if ((size % alignment)); then echo 1; else echo 0; fi ) + size / alignment ))
size=$(( size * alignment ))
totalSize=$(( totalSize + size ))
# Align the end too
if (( totalSize % alignment )); then
totalSize=$(( totalSize + (alignment - totalSize % alignment) ))
fi
echo "Partition: start $start | size $size | totalSize $totalSize"
'';
# This fragment is used to add the desired gap to `totalSize`.
# We're setting `start` and `size` only to mirror the information shown
# for partitions.
gapFragment = partition: ''
start=$totalSize
size=${toString partition.length}
totalSize=$(( totalSize + size ))
echo "Gap: start $start | size $size | totalSize $totalSize"
'';
in ''
set -u
# LBA0 and LBA1 contains the PMBR and GPT.
#
# 2 is LBA2, where the header hole starts.
# One partition entry is 128 bytes long.
gptSize=$((2*512 + partitionEntriesCount*128))
cat <<EOF > script.sfdisk
label: gpt
unit: sectors
first-lba: $(( gptSize % sectorSize ? gptSize / sectorSize + 1 : gptSize / sectorSize ))
sector-size: $sectorSize
table-length: $partitionEntriesCount
grain: $alignment
${optionalString (diskID != null) ''
label-id: ${diskID}
''}
EOF
totalSize=$((gptSize))
echo
echo "Gathering information about partitions."
${each partitions (partition:
if partition ? isGap && partition.isGap then
(gapFragment partition)
else
''
input_img="${if partition.raw != null then partition.raw else ""}"
${sizeFragment partition}
echo ' -> '${lib.escapeShellArg partition.name}": $size / ${if partition ? filesystemType then partition.filesystemType else ""}"
(
echo -n 'start='"$((start/sectorSize))"
echo -n ', size='"$((size/sectorSize))"
echo -n ', type=${partition.partitionType}'
${optionalString (partition.partitionUUID != null)
"echo -n ', uuid=${partition.partitionUUID}'"}
${optionalString (partition ? bootable && partition.bootable)
''echo -n ', attrs="LegacyBIOSBootable"' ''}
${optionalString (partition ? requiredPartition && partition.requiredPartition)
''echo -n ', attrs="RequiredPartition"' ''}
${optionalString (partition ? partitionLabel)
''echo -n ', name="${partition.partitionLabel}"' ''}
echo "" # Finishes the command
) >> script.sfdisk
''
)}
# Allow space for secondary partition table / header.
totalSize=$(( totalSize + gptSize ))
echo "--- script ----"
cat script.sfdisk
echo "--- script ----"
echo
echo "Making image, $totalSize bytes..."
truncate -s $((totalSize)) $img
sfdisk $img < script.sfdisk
totalSize=$((gptSize))
echo
echo "Writing partitions into image"
${each partitions (partition:
if !(partition ? raw && partition.raw != null) && partition ? isGap && partition.isGap then
(gapFragment partition)
else
''
input_img="${if partition.raw != null then partition.raw else ""}"
if [[ "$input_img" == "" ]]; then
input_img="/dev/zero"
fi
${sizeFragment partition}
echo ' -> '${lib.escapeShellArg partition.name}": $size / ${if partition ? filesystemType then partition.filesystemType else ""}"
echo "$start / $size"
dd conv=notrunc if=$input_img of=$img seek=$((start/512)) count=$((size/512)) bs=512
''
)}
${optionalString (hybridMBR != []) ''
echo
echo "Making Hybrid MBR"
echo
sgdisk --hybrid=${concatStringsSep ":" hybridMBR} "$img"
''}
echo
echo "Information about the image:"
ls -lh $img
sfdisk -V --list $img
runHook additionalCommands
set +u
'';
}

View File

@ -0,0 +1,56 @@
{ config, lib, pkgs, ... }:
let
enabled = config.partitioningScheme == "gpt";
inherit (lib)
mkIf
mkMerge
mkOption
types
;
in
{
options.gpt = {
diskID = mkOption {
type = types.nullOr config.helpers.types.uuid;
default = null;
description = lib.mdDoc ''
Identifier for the disk.
'';
};
partitionEntriesCount = mkOption {
type = types.int;
default = 128;
description = lib.mdDoc ''
Number of partitions in the partition table.
The default value is likely appropriate.
'';
};
hybridMBR = mkOption {
type = with types; listOf str;
default = [];
example = [ "1" "3" "6" "EE" ];
description = lib.mdDoc ''
Creates an hybrid MBR with the given (string) partition numbers.
Up to three partitions can be present in the hybrid MBR, an additional
special partition can be placed last, named `EE`. When `EE` is present
last the protective partition for the GPT will be placed last.
See `man sgdisk`
'';
};
};
config = mkMerge [
{ availablePartitioningSchemes = [ "gpt" ]; }
(mkIf enabled {
output = pkgs.callPackage ./builder.nix {
inherit config;
};
})
];
}

View File

@ -0,0 +1,174 @@
{ stdenvNoCC
, lib
, fetchpatch
, buildPackages
, utillinux
, config
}:
let
inherit (lib)
concatMapStringsSep
concatStringsSep
optionalString
;
inherit (config.helpers)
each
;
inherit (config)
partitions
;
in
stdenvNoCC.mkDerivation rec {
inherit (config)
name
alignment
sectorSize
additionalCommands
;
inherit (config.mbr)
diskID
;
img = placeholder "out";
nativeBuildInputs = [
utillinux
];
buildCommand = let
# This fragment is used to compute the (aligned) size of the partition.
# It is used *only* to track the tally of the space used, thus the starting
# offset of the next partition. The filesystem sizes are untouched.
sizeFragment = partition: ''
${if (partition ? offset && partition.offset != null) then ''
# If a partition asks to start at a specific offset, restart tally at
# that location.
offset=$((${toString partition.offset}))
if (( offset < totalSize )); then
echo "Partition '${partition.name}' wanted to start at $offset while we were already at $totalSize"
echo "As of right now, partitions need to be in order."
exit 1
else
totalSize=$offset
fi
start=$totalSize
# *by design* we're not aligning the start of the partition here if an
# offset was given.
'' else ''
# Assume we start where we left off...
start=$totalSize
# Align to the nearest alignment
if (( start % alignment )); then
start=$(( start + (alignment - start % alignment) ))
fi
''}
${
if (partition ? length && partition.length != null) then
''size=$((${toString partition.length}))''
else
''size=$(($(du --apparent-size -B 512 "$input_img" | awk '{ print $1 }') * 512))''
}
size=$(( $(if ((size % alignment)); then echo 1; else echo 0; fi ) + size / alignment ))
size=$(( size * alignment ))
totalSize=$(( totalSize + size ))
# Align the end too
if (( totalSize % alignment )); then
totalSize=$(( totalSize + (alignment - totalSize % alignment) ))
fi
echo "Partition: start $start | size $size | totalSize $totalSize"
'';
# This fragment is used to add the desired gap to `totalSize`.
# We're setting `start` and `size` only to mirror the information shown
# for partitions.
gapFragment = partition: ''
start=$totalSize
size=${toString partition.length}
totalSize=$(( totalSize + size ))
echo "Gap: start $start | size $size | totalSize $totalSize"
'';
in ''
set -u
# LBA0 contains the MBR.
mbrSize=$((1*512))
cat <<EOF > script.sfdisk
label: dos
unit: sectors
first-lba: $(( mbrSize % sectorSize ? mbrSize / sectorSize + 1 : mbrSize / sectorSize ))
sector-size: $sectorSize
grain: $alignment
${optionalString (diskID != null) ''
label-id: ${diskID}
''}
EOF
totalSize=$((mbrSize))
echo
echo "Gathering information about partitions."
${each partitions (partition:
if partition ? isGap && partition.isGap then
(gapFragment partition)
else
''
input_img="${if partition.raw != null then partition.raw else ""}"
${sizeFragment partition}
echo ' -> '${lib.escapeShellArg partition.name}": $size / ${if partition ? filesystemType then partition.filesystemType else ""}"
(
echo -n 'start='"$((start/sectorSize))"
echo -n ', size='"$((size/sectorSize))"
echo -n ', type=${partition.partitionType}'
${optionalString (partition ? bootable && partition.bootable)
''echo -n ', bootable' ''}
echo "" # Finishes the command
) >> script.sfdisk
''
)}
# Allow space for secondary partition table / header.
totalSize=$(( totalSize + mbrSize ))
echo "--- script ----"
cat script.sfdisk
echo "--- script ----"
echo
echo "Making image, $totalSize bytes..."
truncate -s $((totalSize)) $img
sfdisk $img < script.sfdisk
totalSize=$((mbrSize))
echo
echo "Writing partitions into image"
${each partitions (partition:
if !(partition ? raw && partition.raw != null) && partition ? isGap && partition.isGap then
(gapFragment partition)
else
''
input_img="${if partition.raw != null then partition.raw else ""}"
if [[ "$input_img" == "" ]]; then
input_img="/dev/zero"
fi
${sizeFragment partition}
echo ' -> '${lib.escapeShellArg partition.name}": $size / ${if partition ? filesystemType then partition.filesystemType else ""}"
echo "$start / $size"
dd conv=notrunc if=$input_img of=$img seek=$((start/512)) count=$((size/512)) bs=512
''
)}
echo
echo "Information about the image:"
ls -lh $img
sfdisk -V --list $img
runHook additionalCommands
set +u
'';
}

View File

@ -0,0 +1,34 @@
{ config, lib, pkgs, ... }:
let
enabled = config.partitioningScheme == "mbr";
inherit (lib)
mkIf
mkMerge
mkOption
types
;
in
{
options.mbr = {
diskID = mkOption {
type = types.strMatching (
let hex = "[0-9a-fA-F]"; in
"${hex}{8}"
);
default = null;
description = lib.mdDoc ''
Identifier for the disk.
'';
};
};
config = mkMerge [
{ availablePartitioningSchemes = [ "mbr" ]; }
(mkIf enabled {
output = pkgs.callPackage ./builder.nix {
inherit config;
};
})
];
}

View File

@ -0,0 +1,153 @@
{ config, lib, pkgs, ... }:
let
inherit (lib)
mkIf
mkMerge
mkOption
types
;
listEntrySubmodule = {
options = {
};
};
inherit (config) helpers;
partitionSubmodule = { name, config, ... }: {
options = {
name = mkOption {
type = types.str;
description = lib.mdDoc ''
Identifier for the partition.
'';
};
partitionLabel = mkOption {
type = types.str;
default = config.name;
defaultText = lib.literalExpression "config.name";
description = lib.mdDoc ''
Partition label on supported partition schemes. Defaults to ''${name}.
Not to be confused with _filesystem_ label.
'';
};
partitionUUID = mkOption {
type = types.nullOr helpers.types.uuid;
default = null;
description = lib.mdDoc ''
Partition UUID, for supported partition schemes.
Not to be confused with _filesystem_ UUID.
Not to be confused with _partitionType_.
'';
};
length = mkOption {
type = types.nullOr types.int;
default = null;
description = lib.mdDoc ''
Size in bytes for the partition.
Defaults to the filesystem image length (computed at runtime).
'';
};
offset = mkOption {
type = types.nullOr types.int;
default = null;
description = lib.mdDoc ''
Offset (in bytes) the partition starts at.
Defaults to the next aligned location on disk.
'';
};
partitionType = mkOption {
type = types.oneOf [
helpers.types.uuid
(types.strMatching "[0-9a-fA-F]{2}")
];
default = "0FC63DAF-8483-4772-8E79-3D69D8477DE4";
defaultText = "Linux filesystem data (0FC63DAF-8483-4772-8E79-3D69D8477DE4)";
description = lib.mdDoc ''
Partition type UUID.
Not to be confused with _partitionUUID_.
See: https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs
'';
};
bootable = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc ''
Sets the "legacy bios bootable flag" on the partition.
'';
};
requiredPartition = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc ''
For GPT, sets the Required Partition attribute on the partition.
'';
};
filesystem = mkOption {
type = types.nullOr (types.submodule ({
imports = import (../filesystem-image/module-list.nix);
_module.args.pkgs = pkgs;
}));
default = null;
description = lib.mdDoc ''
A filesystem image configuration.
The filesystem image produced by this configuration is the default
value for the `raw` submodule option, unless overriden.
'';
};
isGap = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc ''
When set to true, only the length attribute is used, and describes
an unpartitioned span in the disk image.
'';
};
raw = mkOption {
type = with types; nullOr (oneOf [ package path ]);
defaultText = "[contents of the filesystem attribute]";
default = null;
description = lib.mdDoc ''
Raw image to be used as the partition content.
By default uses the output of the `filesystem` submodule.
'';
};
};
config = mkMerge [
(mkIf (!config.isGap && config.filesystem != null) {
raw = lib.mkDefault config.filesystem.output;
})
];
};
in
{
options = {
partitions = mkOption {
type = with types; listOf (submodule partitionSubmodule);
description = lib.mdDoc ''
List of partitions to include in the disk image.
'';
};
};
}

View File

@ -0,0 +1,262 @@
{ config, lib, pkgs, ... }:
let
inherit (lib)
mkOption
optionalString
types
;
inherit (config)
computeMinimalSize
extraPadding
size
;
in
{
options = {
name = mkOption {
type = types.str;
default = "filesystem-image";
description = "Base name of the output";
};
label = mkOption {
type = with types; nullOr str;
default = null;
description = lib.mdDoc ''
Filesystem label
Not to be confused with either the output name, or the partition label.
'';
};
sectorSize = mkOption {
type = types.int;
internal = true;
description = lib.mdDoc ''
Default value should probably not be changed. Used internally for some
automatic size maths.
'';
};
blockSize = mkOption {
type = types.int;
internal = true;
description = lib.mdDoc ''
Used with the assumption that files are rounded up to blockSize increments.
'';
};
size = mkOption {
type = with types; nullOr int;
default = null;
defaultText = "[automatically computed]";
description = lib.mdDoc ''
When null, size is computed automatically.
Otherwise sets the size of the filesystem image.
Note that the usable space in the disk image will most likely be
smaller than the size given here!
'';
};
computeMinimalSize = mkOption {
internal = true;
type = types.str;
description = lib.mdDoc ''
Filesystem-specific snippet to adapt the size of the filesystem image
so the content can fit.
'';
};
extraPadding = mkOption {
type = types.int;
default = 0;
description = lib.mdDoc ''
When size is computed automatically, how many bytes to add to the
filesystem total size.
'';
};
minimumSize = mkOption {
type = types.int;
internal = true;
default = 0;
description = lib.mdDoc ''
Minimum usable size the filesystem must have. "Usable" here may not
actually be useful.
'';
};
builderFunctions = mkOption {
type = types.lines;
internal = true;
default = "";
description = lib.mdDoc ''
Bash functions required by the builder.
'';
};
buildPhases = mkOption {
type = with types; lazyAttrsOf str;
internal = true;
description = lib.mdDoc ''
Implementation of build phases for the filesystem image.
'';
};
buildPhasesOrder = mkOption {
type = with types; listOf str;
internal = true;
description = lib.mdDoc ''
Order of the filesystem image build phase. Adding to this is likely to
cause issues. Use this sparingly, and as a last resort.
'';
};
populateCommands = lib.mkOption {
type = types.lines;
default = "";
description = lib.mdDoc ''
Commands used to fill the filesystem.
`$PWD` is the root of the filesystem.
'';
};
buildInputs = mkOption {
type = with types; listOf package;
internal = true;
description = lib.mdDoc ''
Allows adding to the builder buildInputs.
'';
};
nativeBuildInputs = mkOption {
type = with types; listOf package;
internal = true;
description = lib.mdDoc ''
Allows adding to the builder nativeBuildInputs.
As this list is built without *splicing*, use `pkgs.buildPackages` as
a source for packages.
'';
};
output = mkOption {
type = types.package;
internal = true;
description = lib.mdDoc ''
The build output for the filesystem image.
'';
};
};
config = {
buildInputs = [
];
nativeBuildInputs = with pkgs.buildPackages; [
libfaketime
];
buildPhasesOrder = [
# Copy the files to be copied into the target filesystem image first in
# the `pwd` during this phase.
"populatePhase"
# Default phase should be fine, pre-allocate the disk image file.
"allocationPhase"
# Phase in which the `mkfs` command is called on the disk image file.
"filesystemPhase"
# Commands to copy the files into the disk image (if the creation command
# does not intrinsically do it).
# The prep/unprep phases change the CWD to the directory containing the
# files to copy into the filesystem image, and back into the build dir.
"_prepCopyPhase"
"copyPhase"
"_unprepCopyPhase"
# Commands where a filesystem check should be ran.
"checkPhase"
# Any other extra business to run, normally left to the consumer.
"additionalCommandsPhase"
];
buildPhases = {
"allocationPhase" = lib.mkDefault ''
${optionalString (size == null) ''
size=$(compute-minimal-size)
''}
if (( size < minimumSize )); then
size=$minimumSize
echo "WARNING: the '${"\${label:-(unlabeled)}"}' partition was too small, size increased to $minimumSize bytes."
fi
truncate -s $size "$img"
'';
"populatePhase" = lib.mkDefault ''
(
cd "$files"
${config.populateCommands}
)
'';
"_prepCopyPhase" = ''
cd "$files"
'';
"_unprepCopyPhase" = ''
cd "$NIX_BUILD_TOP"
'';
"additionalCommandsPhase" = ''
'';
};
builderFunctions = ''
compute-minimal-size() {
local size=0
(
cd files
# Size rounded in blocks. This assumes all files are to be rounded to a
# multiple of blockSize.
# Use of `--apparent-size` is to ensure we don't get the block size of the underlying FS.
# Use of `--block-size` is to get *our* block size.
size=$(find . ! -type d -print0 | du --files0-from=- --apparent-size --block-size "$blockSize" | cut -f1 | sum-lines)
echo "Reserving $size sectors for files..." 1>&2
# Adds one blockSize per directory, they do take some place, in the end.
local directories=$(find . -type d | wc -l)
echo "Reserving $directories sectors for directories..." 1>&2
size=$(( directories + size ))
size=$((size * blockSize))
${computeMinimalSize}
size=$(( size + ${toString extraPadding} ))
echo "$size"
)
}
sum-lines() {
local acc=0
while read -r number; do
acc=$((acc+number))
done
echo "$acc"
}
'';
output = pkgs.callPackage ./builder.nix { inherit config; };
};
}

View File

@ -0,0 +1,68 @@
{ stdenvNoCC, config, ncdu, tree }:
# This builder is heavily influenced by the configuration.
stdenvNoCC.mkDerivation (config.buildPhases // {
inherit (config)
name
buildInputs
filesystem
label
size
minimumSize
blockSize
sectorSize
;
nativeBuildInputs = config.nativeBuildInputs ++ [
ncdu
tree
];
phases = config.buildPhasesOrder;
img = placeholder "out";
outputs = [ "out" "metadata" ];
buildCommand = ''
PS4=" $ "
set -u
header() {
printf "\n:: %s\n\n" "$1"
}
${config.builderFunctions}
# The default stdenv/generic clashes with `runHook`.
# It doesn't override as expected.
unset -f checkPhase
# Location where extra metadata about the filesystem can be stored.
# Use for extra useful debugging data.
mkdir -p $metadata
# Location where the filesystem content will be copied to.
mkdir -p files
files="$(cd files; pwd)"
for phase in ''${phases[@]}; do
if [[ "$phase" != _* ]]; then
header "Running $phase"
fi
runHook "$phase"
done
(
cd "$files"
faketime -f "1970-01-01 00:00:01" tree -a | xz > $metadata/tree.xz
faketime -f "1970-01-01 00:00:01" ncdu -0x -o - | xz > $metadata/ncdu.xz
)
set +u
'';
passthru = {
inherit (config) filesystem;
};
})

View File

@ -0,0 +1,18 @@
/**
* Evaluates the configuration for a single filesystem image build.
*/
{ pkgs
, modules ? []
, config ? {}
}:
let config' = config; in
rec {
module = { imports = import ./module-list.nix; };
config = (pkgs.lib.evalModules {
modules = [
{ _module.args.pkgs = pkgs; }
module
config'
] ++ modules;
}).config;
}

View File

@ -0,0 +1,62 @@
{ config, lib, pkgs, ... }:
let
enabled = config.filesystem == "btrfs";
inherit (lib)
escapeShellArg
mkIf
mkMerge
mkOption
optionalString
types
;
inherit (config) label sectorSize blockSize;
inherit (config.btrfs) partitionID;
in
{
options.btrfs = {
partitionID = mkOption {
type = types.nullOr config.helpers.types.uuid;
example = "45454545-4545-4545-4545-454545454545";
default = null;
description = lib.mdDoc ''
Volume ID of the filesystem.
'';
};
};
config = mkMerge [
{ availableFilesystems = [ "btrfs" ]; }
(mkIf enabled {
nativeBuildInputs = with pkgs.buildPackages; [
btrfs-progs
];
blockSize = config.helpers.size.KiB 4;
sectorSize = lib.mkDefault 512;
# Generated an empty filesystem, it was 114294784 bytes long.
# Rounded up to 110MiB.
minimumSize = config.helpers.size.MiB 110;
computeMinimalSize = ''
'';
buildPhases = {
copyPhase = ''
mkfs.btrfs \
-r . \
${optionalString (partitionID != null) "-U ${partitionID}"} \
${optionalString (label != null) "-L ${escapeShellArg label}"} \
${optionalString (config.size == null) "--shrink"} \
"$img"
'';
checkPhase = ''
faketime -f "1970-01-01 00:00:01" btrfs check "$img"
'';
};
})
];
}

View File

@ -0,0 +1,29 @@
{ config, lib, ... }:
let
inherit (lib)
mkOption
types
;
in
{
imports = [
./btrfs.nix
./ext4.nix
./fat32.nix
./squashfs.nix
];
options = {
availableFilesystems = mkOption {
type = with types; listOf str;
internal = true;
};
filesystem = mkOption {
type = types.enum config.availableFilesystems;
description = lib.mdDoc ''
Filesystem used in this filesystem image.
'';
};
};
}

View File

@ -0,0 +1,128 @@
{ config, lib, pkgs, ... }:
let
enabled = config.filesystem == "ext4";
inherit (lib)
escapeShellArg
mkIf
mkMerge
mkOption
optionalString
types
;
inherit (config.helpers)
chopDecimal
;
inherit (config) label sectorSize blockSize;
inherit (config.ext4) partitionID;
# Bash doesn't do floating point representations. Multiplications and divisions
# are handled with enough precision that we can multiply and divide to get a precision.
precision = 1000;
makeFudge = f: toString (chopDecimal (f * precision));
# This applies only to 256MiB and greater.
# For smaller than 256MiB images the overhead from the FS is much greater.
# This will also let *some* slack space at the end at greater sizes.
# This is the value at 512MiB where it goes slightly down compared to 256MiB.
fudgeFactor = makeFudge 0.05208587646484375;
# This table was built using a script that built an image with `make_ext4fs`
# for the given size in MiB, and recorded the available size according to `df`.
smallFudgeLookup = lib.strings.concatStringsSep "\n" (lib.lists.reverseList(
lib.attrsets.mapAttrsToList (size: factor: ''
elif (( size > ${toString size} )); then
fudgeFactor=${toString factor}
'') {
"${toString (config.helpers.size.MiB 5)}" = makeFudge 0.84609375;
"${toString (config.helpers.size.MiB 8)}" = makeFudge 0.5419921875;
"${toString (config.helpers.size.MiB 16)}" = makeFudge 0.288818359375;
"${toString (config.helpers.size.MiB 32)}" = makeFudge 0.1622314453125;
"${toString (config.helpers.size.MiB 64)}" = makeFudge 0.09893798828125;
"${toString (config.helpers.size.MiB 128)}" = makeFudge 0.067291259765625;
"${toString (config.helpers.size.MiB 256)}" = makeFudge 0.0518646240234375;
}
));
minimumSize = config.helpers.size.MiB 5;
in
{
options.ext4 = {
partitionID = mkOption {
type = types.nullOr config.helpers.types.uuid;
example = "45454545-4545-4545-4545-454545454545";
default = null;
description = lib.mdDoc ''
Volume ID of the filesystem.
'';
};
};
config = mkMerge [
{ availableFilesystems = [ "ext4" ]; }
(mkIf enabled {
nativeBuildInputs = with pkgs.buildPackages; [
e2fsprogs
make_ext4fs
];
blockSize = config.helpers.size.KiB 4;
sectorSize = lib.mkDefault 512;
inherit minimumSize;
computeMinimalSize = ''
# `local size` is in bytes.
# We don't have a static reserved factor figured out. It is rather hard with
# ext4fs as there are multiple factors increasing the overhead.
local reservedSize=0
local fudgeFactor=${toString fudgeFactor}
# Instead we rely on a lookup table. See how it is built in the derivation file.
if (( size < ${toString (config.helpers.size.MiB 256)} )); then
echo "$size is smaller than 256MiB; using the lookup table." 1>&2
# A bit of a hack, though allows us to build the lookup table using only elifs.
if false; then
:
${smallFudgeLookup}
else
# The data is smaller than 5MiB... The filesystem image size will likely
# not be able to accomodate... here we handle it in another way.
fudgeFactor=0
echo "Fudge factor skipped for extra small partition. Instead increasing by a fixed amount." 1>&2
size=$(( size + ${toString minimumSize}))
fi
fi
local reservedSize=$(( size * $fudgeFactor / ${toString precision} ))
echo "Fudge factor: $fudgeFactor / ${toString precision}" 1>&2
echo -n "Adding reservedSize: $size + $reservedSize = " 1>&2
size=$((size + reservedSize))
echo "$size" 1>&2
'';
buildPhases = {
copyPhase = ''
faketime -f "1970-01-01 00:00:01" \
make_ext4fs \
-b $blockSize \
-l $size \
${optionalString (partitionID != null) "-U ${partitionID}"} \
${optionalString (label != null) "-L ${escapeShellArg label}"} \
"$img" \
.
'';
checkPhase = ''
EXT2FS_NO_MTAB_OK=yes faketime -f "1970-01-01 00:00:01" fsck.ext4 -y -f "$img" || :
EXT2FS_NO_MTAB_OK=yes faketime -f "1970-01-01 00:00:01" fsck.ext4 -n -f "$img"
'';
};
})
];
}

View File

@ -0,0 +1,136 @@
{ config, lib, pkgs, ... }:
let
enabled = config.filesystem == "fat32";
inherit (lib)
escapeShellArg
mkIf
mkMerge
mkOption
optionalString
types
;
inherit (config.helpers)
chopDecimal
;
type32bitHex = types.strMatching "[0-9a-fA-F]{1,8}";
inherit (config) label sectorSize blockSize;
inherit (config.fat32) partitionID;
# Should these be configurable?
# The default from `mkfs.fat`.
reservedSectors = 32;
# The default from `mkfs.fat`.
hiddenSectors = 0;
# The default from `mkfs.fat`.
numberOfFats = 2;
# Extra padding per FAT, a constant in code
fatPadding = 4;
# I have not been able to validate that it could be different from 1 for FAT32.
# It seems the different values (e.g. 4) are for FAT12 and FAT16.
# This is the only "bad" assumption here.
clusterSize = 1;
# Bash doesn't do floating point representations. Multiplications and divisions
# are handled with enough precision that we can multiply and divide to get a precision.
precision = 1000;
in
{
options.fat32 = {
partitionID = mkOption {
type = types.nullOr type32bitHex;
example = "2e24ec82";
default = null;
defaultText = "[Depends on the file system creation time]";
description = lib.mdDoc ''
Volume ID of the filesystem.
The default is a number which depends on the file system creation time.
'';
};
};
config = mkMerge [
{ availableFilesystems = [ "fat32" ]; }
(mkIf enabled {
nativeBuildInputs = with pkgs.buildPackages; [
dosfstools
mtools
];
blockSize = lib.mkDefault 512;
sectorSize = lib.mkDefault 512;
minimumSize = config.helpers.size.KiB 500;
buildPhases = {
checkPhase = ''
# Always check and verify FS
fsck.vfat -a "$img" || :
fsck.vfat -vn "$img"
'';
filesystemPhase = ''
fatSize=16
if (( size > 1024*1024*32 )); then
fatSize=32
fi
faketime -f "1970-01-01 00:00:01" mkfs.vfat \
-F $fatSize \
-R ${toString reservedSectors} \
-h ${toString hiddenSectors} \
-s ${toString (blockSize / sectorSize)} \
-S ${toString sectorSize} \
${optionalString (partitionID != null) "-i ${partitionID}"} \
${optionalString (label != null) "-n ${escapeShellArg label}"} \
"$img"
'';
copyPhase = ''
(
for f in ./* ./.*; do
if [[ "$f" != "./." && "$f" != "./.." ]]; then
faketime -f "1970-01-01 00:00:01" \
mcopy -psv -i "$img" "$f" ::
fi
done
)
'';
};
computeMinimalSize = ''
# `local size` is in bytes.
# This amount is a static amount of reserved space.
local static_reserved=${toString ( (reservedSectors + hiddenSectors) * sectorSize )}
# This is a constant representing the relative reserved space ratio.
local relative_reserved=${
chopDecimal (
precision - (
1.0 * sectorSize / ((clusterSize * sectorSize) + (numberOfFats * fatPadding))
# ^ forces floating point
) * precision
)
}
# Rounds up the likely truncated result. At worst it's a bit more space.
(( relative_reserved++ ))
echo "static_reserved=$static_reserved" 1>&2
echo "relative_reserved=$relative_reserved" 1>&2
local reservedSize=$(( (static_reserved + size) * relative_reserved / ${toString precision} + static_reserved ))
echo -n "Adding reservedSize: $size + $reservedSize = " 1>&2
size=$((size + reservedSize))
echo "$size" 1>&2
'';
})
];
}

View File

@ -0,0 +1,86 @@
{ config, lib, pkgs, ... }:
let
enabled = config.filesystem == "squashfs";
inherit (lib)
escapeShellArg
mkIf
mkMerge
mkOption
optionalString
types
;
inherit (config) label sectorSize blockSize;
inherit (config.squashfs)
compression
compressionParams
;
in
{
options.squashfs = {
compression = mkOption {
type = types.enum [
# Uses the name used in the -comp param
"gzip"
"xz"
"zstd"
];
default = "xz";
description = lib.mdDoc ''
Volume ID of the filesystem.
'';
};
compressionParams = mkOption {
type = types.str;
internal = true;
};
};
config = mkMerge [
{ availableFilesystems = [ "squashfs" ]; }
(mkIf enabled {
squashfs.compressionParams = mkMerge [
(mkIf (compression == "xz") "-Xdict-size 100%")
(mkIf (compression == "zstd") "-Xcompression-level 6")
];
nativeBuildInputs = with pkgs.buildPackages; [
squashfsTools
];
# NixOS's make-squashfs uses 1MiB
# mksquashfs's help says its default is 128KiB
blockSize = config.helpers.size.MiB 1;
# This is actually unused (and irrelevant)
sectorSize = lib.mkDefault 512;
minimumSize = 0;
computeMinimalSize = "";
buildPhases = {
copyPhase = ''
# The empty pre-allocated file will confuse mksquashfs
rm "$img"
(
# This also activates dotglob automatically.
# Using this means hidden files will be added too.
GLOBIGNORE=".:.."
# Using `.` as the input will put the PWD, including its name
# in the root of the filesystem.
mksquashfs \
* \
"$img" \
-info \
-b "$blockSize" \
-no-hardlinks -keep-as-directory -all-root \
-comp "${compression}" ${compressionParams} \
-processors $NIX_BUILD_CORES
)
'';
};
})
];
}

View File

@ -0,0 +1,5 @@
[
../helpers.nix
./basic.nix
./filesystem
]

View File

@ -0,0 +1,70 @@
{ lib, ... }:
let
inherit (lib)
concatMapStringsSep
mkOption
splitString
types
;
in
{
options.helpers = mkOption {
# Unspecified on purpose
type = types.attrs;
internal = true;
};
config.helpers = rec {
/**
* Silly alias to concat/map script segments for a given list.
*/
each = els: fn: (
concatMapStringsSep "\n" (el:
fn el
) els);
/**
* Provides user-friendly aliases for defining sizes.
*/
size = rec {
TiB = x: 1024 * (GiB x);
GiB = x: 1024 * (MiB x);
MiB = x: 1024 * (KiB x);
KiB = x: 1024 * x;
};
/**
* Drops the decimal portion of a floating point number.
*/
chopDecimal = f: first (splitString "." (toString f));
/**
* Like `last`, but for the first element of a list.
*/
first = list: lib.lists.last (lib.lists.reverseList list);
types = {
uuid = lib.types.strMatching (
let hex = "[0-9a-fA-F]"; in
"${hex}{8}-${hex}{4}-${hex}{4}-${hex}{4}-${hex}{12}"
);
};
makeGap = length: {
isGap = true;
inherit length;
};
makeESP = args: lib.recursiveUpdate {
name = "ESP-filesystem";
partitionLabel = "$ESP";
partitionType = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B";
partitionUUID = "63E19453-EF00-4BD9-9AAF-000000000000";
filesystem = {
filesystem = "fat32";
label = "$ESP";
fat32.partitionID = "ef00ef00";
};
} args;
};
}