push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
{
runCommand,
lib,
type ? "zstd",
zstd,
}:
firmware:
let
compressor =
{
xz = {
ext = "xz";
nativeBuildInputs = [ ];
cmd = file: target: ''xz -9c -T1 -C crc32 --lzma2=dict=2MiB "${file}" > "${target}"'';
};
zstd = {
ext = "zst";
nativeBuildInputs = [ zstd ];
cmd = file: target: ''zstd -T1 -19 --long --check -f "${file}" -o "${target}"'';
};
}
.${type} or (throw "Unsupported compressor type for firmware.");
args = {
allowedRequisites = [ ];
inherit (compressor) nativeBuildInputs;
}
// lib.optionalAttrs (firmware ? meta) { inherit (firmware) meta; };
in
runCommand "${firmware.name}-${type}" args ''
mkdir -p $out/lib
(cd ${firmware} && find lib/firmware -type d -print0) |
(cd $out && xargs -0 mkdir -v --)
(cd ${firmware} && find lib/firmware -type f -print0) |
(cd $out && xargs -0rtP "$NIX_BUILD_CORES" -n1 \
sh -c '${compressor.cmd "${firmware}/$1" "$1.${compressor.ext}"}' --)
(cd ${firmware} && find lib/firmware -type l) | while read link; do
target="$(readlink "${firmware}/$link")"
if [ -f "${firmware}/$link" ]; then
ln -vs -- "''${target/^${firmware}/$out}.${compressor.ext}" "$out/$link.${compressor.ext}"
else
ln -vs -- "''${target/^${firmware}/$out}" "$out/$link"
fi
done
echo "Checking for broken symlinks:"
find -L $out -type l -print -execdir false -- '{}' '+'
''

View File

@@ -0,0 +1,59 @@
rec {
cat = {
executable = pkgs: "cat";
ubootName = "none";
extension = ".cpio";
};
gzip = {
executable = pkgs: "${pkgs.gzip}/bin/gzip";
defaultArgs = [ "-9n" ];
ubootName = "gzip";
extension = ".gz";
};
bzip2 = {
executable = pkgs: "${pkgs.bzip2}/bin/bzip2";
ubootName = "bzip2";
extension = ".bz2";
};
xz = {
executable = pkgs: "${pkgs.xz}/bin/xz";
defaultArgs = [
"--check=crc32"
"--lzma2=dict=512KiB"
];
extension = ".xz";
};
lzma = {
executable = pkgs: "${pkgs.xz}/bin/lzma";
defaultArgs = [
"--check=crc32"
"--lzma1=dict=512KiB"
];
ubootName = "lzma";
extension = ".lzma";
};
lz4 = {
executable = pkgs: "${pkgs.lz4}/bin/lz4";
defaultArgs = [ "-l" ];
ubootName = "lz4";
extension = ".lz4";
};
lzop = {
executable = pkgs: "${pkgs.lzop}/bin/lzop";
ubootName = "lzo";
extension = ".lzo";
};
zstd = {
executable = pkgs: "${pkgs.zstd}/bin/zstd";
defaultArgs = [ "-10" ];
ubootName = "zstd";
extension = ".zst";
};
pigz = gzip // {
executable = pkgs: "${pkgs.pigz}/bin/pigz";
};
pixz = xz // {
executable = pkgs: "${pkgs.pixz}/bin/pixz";
defaultArgs = [ ];
};
}

View File

@@ -0,0 +1,25 @@
{
rustPlatform,
lib,
}:
rustPlatform.buildRustPackage {
pname = "make-initrd-ng";
version = "0.1.0";
src = ./make-initrd-ng;
cargoLock.lockFile = ./make-initrd-ng/Cargo.lock;
passthru.updateScript = ./make-initrd-ng/update.sh;
meta = {
description = "Tool for copying binaries and their dependencies";
mainProgram = "make-initrd-ng";
maintainers = with lib.maintainers; [
das_j
elvishjerricco
k900
];
license = lib.licenses.mit;
};
}

View File

@@ -0,0 +1,120 @@
let
# Some metadata on various compression programs, relevant to naming
# the initramfs file and, if applicable, generating a u-boot image
# from it.
compressors = import ./initrd-compressor-meta.nix;
# Get the basename of the actual compression program from the whole
# compression command, for the purpose of guessing the u-boot
# compression type and filename extension.
compressorName = fullCommand: builtins.elemAt (builtins.match "([^ ]*/)?([^ ]+).*" fullCommand) 1;
in
{
stdenvNoCC,
cpio,
ubootTools,
lib,
pkgsBuildHost,
makeInitrdNGTool,
binutils,
runCommand,
# Name of the derivation (not of the resulting file!)
name ? "initrd",
# Program used to compress the cpio archive; use "cat" for no compression.
# This can also be a function which takes a package set and returns the path to the compressor,
# such as `pkgs: "${pkgs.lzop}/bin/lzop"`.
compressor ? "gzip",
_compressorFunction ?
if lib.isFunction compressor then
compressor
else if !builtins.hasContext compressor && builtins.hasAttr compressor compressors then
compressors.${compressor}.executable
else
_: compressor,
_compressorExecutable ? _compressorFunction pkgsBuildHost,
_compressorName ? compressorName _compressorExecutable,
_compressorMeta ? compressors.${_compressorName} or { },
# List of arguments to pass to the compressor program, or null to use its defaults
compressorArgs ? null,
_compressorArgsReal ?
if compressorArgs == null then _compressorMeta.defaultArgs or [ ] else compressorArgs,
# Filename extension to use for the compressed initramfs. This is
# included for clarity, but $out/initrd will always be a symlink to
# the final image.
# If this isn't guessed, you may want to complete the metadata above and send a PR :)
extension ?
_compressorMeta.extension
or (throw "Unrecognised compressor ${_compressorName}, please specify filename extension"),
# List of { source = path_or_derivation; target = "/path"; }
# The paths are copied into the initramfs in their nix store path
# form, then linked at the root according to `target`.
contents,
# List of uncompressed cpio files to prepend to the initramfs. This
# can be used to add files in specified paths without them becoming
# symlinks to store paths.
prepend ? [ ],
# Whether to wrap the initramfs in a u-boot image.
makeUInitrd ? stdenvNoCC.hostPlatform.linux-kernel.target == "uImage",
# If generating a u-boot image, the architecture to use. The default
# guess may not align with u-boot's nomenclature correctly, so it can
# be overridden.
# See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L81-106 for a list.
uInitrdArch ? stdenvNoCC.hostPlatform.ubootArch,
# The name of the compression, as recognised by u-boot.
# See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L195-204 for a list.
# If this isn't guessed, you may want to complete the metadata above and send a PR :)
uInitrdCompression ?
_compressorMeta.ubootName
or (throw "Unrecognised compressor ${_compressorName}, please specify uInitrdCompression"),
}:
runCommand name
{
compress = "${_compressorExecutable} ${lib.escapeShellArgs _compressorArgsReal}";
passthru = {
compressorExecutableFunction = _compressorFunction;
compressorArgs = _compressorArgsReal;
};
inherit
extension
makeUInitrd
uInitrdArch
prepend
;
${if makeUInitrd then "uInitrdCompression" else null} = uInitrdCompression;
passAsFile = [ "contents" ];
contents = builtins.toJSON contents;
nativeBuildInputs = [
makeInitrdNGTool
cpio
]
++ lib.optional makeUInitrd ubootTools;
}
''
mkdir -p ./root/{run,tmp,var/empty}
ln -s ../run ./root/var/run
make-initrd-ng "$contentsPath" ./root
mkdir "$out"
(cd root && find . -exec touch -h -d '@1' '{}' +)
for PREP in $prepend; do
cat $PREP >> $out/initrd
done
(cd root && find . -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null | eval -- $compress >> "$out/initrd")
if [ -n "$makeUInitrd" ]; then
mkimage -A "$uInitrdArch" -O linux -T ramdisk -C "$uInitrdCompression" -d "$out/initrd" $out/initrd.img
# Compatibility symlink
ln -sf "initrd.img" "$out/initrd"
else
ln -s "initrd" "$out/initrd$extension"
fi
''

View File

@@ -0,0 +1,163 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "goblin"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7666983ed0dd8d21a6f6576ee00053ca0926fb281a5522577a4dbd0f1b54143"
dependencies = [
"log",
"plain",
"scroll",
]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "make-initrd-ng"
version = "0.1.0"
dependencies = [
"eyre",
"goblin",
"libc",
"serde",
"serde_json",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "scroll"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da"
dependencies = [
"scroll_derive",
]
[[package]]
name = "scroll_derive"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

View File

@@ -0,0 +1,14 @@
[package]
name = "make-initrd-ng"
version = "0.1.0"
authors = ["Will Fancher <elvishjerricco@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eyre = "0.6.8"
goblin = "0.5.0"
libc = "0.2.171"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@@ -0,0 +1,82 @@
# What is this for?
NixOS's traditional initrd is generated by listing the paths that
should be included in initrd and copying the full runtime closure of
those paths into the archive. For most things, like almost any
executable, this involves copying the entirety of huge packages like
glibc, when only things like the shared library files are needed. To
solve this, NixOS does a variety of patchwork to edit the files being
copied in so they only refer to small, patched up paths. For instance,
executables and their shared library dependencies are copied into an
`extraUtils` derivation, and every ELF file is patched to refer to
files in that output.
The problem with this is that it is often difficult to correctly patch
some things. For instance, systemd bakes the path to the `mount`
command into the binary, so patchelf is no help. Instead, it's very
often easier to simply copy the desired files to their original store
locations in initrd and not copy their entire runtime closure. This
does mean that it is the burden of the developer to ensure that all
necessary dependencies are copied in, as closures won't be
consulted. However, it is rare that full closures are actually
desirable, so in the traditional initrd, the developer was likely to
do manual work on patching the dependencies explicitly anyway.
# How it works
This program is similar to its inspiration (`find-libs` from the
traditional initrd), except that it also handles symlinks and
directories according to certain rules. As input, it receives a
sequence of pairs of paths. The first path is an object to copy into
initrd. The second path (if not empty) is the path to a symlink that
should be placed in the initrd, pointing to that object. How that
object is copied depends on its type.
1. A regular file is copied directly to the same absolute path in the
initrd.
- If it is *also* an ELF file, then all of its direct shared
library dependencies are also listed as objects to be copied.
- If an unwrapped file exists as `.[filename]-wrapped`, then it is
also listed as an object to be copied.
2. A directory's direct children are listed as objects to be copied,
and a directory at the same absolute path in the initrd is created.
3. A symlink's target is listed as an object to be copied.
There are a couple of quirks to mention here. First, the term "object"
refers to the final file path that the developer intends to have
copied into initrd. This means any parent directory is not considered
an object just because its child was listed as an object in the
program input; instead those intermediate directories are simply
created in support of the target object. Second, shared libraries,
directory children, and symlink targets aren't immediately recursed,
because they simply get listed as objects themselves, and are
therefore traversed when they themselves are processed. Finally,
symlinks in the intermediate directories leading to an object are
preserved, meaning an input object `/a/symlink/b` will just result in
initrd containing `/a/symlink -> /target/b` and `/target/b`, even if
`/target` has other children. Preserving symlinks in this manner is
important for things like systemd.
These rules automate the most important and obviously necessary
copying that needs to be done in most cases, allowing programs and
configuration files to go unpatched, while keeping the content of the
initrd to a minimum.
# Why Rust?
- A prototype of this logic was written in Bash, in an attempt to keep
with its `find-libs` ancestor, but that program was difficult to
write, and ended up taking several minutes to run. This program runs
in less than a second, and the code is substantially easier to work
with.
- This will not require end users to install a rust toolchain to use
NixOS, as long as this tool is cached by Hydra. And if you're
bootstrapping NixOS from source, rustc is already required anyway.
- Rust was favored over Python for its type system, and because if you
want to go fast, why not go *really fast*?

View File

@@ -0,0 +1,350 @@
use std::collections::{BTreeSet, HashSet, VecDeque};
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::hash::Hash;
use std::iter::FromIterator;
use std::os::unix;
use std::os::unix::fs::PermissionsExt;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use libc::umask;
use eyre::Context;
use goblin::{elf::Elf, Object};
use serde::Deserialize;
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Deserialize, Hash)]
#[serde(rename_all = "lowercase")]
enum DLOpenPriority {
Required,
Recommended,
Suggested,
}
#[derive(PartialEq, Eq, Debug, Deserialize, Clone, Hash)]
#[serde(rename_all = "camelCase")]
struct DLOpenConfig {
use_priority: DLOpenPriority,
features: BTreeSet<String>,
}
#[derive(Deserialize, Debug)]
struct DLOpenNote {
soname: Vec<String>,
feature: Option<String>,
// description is in the spec, but we don't need it here.
// description: Option<String>,
priority: Option<DLOpenPriority>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
struct StoreInput {
source: String,
target: Option<String>,
dlopen: Option<DLOpenConfig>,
}
#[derive(PartialEq, Eq, Hash, Clone)]
struct StorePath {
path: Box<Path>,
dlopen: Option<DLOpenConfig>,
}
struct NonRepeatingQueue<T> {
queue: VecDeque<T>,
seen: HashSet<T>,
}
impl<T> NonRepeatingQueue<T> {
fn new() -> NonRepeatingQueue<T> {
NonRepeatingQueue {
queue: VecDeque::new(),
seen: HashSet::new(),
}
}
}
impl<T: Clone + Eq + Hash> NonRepeatingQueue<T> {
fn push_back(&mut self, value: T) -> bool {
if self.seen.contains(&value) {
false
} else {
self.seen.insert(value.clone());
self.queue.push_back(value);
true
}
}
fn pop_front(&mut self) -> Option<T> {
self.queue.pop_front()
}
}
fn add_dependencies<P: AsRef<Path> + AsRef<OsStr> + std::fmt::Debug>(
source: P,
elf: Elf,
contents: &[u8],
dlopen: &Option<DLOpenConfig>,
queue: &mut NonRepeatingQueue<StorePath>,
) -> eyre::Result<()> {
if let Some(interp) = elf.interpreter {
queue.push_back(StorePath {
path: Box::from(Path::new(interp)),
dlopen: dlopen.clone(),
});
}
let mut dlopen_libraries = vec![];
if let Some(dlopen) = dlopen {
for n in elf
.iter_note_sections(&contents, Some(".note.dlopen"))
.into_iter()
.flatten()
{
let note = n.wrap_err_with(|| format!("bad note in {:?}", source))?;
// Payload is padded and zero terminated
let payload = &note.desc[..note
.desc
.iter()
.position(|x| *x == 0)
.unwrap_or(note.desc.len())];
let parsed = serde_json::from_slice::<Vec<DLOpenNote>>(payload)?;
for mut parsed_note in parsed {
if dlopen.use_priority
>= parsed_note.priority.unwrap_or(DLOpenPriority::Recommended)
|| parsed_note
.feature
.map(|f| dlopen.features.contains(&f))
.unwrap_or(false)
{
dlopen_libraries.append(&mut parsed_note.soname);
}
}
}
}
let rpaths = if elf.runpaths.len() > 0 {
elf.runpaths
} else if elf.rpaths.len() > 0 {
elf.rpaths
} else {
vec![]
};
let rpaths_as_path = rpaths
.into_iter()
.flat_map(|p| p.split(":"))
.map(|p| Box::<Path>::from(Path::new(p)))
.collect::<Vec<_>>();
for line in elf
.libraries
.into_iter()
.map(|s| s.to_string())
.chain(dlopen_libraries)
{
let mut found = false;
for path in &rpaths_as_path {
let lib = path.join(&line);
if lib.exists() {
// No need to recurse. The queue will bring it back round.
queue.push_back(StorePath {
path: Box::from(lib.as_path()),
dlopen: dlopen.clone(),
});
found = true;
break;
}
}
if !found {
// glibc makes it tricky to make this an error because
// none of the files have a useful rpath.
println!(
"Warning: Couldn't satisfy dependency {} for {:?}",
line,
OsStr::new(&source)
);
}
}
Ok(())
}
fn copy_file<
P: AsRef<Path> + AsRef<OsStr> + std::fmt::Debug,
S: AsRef<Path> + AsRef<OsStr> + std::fmt::Debug,
>(
source: P,
target: S,
dlopen: &Option<DLOpenConfig>,
queue: &mut NonRepeatingQueue<StorePath>,
) -> eyre::Result<()> {
fs::copy(&source, &target)
.wrap_err_with(|| format!("failed to copy {:?} to {:?}", source, target))?;
let contents =
fs::read(&source).wrap_err_with(|| format!("failed to read from {:?}", source))?;
if let Ok(Object::Elf(e)) = Object::parse(&contents) {
add_dependencies(source, e, &contents, &dlopen, queue)?;
};
Ok(())
}
fn queue_dir<P: AsRef<Path> + std::fmt::Debug>(
source: P,
dlopen: &Option<DLOpenConfig>,
queue: &mut NonRepeatingQueue<StorePath>,
) -> eyre::Result<()> {
for entry in
fs::read_dir(&source).wrap_err_with(|| format!("failed to read dir {:?}", source))?
{
let entry = entry?;
// No need to recurse. The queue will bring us back round here on its own.
queue.push_back(StorePath {
path: Box::from(entry.path().as_path()),
dlopen: dlopen.clone(),
});
}
Ok(())
}
fn handle_path(
root: &Path,
p: StorePath,
queue: &mut NonRepeatingQueue<StorePath>,
) -> eyre::Result<()> {
let mut source = PathBuf::new();
let mut target = Path::new(root).to_path_buf();
let mut iter = p.path.components().peekable();
while let Some(comp) = iter.next() {
match comp {
Component::Prefix(_) => panic!("This tool is not meant for Windows"),
Component::RootDir => {
target.clear();
target.push(root);
source.clear();
source.push("/");
}
Component::CurDir => {}
Component::ParentDir => {
// Don't over-pop the target if the path has too many ParentDirs
if source.pop() {
target.pop();
}
}
Component::Normal(name) => {
target.push(name);
source.push(name);
let typ = fs::symlink_metadata(&source)
.wrap_err_with(|| format!("failed to get symlink metadata for {:?}", source))?
.file_type();
if typ.is_file() && !target.exists() {
copy_file(&source, &target, &p.dlopen, queue)?;
if let Some(filename) = source.file_name() {
source.set_file_name(OsString::from_iter([
OsStr::new("."),
filename,
OsStr::new("-wrapped"),
]));
let wrapped_path = source.as_path();
if wrapped_path.exists() {
queue.push_back(StorePath {
path: Box::from(wrapped_path),
dlopen: p.dlopen.clone(),
});
}
}
} else if typ.is_symlink() {
let link_target = fs::read_link(&source)
.wrap_err_with(|| format!("failed to resolve symlink of {:?}", source))?;
// Create the link, then push its target to the queue
if !target.exists() && !target.is_symlink() {
unix::fs::symlink(&link_target, &target).wrap_err_with(|| {
format!("failed to symlink {:?} to {:?}", link_target, target)
})?;
}
source.pop();
source.push(link_target);
while let Some(c) = iter.next() {
source.push(c);
}
let link_target_path = source.as_path();
if link_target_path.exists() {
queue.push_back(StorePath {
path: Box::from(link_target_path),
dlopen: p.dlopen.clone(),
});
}
break;
} else if typ.is_dir() {
if !target.exists() {
fs::create_dir(&target)
.wrap_err_with(|| format!("failed to create dir {:?}", target))?;
}
// Only recursively copy if the directory is the target object
if iter.peek().is_none() {
queue_dir(&source, &p.dlopen, queue)
.wrap_err_with(|| format!("failed to queue dir {:?}", source))?;
}
}
}
}
}
Ok(())
}
fn main() -> eyre::Result<()> {
let args: Vec<String> = env::args().collect();
let contents =
fs::read(&args[1]).wrap_err_with(|| format!("failed to open file {:?}", &args[1]))?;
let input = serde_json::from_slice::<Vec<StoreInput>>(&contents)
.wrap_err_with(|| {
let text = String::from_utf8_lossy(&contents);
format!("failed to parse JSON '{}' in {:?}",
text,
&args[1])
})?;
let output = &args[2];
let out_path = Path::new(output);
// The files we create should not be writable.
unsafe { umask(0o022) };
let mut queue = NonRepeatingQueue::<StorePath>::new();
for sp in input {
let obj_path = Path::new(&sp.source);
queue.push_back(StorePath {
path: Box::from(obj_path),
dlopen: sp.dlopen,
});
if let Some(target) = sp.target {
println!("{} -> {}", &target, &sp.source);
// We don't care about preserving symlink structure here
// nearly as much as for the actual objects.
let link_string = format!("{}/{}", output, target);
let link_path = Path::new(&link_string);
let mut link_parent = link_path.to_path_buf();
link_parent.pop();
fs::create_dir_all(&link_parent)
.wrap_err_with(|| format!("failed to create directories to {:?}", link_parent))?;
unix::fs::symlink(obj_path, link_path)
.wrap_err_with(|| format!("failed to symlink {:?} to {:?}", obj_path, link_path))?;
}
}
while let Some(obj) = queue.pop_front() {
handle_path(out_path, obj, &mut queue)?;
}
Ok(())
}

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env nix-shell
#!nix-shell -p cargo -i bash
cd "$(dirname "$0")"
cargo update

View File

@@ -0,0 +1,128 @@
# Create an initramfs containing the closure of the specified
# file system objects. An initramfs is used during the initial
# stages of booting a Linux system. It is loaded by the boot loader
# along with the kernel image. It's supposed to contain everything
# (such as kernel modules) necessary to allow us to mount the root
# file system. Once the root file system is mounted, the `real' boot
# script can be called.
#
# An initramfs is a cpio archive, and may be compressed with a number
# of algorithms.
let
# Some metadata on various compression programs, relevant to naming
# the initramfs file and, if applicable, generating a u-boot image
# from it.
compressors = import ./initrd-compressor-meta.nix;
# Get the basename of the actual compression program from the whole
# compression command, for the purpose of guessing the u-boot
# compression type and filename extension.
compressorName = fullCommand: builtins.elemAt (builtins.match "([^ ]*/)?([^ ]+).*" fullCommand) 1;
in
{
stdenvNoCC,
cpio,
ubootTools,
lib,
pkgsBuildHost,
# Name of the derivation (not of the resulting file!)
name ? "initrd",
# Program used to compress the cpio archive; use "cat" for no compression.
# This can also be a function which takes a package set and returns the path to the compressor,
# such as `pkgs: "${pkgs.lzop}/bin/lzop"`.
compressor ? "gzip",
_compressorFunction ?
if lib.isFunction compressor then
compressor
else if !builtins.hasContext compressor && builtins.hasAttr compressor compressors then
compressors.${compressor}.executable
else
_: compressor,
_compressorExecutable ? _compressorFunction pkgsBuildHost,
_compressorName ? compressorName _compressorExecutable,
_compressorMeta ? compressors.${_compressorName} or { },
# List of arguments to pass to the compressor program, or null to use its defaults
compressorArgs ? null,
_compressorArgsReal ?
if compressorArgs == null then _compressorMeta.defaultArgs or [ ] else compressorArgs,
# Filename extension to use for the compressed initramfs. This is
# included for clarity, but $out/initrd will always be a symlink to
# the final image.
# If this isn't guessed, you may want to complete the metadata above and send a PR :)
extension ?
_compressorMeta.extension
or (throw "Unrecognised compressor ${_compressorName}, please specify filename extension"),
# List of { object = path_or_derivation; symlink = "/path"; }
# The paths are copied into the initramfs in their nix store path
# form, then linked at the root according to `symlink`.
contents,
# List of uncompressed cpio files to prepend to the initramfs. This
# can be used to add files in specified paths without them becoming
# symlinks to store paths.
prepend ? [ ],
# Whether to wrap the initramfs in a u-boot image.
makeUInitrd ? stdenvNoCC.hostPlatform.linux-kernel.target or "dummy" == "uImage",
# If generating a u-boot image, the architecture to use. The default
# guess may not align with u-boot's nomenclature correctly, so it can
# be overridden.
# See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L81-106 for a list.
uInitrdArch ? stdenvNoCC.hostPlatform.linuxArch,
# The name of the compression, as recognised by u-boot.
# See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L195-204 for a list.
# If this isn't guessed, you may want to complete the metadata above and send a PR :)
uInitrdCompression ?
_compressorMeta.ubootName
or (throw "Unrecognised compressor ${_compressorName}, please specify uInitrdCompression"),
}:
let
# !!! Move this into a public lib function, it is probably useful for others
toValidStoreName =
x: with builtins; lib.concatStringsSep "-" (filter (x: !(isList x)) (split "[^a-zA-Z0-9_=.?-]+" x));
in
stdenvNoCC.mkDerivation (
rec {
inherit
name
makeUInitrd
extension
uInitrdArch
prepend
;
builder = ./make-initrd.sh;
nativeBuildInputs = [
cpio
]
++ lib.optional makeUInitrd ubootTools;
compress = "${_compressorExecutable} ${lib.escapeShellArgs _compressorArgsReal}";
# Pass the function through, for reuse in append-initrd-secrets. The
# function is used instead of the string, in order to support
# cross-compilation (append-initrd-secrets running on a different
# architecture than what the main initramfs is built on).
passthru = {
compressorExecutableFunction = _compressorFunction;
compressorArgs = _compressorArgsReal;
};
# !!! should use XML.
objects = map (x: x.object) contents;
symlinks = map (x: x.symlink) contents;
suffices = map (x: if x ? suffix then x.suffix else "none") contents;
closureInfo = "${pkgsBuildHost.closureInfo { rootPaths = objects; }}";
}
// lib.optionalAttrs makeUInitrd {
uInitrdCompression = uInitrdCompression;
}
)

View File

@@ -0,0 +1,49 @@
set -o pipefail
objects=($objects)
symlinks=($symlinks)
suffices=($suffices)
mkdir root
# Needed for splash_helper, which gets run before init.
mkdir root/dev
mkdir root/sys
mkdir root/proc
for ((n = 0; n < ${#objects[*]}; n++)); do
object=${objects[$n]}
symlink=${symlinks[$n]}
suffix=${suffices[$n]}
if test "$suffix" = none; then suffix=; fi
mkdir -p $(dirname root/$symlink)
ln -s $object$suffix root/$symlink
done
# Get the paths in the closure of `object'.
storePaths="$(cat $closureInfo/store-paths)"
# Paths in cpio archives *must* be relative, otherwise the kernel
# won't unpack 'em.
(cd root && cp -prP --parents $storePaths .)
# Put the closure in a gzipped cpio archive.
mkdir -p $out
for PREP in $prepend; do
cat $PREP >> $out/initrd
done
(cd root && find * .[^.*] -exec touch -h -d '@1' '{}' +)
(cd root && find * .[^.*] -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null | eval -- $compress >> "$out/initrd")
if [ -n "$makeUInitrd" ]; then
mkimage -A "$uInitrdArch" -O linux -T ramdisk -C "$uInitrdCompression" -d "$out/initrd" $out/initrd.img
# Compatibility symlink
ln -sf "initrd.img" "$out/initrd"
else
ln -s "initrd" "$out/initrd$extension"
fi

View File

@@ -0,0 +1,32 @@
# Given a kernel build (with modules in $kernel/lib/modules/VERSION),
# produce a module tree in $out/lib/modules/VERSION that contains only
# the modules identified by `rootModules', plus their dependencies.
# Also generate an appropriate modules.dep.
{
stdenvNoCC,
kernel,
firmware,
nukeReferences,
rootModules,
kmod,
allowMissing ? false,
extraFirmwarePaths ? [ ],
}:
stdenvNoCC.mkDerivation {
name = kernel.name + "-shrunk";
builder = ./modules-closure.sh;
nativeBuildInputs = [
nukeReferences
kmod
];
inherit
kernel
firmware
rootModules
allowMissing
extraFirmwarePaths
;
allowedReferences = [ "out" ];
}

View File

@@ -0,0 +1,115 @@
# When no modules are built, the $out/lib/modules directory will not
# exist. Because the rest of the script assumes it does exist, we
# handle this special case first.
if ! test -d "$kernel/lib/modules"; then
if test -z "$rootModules" || test -n "$allowMissing"; then
mkdir -p "$out"
exit 0
else
echo "Required modules: $rootModules"
echo "Can not derive a closure of kernel modules because no modules were provided."
exit 1
fi
fi
version=$(cd $kernel/lib/modules && ls -d *)
echo "kernel version is $version"
# Determine the dependencies of each root module.
mkdir -p $out/lib/modules/"$version"
touch closure
for module in $rootModules; do
echo "root module: $module"
modprobe --config no-config -d $kernel --set-version "$version" --show-depends "$module" \
| while read cmd module args; do
case "$cmd" in
builtin)
touch found
echo "$module" >>closure
echo " builtin dependency: $module";;
insmod)
touch found
if ! test -e "$module"; then
echo " dependency not found: $module"
exit 1
fi
target=$(echo "$module" | sed "s^$NIX_STORE.*/lib/modules/^$out/lib/modules/^")
if test -e "$target"; then
echo " dependency already copied: $module"
continue
fi
echo "$module" >>closure
echo " copying dependency: $module"
mkdir -p $(dirname $target)
cp "$module" "$target"
# If the kernel is compiled with coverage instrumentation, it
# contains the paths of the *.gcda coverage data output files
# (which it doesn't actually use...). Get rid of them to prevent
# the whole kernel from being included in the initrd.
nuke-refs "$target"
echo "$target" >> $out/insmod-list;;
*)
echo " unexpected modprobe output: $cmd $module"
exit 1;;
esac
done || test -n "$allowMissing"
if ! test -e found; then
echo " not found"
if test -z "$allowMissing"; then
exit 1
fi
else
rm found
fi
done
cd "$firmware"
for module in $(< ~-/closure); do
# for builtin modules, modinfo will reply with a wrong output looking like:
# $ modinfo -F firmware unix
# name: unix
#
# There is a pending attempt to fix this:
# https://github.com/NixOS/nixpkgs/pull/96153
# https://lore.kernel.org/linux-modules/20200823215433.j5gc5rnsmahpf43v@blumerang/T/#u
#
# For now, the workaround is just to filter out the extraneous lines out
# of its output.
modinfo -b $kernel --set-version "$version" -F firmware $module | grep -v '^name:' | while read -r i; do
echo "firmware for $module: $i"
for name in "$i" "$i.xz" "$i.zst" ""; do
[ -z "$name" ] && echo "WARNING: missing firmware $i for module $module"
if cp -v --parents --no-preserve=mode lib/firmware/$name "$out" 2>/dev/null; then
break
fi
done
done || :
done
for path in $extraFirmwarePaths; do
mkdir -p $(dirname $out/lib/firmware/$path)
for name in "$path" "$path.xz" "$path.zst" ""; do
if cp -v --parents --no-preserve=mode lib/firmware/$name "$out" 2>/dev/null; then
break
fi
done
done
if test -e lib/firmware/edid ; then
echo "lib/firmware/edid found, copying."
mkdir -p "$out/lib/firmware"
cp -v --no-preserve=mode --recursive --dereference --no-target-directory lib/firmware/edid "$out/lib/firmware/edid"
else
echo "lib/firmware/edid not found, skipping."
fi
# copy module ordering hints for depmod
cp $kernel/lib/modules/"$version"/modules.order $out/lib/modules/"$version"/.
cp $kernel/lib/modules/"$version"/modules.builtin $out/lib/modules/"$version"/.
depmod -b $out -a $version
# remove original hints from final derivation
rm $out/lib/modules/"$version"/modules.order
rm $out/lib/modules/"$version"/modules.builtin