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:
parent
d25d3b87e7
commit
0f3ac0bef1
@ -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;
|
||||
};
|
||||
}
|
||||
)
|
@ -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.
|
||||
|
@ -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
|
||||
'';
|
||||
})
|
||||
];
|
||||
}
|
@ -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
|
||||
'';
|
||||
};
|
||||
}
|
@ -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)
|
||||
]
|
@ -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}";
|
||||
});
|
||||
});
|
||||
}
|
||||
)
|
||||
]
|
@ -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
|
@ -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"
|
@ -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"
|
@ -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"
|
@ -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"
|
@ -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"
|
@ -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"
|
@ -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
|
@ -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))
|
||||
];
|
||||
};
|
||||
}
|
@ -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))
|
||||
];
|
||||
}
|
@ -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";
|
||||
}
|
@ -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}
|
||||
''
|
@ -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
|
@ -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}
|
||||
''
|
||||
|
@ -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
|
@ -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
|
||||
}
|
10
overlay/image-builder/default.nix
Normal file
10
overlay/image-builder/default.nix
Normal 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;
|
||||
};
|
||||
}
|
73
overlay/image-builder/disk-image/basic.nix
Normal file
73
overlay/image-builder/disk-image/basic.nix
Normal 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.
|
||||
# '';
|
||||
# };
|
||||
};
|
||||
}
|
18
overlay/image-builder/disk-image/eval-config.nix
Normal file
18
overlay/image-builder/disk-image/eval-config.nix
Normal 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;
|
||||
}
|
8
overlay/image-builder/disk-image/module-list.nix
Normal file
8
overlay/image-builder/disk-image/module-list.nix
Normal 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
|
||||
]
|
@ -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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
@ -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
|
||||
'';
|
||||
}
|
@ -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;
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
@ -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
|
||||
'';
|
||||
}
|
@ -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;
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
153
overlay/image-builder/disk-image/partitions.nix
Normal file
153
overlay/image-builder/disk-image/partitions.nix
Normal 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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
262
overlay/image-builder/filesystem-image/basic.nix
Normal file
262
overlay/image-builder/filesystem-image/basic.nix
Normal 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; };
|
||||
};
|
||||
}
|
68
overlay/image-builder/filesystem-image/builder.nix
Normal file
68
overlay/image-builder/filesystem-image/builder.nix
Normal 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;
|
||||
};
|
||||
})
|
18
overlay/image-builder/filesystem-image/eval-config.nix
Normal file
18
overlay/image-builder/filesystem-image/eval-config.nix
Normal 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;
|
||||
}
|
62
overlay/image-builder/filesystem-image/filesystem/btrfs.nix
Normal file
62
overlay/image-builder/filesystem-image/filesystem/btrfs.nix
Normal 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"
|
||||
'';
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
@ -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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
128
overlay/image-builder/filesystem-image/filesystem/ext4.nix
Normal file
128
overlay/image-builder/filesystem-image/filesystem/ext4.nix
Normal 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"
|
||||
'';
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
136
overlay/image-builder/filesystem-image/filesystem/fat32.nix
Normal file
136
overlay/image-builder/filesystem-image/filesystem/fat32.nix
Normal 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
|
||||
'';
|
||||
|
||||
})
|
||||
];
|
||||
}
|
@ -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
|
||||
)
|
||||
'';
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
5
overlay/image-builder/filesystem-image/module-list.nix
Normal file
5
overlay/image-builder/filesystem-image/module-list.nix
Normal file
@ -0,0 +1,5 @@
|
||||
[
|
||||
../helpers.nix
|
||||
./basic.nix
|
||||
./filesystem
|
||||
]
|
70
overlay/image-builder/helpers.nix
Normal file
70
overlay/image-builder/helpers.nix
Normal 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;
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user