#!/bin/bash # lfs-kernel -- build and install a kernel. # # lfs-kernel what a build would do (nothing is built) # lfs-kernel --run build and install the configured version # lfs-kernel load [--boot] # load it now, and at every boot with --boot # lfs-kernel list what kernel.org offers right now # lfs-kernel 6.12.40 --run that version, once (config untouched) # lfs-kernel latest --run the newest stable, once # lfs-kernel detect-esp print the EFI directory this would use # lfs-kernel verify [ver] check that everything is installed # lfs-kernel firmware install what FIRMWARE_ONLY names, no kernel build # lfs-kernel status how far the last build got, what is installed # lfs-kernel promote [ver] [--run] # make the test kernel (vmlinuz_new) the one that # boots; the current one stays as vmlinuz_old # lfs-kernel enable --list hardware this tool knows by name # lfs-kernel enable ath9k-htc # put what that hardware needs into kernel.conf # lfs-kernel modules [text] what this kernel has, and what is loaded # lfs-kernel config what the next build's .config comes from # lfs-kernel config list the saved configs (the last two that BOOTED) # lfs-kernel config diff [a] [b] # what changed between two saved configs # lfs-kernel config search # which symbols that text means, and their state # lfs-kernel config set SYM=y|m|n [...] # add them to CONFIG_SET in kernel.conf # lfs-kernel config save [--good] # keep the running config; --good: it BOOTS # lfs-kernel config from last-good # build from that config, not from the running # kernel # lfs-kernel config unset SYM [...] # take them out again # lfs-kernel firmware --missing [--fix] [--all] # firmware this machine asks for and has not got, # from the boot log, this tree's modules and the # config; --fix installs it at exactly the path # the driver asked for, --all includes the files # a module merely DECLARES (other chips' -- large # and mostly unused) # # Settings: /etc/pkgusr/kernel.conf, INSTALLED by `make install` so it can be # edited before the first run. The build commands are in one block below, # after the line THE COMMANDS; everything above it picks a version and checks # paths. set -u CONF=/etc/pkgusr/kernel.conf MIRROR_JSON=https://www.kernel.org/releases.json MIRROR_TAR=https://cdn.kernel.org/pub/linux/kernel # -------------------------------------------------------------------------- # where does the kernel go? $ESP is the directory on the EFI System # Partition that your boot manager reads -- for rEFInd, the one holding your # vmlinuz. Find it with: findmnt -t vfat / ls /boot/efi/EFI # -------------------------------------------------------------------------- _detect_esp() { local m for m in $(findmnt -rn -t vfat,msdos -o TARGET 2>/dev/null); do # a directory that already holds a kernel is the right one for d in "$m"/EFI/*/ "$m"/EFI/BOOT/ "$m"/; do [ -d "$d" ] || continue case "$(echo "$d"*vmlinuz* "$d"*bzImage* 2>/dev/null)" in *vmlinuz*|*bzImage*) printf '%s' "${d%/}"; return 0 ;; esac done done # nothing with a kernel in it: the first vfat mount, if any findmnt -rn -t vfat,msdos -o TARGET 2>/dev/null | head -n1 } # A BUILD IS NOT SOMETHING YOU START BY TYPING FOUR WORDS. # # `lfs-kernel` alone compiled a kernel, installed it over the one the # machine boots, installed the modules, fetched firmware and copied the # Documentation -- an hour, from a bare command. Every other tool here # prints the plan and waits for --run; this one now does too. --run is # taken from anywhere on the line, so `lfs-kernel 7.2.6 --run` and # `lfs-kernel --run` both read naturally. _RUN=0 _rest=() for _a in "$@"; do case "$_a" in --run) _RUN=1 ;; *) _rest+=("$_a") ;; esac done set -- ${_rest[@]+"${_rest[@]}"} case "${1:-}" in list) echo "# kernel.org offers:" _releases | awk '{printf " %-12s %s\n", $1, $2}' echo "# build one: lfs-kernel " exit 0 ;; detect-esp) printf '%s\n' "$(_detect_esp)"; exit 0 ;; verify) shift; _VERIFY=1; _CLI_VERSION="${1:-}" ;; enable) # hardware by name: what symbols it needs, written into kernel.conf shift; _ENABLE=1; _ENARGS=("$@") ;; modules|mods) # what this kernel actually has, and what is loaded shift; _MODCMD=1; _MODARGS=("$@") ;; load|unload) # load a module now, and (--boot) at every boot _LOADCMD="$1"; shift; _LOADARGS=("$@") ;; status|state) # how far the last build got, and what is installed -- reads only shift; _STATUS=1 ;; promote) # the test kernel becomes the default one (vmlinuz_new -> vmlinuz) shift; _PROMOTE=1; _PROMOTE_ARG="${1:-}" ;; config) # the config, without building anything: what it will be made of, # what the last two booted kernels used, and the one place this # machine's own answers are written down (CONFIG_SET) shift; _CFGCMD=1; _CFGARGS=("$@") ;; firmware) # firmware alone, with no kernel build: the blobs are a separate # package and a missing one is a run-time problem, so fixing it must # not mean rebuilding a kernel that is already installed and booted. shift; _FWONLY=1; _FWARG=""; _FWFIX=0; _FWALL=0 for _a in "$@"; do case "$_a" in --fix) _FWFIX=1 ;; --all) _FWALL=1 ;; --missing|missing) _FWARG="--missing" ;; *) echo "!! lfs-kernel firmware: unknown argument '$_a'"; exit 1 ;; esac done [ "$_FWFIX" = 1 ] || [ "$_FWALL" = 1 ] && _FWARG="--missing" ;; -h|--help) # the slice is the header above, which grew: derive the end from # the header itself rather than hard-coding a line number that a # new command silently pushes out of range. sed -n '2,/^# Settings:/p' "$0" | sed '$d' | sed 's/^# \?//'; exit 0 ;; "") ;; search|find) # `lfs-kernel search AR9271` is what a person types, and it used to # mean "build the kernel called search" -- see the version check # below for what that really did. It is the config search. shift; _CFGCMD=1; _CFGARGS=("search" "$@") ;; set) shift; _CFGCMD=1; _CFGARGS=("set" "$@") ;; -*) echo "!! unknown option: $1 (try: lfs-kernel --help)"; exit 1 ;; *) # A WORD THAT IS NOT A VERSION IS NOT A VERSION. # # Anything unrecognised became the version to build, and the version # from kernel.conf then overwrote it (below) -- so a typo, or a # subcommand this tool does not have, silently started a FULL BUILD # of the configured kernel. `lfs-kernel search AR9271` rebuilt and # reinstalled 7.2.6, fetched the firmware and said nothing about the # word `search` at all. case "$1" in latest|[0-9]*.[0-9]*) _CLI_VERSION="$1" ;; *) echo "!! lfs-kernel: '$1' is neither a command nor a version." echo " commands: status, list, enable, modules, load," echo " config, search, firmware, verify, detect-esp" echo " a version looks like 7.2.6, or the word latest." case "$1" in conf*|cfg) echo " did you mean: lfs-kernel config" ;; mod*|lsmod) echo " did you mean: lfs-kernel modules" ;; en*|add) echo " did you mean: lfs-kernel enable --list" ;; fw|firm*) echo " did you mean: lfs-kernel firmware --missing" ;; esac exit 1 ;; esac ;; esac # The config is INSTALLED, not written on first run: it is a file to edit # before building, not a side effect of trying to. `make install` places it # and never overwrites an edited one. if [ ! -f "$CONF" ]; then echo "!! $CONF is missing. It ships with the tools:" echo " make install # places $CONF" echo " or write one; lfs-kernel detect-esp prints the EFI directory." exit 1 fi . "$CONF" # THE COMMAND LINE WINS. `lfs-kernel 6.12.40` sets VERSION, and then this # file -- sourced after it -- set VERSION=latest straight over the top: the # documented "that version, once" built whatever kernel.conf said instead, # and `verify 7.2.4` reported on another kernel entirely. Re-apply it here, # where nothing can overwrite it again. [ -n "${_CLI_VERSION:-}" ] && VERSION="$_CLI_VERSION" # A CONFIG FROM AN OLDER RELEASE. # # kernel.conf is never overwritten once edited, so an installed copy can # predate a change to what the settings MEAN. SRC in particular was dropped # in 1.14.123 -- the sources live in the package user's home now -- and a # config still setting it is describing a directory nothing reads. if [ -n "${SRC:-}" ]; then echo "# note: SRC is set in $CONF and is no longer used -- the sources" echo "# live in the package user's home (see PKG). A newer config" echo "# without it is at $CONF.new if make install left one." unset SRC fi [ -n "${PKG:-}" ] || { echo "!! $CONF has no PKG setting, so it predates 1.14.123." echo " Take the new one: cp $CONF{.new,} (yours is $CONF.new's basis)" echo " or add: PKG=linux" exit 1 } : "${PKG:=linux}"; : "${JOBS:=-j$(nproc)}" : "${CONFIG_FROM:=running}"; : "${V4L2LOOPBACK:=no}" : "${KERNEL_NAME:=vmlinuz}" # -------------------------------------------------------------------------- # which version? # -------------------------------------------------------------------------- _releases() { # " " per line, in kernel.org's order # Pair the keys by NAME, not by position: releases.json lists "version" # before "moniker", and a paste of alternate lines silently swapped the # two columns -- so `awk $1=="stable"` matched nothing and "latest" # resolved to the empty string. Key order in someone else's JSON is not # a thing to depend on. wget -qO- "$MIRROR_JSON" 2>/dev/null \ | tr ',{}' '\n\n\n' \ | awk -F'"' ' /"moniker"/ { m = $4 } /"version"/ { v = $4 } m != "" && v != "" { print m, v; m = ""; v = "" }' } _latest_stable() { _releases | awk '$1=="stable"{print $2; exit}'; } # `verify` inspects what is on disk, so it must not fetch: resolving # "latest" would ask kernel.org for a version it is never going to look at, # and fail on a machine with no network while reporting on one that is # perfectly installed. if [ "${_VERIFY:-0}" = 1 ] || [ "${_CFGCMD:-0}" = 1 ] \ || [ "${_ENABLE:-0}" = 1 ] || [ "${_MODCMD:-0}" = 1 ] \ || [ "${_STATUS:-0}" = 1 ] || [ -n "${_LOADCMD:-}" ] \ || [ "${_PROMOTE:-0}" = 1 ]; then : elif [ "${_FWONLY:-0}" = 1 ] && [ "$VERSION" = latest ]; then # A FIRMWARE REPORT IS ABOUT THIS MACHINE, NOT ABOUT KERNEL.ORG. # # `lfs-kernel firmware --missing` asked the website which kernel is # newest -- for a question answered entirely by /lib/modules and the # boot log -- printed "newest stable on kernel.org", and on a machine # with no network it EXITED instead of reporting. VERSION="$(uname -r | sed "s/-.*//")" if [ ! -d "/lib/modules/$VERSION" ] && [ ! -d "/lib/modules/$(uname -r)" ]; then VERSION="$(_latest_stable)" [ -n "$VERSION" ] || { echo "!! no /lib/modules for the running kernel, and kernel.org is unreachable"; exit 1; } fi elif [ "$VERSION" = latest ]; then VERSION="$(_latest_stable)" [ -n "$VERSION" ] || { echo "!! could not reach $MIRROR_JSON -- set VERSION in $CONF"; exit 1; } echo "# newest stable on kernel.org: $VERSION" fi # THE KERNEL IS A PACKAGE. Its sources belong in its own account's home, # next to every other package's, so `packagemanager verify`, pkg.lst and # `remove` all work on it. SRC used to be a free-standing directory in the # config -- a location invented outside the model, which is exactly what the # model exists to avoid. acct="p_$PKG" # `verify` must not CREATE anything -- a report that makes an account is not # a report. It only reads $acct's home below. if [ "${_VERIFY:-0}" != 1 ] && [ "${_CFGCMD:-0}" != 1 ] \ && [ "${_ENABLE:-0}" != 1 ] && [ "${_MODCMD:-0}" != 1 ] \ && [ "${_STATUS:-0}" != 1 ] && [ -z "${_LOADCMD:-}" ] \ && [ "${_PROMOTE:-0}" != 1 ] \ && ! id "$acct" >/dev/null 2>&1; then packagemanager add-user "$PKG" || { echo "!! could not create $acct"; exit 1; } fi [ "${_VERIFY:-0}" = 1 ] || [ "${_CFGCMD:-0}" = 1 ] \ || [ "${_ENABLE:-0}" = 1 ] || [ "${_MODCMD:-0}" = 1 ] \ || [ "${_STATUS:-0}" = 1 ] || [ -n "${_LOADCMD:-}" ] \ || [ "${_PROMOTE:-0}" = 1 ] \ || lfs-helper fix-home "$acct" --run >/dev/null 2>&1 || true SRC="$(getent passwd "$acct" 2>/dev/null | cut -d: -f6)/src" [ "${_VERIFY:-0}" = 1 ] || [ "${_CFGCMD:-0}" = 1 ] \ || [ "${_ENABLE:-0}" = 1 ] || [ "${_MODCMD:-0}" = 1 ] \ || [ "${_STATUS:-0}" = 1 ] || [ -n "${_LOADCMD:-}" ] \ || [ "${_PROMOTE:-0}" = 1 ] \ || install -d -o "$acct" -g "$acct" -m 755 "$SRC" # Everything that touches the tree runs AS THE PACKAGE USER: a root-owned # file left in its home would break the next build and every rule about who # owns what. Only the tarball fetch and the ESP copies are root's. _as_pkg() { su - "$acct" -c "cd '$tree' 2>/dev/null || cd '$SRC'; $*"; } tree="$SRC/linux-$VERSION" tarball="$SRC/linux-$VERSION.tar.xz" major="${VERSION%%.*}" # -------------------------------------------------------------------------- # THE CONFIGS ARE KEPT, OUTSIDE THE TREE. # # A .config lives in a source tree, and a source tree is deleted by hand or # by `packagemanager remove linux`, which takes the account's home, # and a failed build can leave one unusable. What is lost with it is the # only record of every answer this machine has ever given -- hours of # menuconfig, and the difference between a kernel that boots and one that # does not. # # So every config this tool builds with is copied to $KCFG_DIR, which is # root's, small (a config is ~200 kB) and has nothing to do with the tree: # # config-- every build, newest CONFIG_KEEP kept # last-good.config the config of the kernel that BOOTED # prev-good.config the one before that # # The last two are the ones worth having: they are known to work on this # hardware. CONFIG_FROM=last-good starts from one, `config diff` says what # changed between them, and `config set` is how an answer is added without # hand-editing a file that the next build overwrites. # -------------------------------------------------------------------------- KCFG_DIR="${KCFG_DIR:-/etc/pkgusr/kernel-configs}" : "${CONFIG_KEEP:=5}" _kcfg_readable() { # a config file with something in it [ -s "$1" ] && grep -q '^CONFIG_' "$1" 2>/dev/null } _kcfg_newest() { # the newest archived build config, or empty ls -1t "$KCFG_DIR"/config-* 2>/dev/null | head -n1 } _kcfg_ver() { # the kernel version a config file was built for sed -n 's/^# Linux\/[^ ]* \([0-9][^ ]*\) Kernel Configuration.*/\1/p' "$1" \ 2>/dev/null | head -n1 } _kcfg_archive() { # -- keep a copy of what we built with local src="$1" ver="$2" dst _kcfg_readable "$src" || return 0 install -d -m 755 "$KCFG_DIR" || return 0 dst="$KCFG_DIR/config-$ver-$(date +%Y%m%d-%H%M%S)" cp "$src" "$dst" 2>/dev/null || return 0 chmod 644 "$dst" echo "# config kept as $dst" # PRUNE THE BUILD SNAPSHOTS ONLY. last-good/prev-good are not in this # pattern and are never deleted here. ls -1t "$KCFG_DIR"/config-* 2>/dev/null | tail -n +"$((CONFIG_KEEP + 1))" \ | while read -r _old; do rm -f "$_old"; done } _kcfg_mark_good() { # -- this kernel has booted local src="$1" ver="$2" _kcfg_readable "$src" || return 0 install -d -m 755 "$KCFG_DIR" || return 0 # the same version again is the same kernel: refresh it, do not push the # OTHER working config out with a copy of this one if [ -f "$KCFG_DIR/last-good.config" ] \ && [ "$(_kcfg_ver "$KCFG_DIR/last-good.config")" != "$ver" ]; then cp "$KCFG_DIR/last-good.config" "$KCFG_DIR/prev-good.config" 2>/dev/null chmod 644 "$KCFG_DIR/prev-good.config" 2>/dev/null fi cp "$src" "$KCFG_DIR/last-good.config" 2>/dev/null || return 0 chmod 644 "$KCFG_DIR/last-good.config" echo "# the config of $ver is now $KCFG_DIR/last-good.config" } # THE CONFIG THIS MACHINE IS RUNNING ON, wherever it can be read from. _kcfg_current() { local c for c in "$tree/.config" "$KCFG_DIR/last-good.config" "$(_kcfg_newest)"; do [ -n "$c" ] && _kcfg_readable "$c" && { printf '%s' "$c"; return 0; } done if [ -r /proc/config.gz ]; then c="$(mktemp)"; gunzip < /proc/config.gz > "$c" 2>/dev/null \ && { printf '%s' "$c"; return 0; } rm -f "$c" fi return 1 } _kcfg_state() { # -- what that symbol is, in words local f="$1" sym="CONFIG_${2#CONFIG_}" l l="$(grep -E "^($sym=|# $sym is not set)" "$f" 2>/dev/null | head -n1)" [ -n "$l" ] || { printf 'absent'; return; } case "$l" in "# $sym is not set") printf 'not set' ;; *) printf '%s' "${l#*=}" ;; esac } # ONE WRITER FOR kernel.conf. `config set`, `config from` and `enable` all # change one KEY="value" line in a file the user edits by hand, so they all # back it up the same way and none of them rewrites anything else. _conf_set_var() { # local key="$1" val="$2" bak tmp bak="$CONF.bak-$(date +%Y%m%d-%H%M%S)" cp -p "$CONF" "$bak" || { echo "!! could not back up $CONF"; return 1; } if grep -qE "^[[:space:]]*$key=" "$CONF"; then tmp="$(mktemp)" awk -v v="$key=\"$val\"" -v k="^[[:space:]]*$key=" ' $0 ~ k && !done { print v; done = 1; next } { print }' "$CONF" > "$tmp" && cat "$tmp" > "$CONF" rm -f "$tmp" else printf '\n%s="%s"\n' "$key" "$val" >> "$CONF" fi echo "# $CONF (backup: $(basename "$bak"))" echo "# $key=\"$val\"" } # WHAT A SYMBOL IS, ACCORDING TO THE TREE: bool, tristate, or unknown -- # which decides whether `enable` may say =m. _ksym_type() { # local sym="${1#CONFIG_}" [ -d "$tree" ] || return 0 grep -rhA3 --include='Kconfig*' -E "^[[:space:]]*(menu)?config[[:space:]]+$sym\$" \ "$tree" 2>/dev/null \ | grep -m1 -oE '^[[:space:]]*(bool|tristate|string|int|hex)' \ | tr -d '[:space:]' } # THE SYMBOLS A KCONFIG FILE DEFINES, with their prompt -- the only way a # search can say "ATH9K_HTC: Atheros HTC based wireless cards" instead of # printing every symbol in a file that happens to mention the word. _ksym_scan() { # -- SYMBOLtypeprompt, for matches only local pat="$1" [ -d "$tree" ] || return 0 grep -rlis --include='Kconfig*' -- "$pat" "$tree" 2>/dev/null | head -n 40 \ | xargs -r awk -v pat="$pat" ' function low(x) { return tolower(x) } /^[[:space:]]*(menu)?config[[:space:]]+[A-Za-z0-9_]+/ { sym = $2; next } sym != "" && /^[[:space:]]*(bool|tristate|string|int|hex)([[:space:]]|$)/ { type = $1 p = $0 # the prompt is the FIRST quoted string on the line, if any: # `tristate "Atheros HTC based wireless cards support"` if (match(p, /"[^"]*"/)) p = substr(p, RSTART + 1, RLENGTH - 2) else p = "" if (index(low(sym), low(pat)) || index(low(p), low(pat))) print sym "\t" type "\t" p sym = ""; next } { if ($0 ~ /^[[:space:]]*$/) sym = "" }' 2>/dev/null \ | sort -u } # THE MODULES THIS KERNEL HAS: which version's /lib/modules to read. _kmod_ver() { local r; r="$(uname -r)" [ -d "/lib/modules/$r" ] && { printf '%s' "$r"; return 0; } ls -1 /lib/modules 2>/dev/null | sort -V | tail -n1 } # THE NEWEST SOURCE TREE, for a `config` command that names no version. _kcfg_tree() { [ -d "${SRC:-}" ] || return 0 ls -1d "$SRC"/linux-*/ 2>/dev/null | grep -v '\-headers' | sort -V | tail -n1 \ | sed 's:/$::' } if [ "${_CFGCMD:-0}" = 1 ]; then [ -d "$tree" ] || tree="$(_kcfg_tree)" _sub="${_CFGARGS[0]-show}" case "$_sub" in show|"") echo "# the next build's .config" echo "# CONFIG_FROM = $CONFIG_FROM" echo "# CONFIG_REFRESH = ${CONFIG_REFRESH:-olddefconfig}" echo "# CONFIG_SET = ${CONFIG_SET:-(nothing)}" echo "# kept in $KCFG_DIR (CONFIG_KEEP=$CONFIG_KEEP builds)" echo for _f in last-good prev-good; do if [ -f "$KCFG_DIR/$_f.config" ]; then printf '# %-16s %s (%s)\n' "$_f.config" \ "$(_kcfg_ver "$KCFG_DIR/$_f.config")" \ "$(date -r "$KCFG_DIR/$_f.config" '+%Y-%m-%d %H:%M')" else printf '# %-16s -- none yet\n' "$_f.config" fi done echo echo "# add an option: lfs-kernel config set NET_VENDOR_ATHEROS=y" echo "# find its name: lfs-kernel config search atheros" echo "# then build: lfs-kernel" exit 0 ;; list|ls) if [ ! -d "$KCFG_DIR" ]; then echo "# no saved configs yet ($KCFG_DIR) -- the next build writes one" exit 0 fi echo "# $KCFG_DIR" for _f in "$KCFG_DIR"/last-good.config "$KCFG_DIR"/prev-good.config; do [ -f "$_f" ] && printf ' %-34s %-10s %s\n' "$(basename "$_f")" \ "$(_kcfg_ver "$_f")" "$(date -r "$_f" '+%Y-%m-%d %H:%M')" done ls -1t "$KCFG_DIR"/config-* 2>/dev/null | while read -r _f; do printf ' %-34s %-10s %s\n' "$(basename "$_f")" \ "$(_kcfg_ver "$_f")" "$(date -r "$_f" '+%Y-%m-%d %H:%M')" done echo "# start from one: CONFIG_FROM=file: in $CONF" echo "# or CONFIG_FROM=last-good / prev-good / saved" exit 0 ;; diff) _a="${_CFGARGS[1]:-$KCFG_DIR/prev-good.config}" _b="${_CFGARGS[2]:-$KCFG_DIR/last-good.config}" [ -f "$_a" ] || _a="$KCFG_DIR/$_a" [ -f "$_b" ] || _b="$KCFG_DIR/$_b" for _f in "$_a" "$_b"; do [ -f "$_f" ] || { echo "!! no such config: $_f"; echo " lfs-kernel config list"; exit 1; } done echo "# $_a ($(_kcfg_ver "$_a")) -> $_b ($(_kcfg_ver "$_b"))" echo "# < only in the first, > only in the second" diff <(grep -E '^(CONFIG_|# CONFIG_)' "$_a" | sort) \ <(grep -E '^(CONFIG_|# CONFIG_)' "$_b" | sort) \ | grep -E '^[<>]' | sort -k2 || echo "# identical" exit 0 ;; search|find) _pat="${_CFGARGS[1]:-}" [ -n "$_pat" ] || { echo "!! lfs-kernel config search "; exit 1; } _cfg="$(_kcfg_current || true)" if [ -n "$_cfg" ]; then echo "# '$_pat' in $_cfg" grep -iE "^(CONFIG_[A-Z0-9_]*${_pat}[A-Z0-9_]*=|# CONFIG_[A-Z0-9_]*${_pat}[A-Z0-9_]* is not set)" \ "$_cfg" | head -n 25 | sed 's/^/ /' fi echo if [ -d "$tree" ]; then # BY NAME OR BY PROMPT, NOT BY FILE. Listing every symbol of # every Kconfig that mentions the word gave 40 lines of MIPS # console options for "atheros". A symbol is a hit when its own # name or its prompt says so, and the prompt is what tells you # which one you want. echo "# symbols in $tree (state, type, what it is)" _hits="$(_ksym_scan "$_pat")" if [ -n "$_hits" ]; then printf '%s\n' "$_hits" | head -n 30 | while IFS="$(printf '\t')" read -r _sym _ty _pr; do printf ' %-26s %-9s %-9s %s\n' "$_sym" \ "$( [ -n "$_cfg" ] && _kcfg_state "$_cfg" "$_sym" )" \ "$_ty" "$_pr" done _n="$(printf '%s\n' "$_hits" | wc -l)" [ "$_n" -gt 30 ] && echo " ... $((_n - 30)) more" else echo " (nothing whose name or prompt says '$_pat')" fi else echo "# no kernel tree here to search; unpack one with: lfs-kernel " fi echo echo "# a NET_VENDOR_* / WLAN_VENDOR_* symbol only opens the submenu:" echo "# the DRIVER under it has to be set too." echo "# known hardware, by name: lfs-kernel enable --list" echo "# set a symbol: lfs-kernel config set ATH9K_HTC=m" [ -d "$tree" ] \ && echo "# read the help text: cd $tree && make menuconfig (/ searches)" exit 0 ;; save) # SEED THE ARCHIVE WITHOUT A BUILD. Before the first build there is # nothing saved, so CONFIG_FROM=last-good has nothing to read -- and # the config worth keeping is already on this machine. _src="${_CFGARGS[1]:-}" _good=0 for _a in ${_CFGARGS[@]+"${_CFGARGS[@]}"}; do [ "$_a" = "--good" ] && _good=1 done [ "$_src" = "--good" ] && _src="" if [ -z "$_src" ]; then if [ -r /proc/config.gz ]; then _src="$(mktemp)"; gunzip < /proc/config.gz > "$_src" _what="the running kernel" elif _kcfg_readable "$tree/.config"; then _src="$tree/.config"; _what="$tree" else echo "!! nothing to save: no /proc/config.gz and no .config in a tree" exit 1 fi else _what="$_src" fi _kcfg_readable "$_src" || { echo "!! $_src is not a kernel config"; exit 1; } _ver="$(_kcfg_ver "$_src")" [ -n "$_ver" ] || _ver="$(uname -r | sed 's/-.*//')" echo "# saving $_what (kernel $_ver)" _kcfg_archive "$_src" "$_ver" if [ "$_good" = 1 ]; then _kcfg_mark_good "$_src" "$_ver" else echo "# it is a build snapshot, not a known-good config." echo "# --good marks it as one (use it for a kernel that BOOTS)." fi echo "# start from it next time: lfs-kernel config from last-good" exit 0 ;; from) _val="${_CFGARGS[1]:-}" case "$_val" in running|last-good|good|prev-good|prev|saved|last|keep|file:*) ;; "") echo "# CONFIG_FROM = $CONFIG_FROM" echo "# running | last-good | prev-good | saved | keep | file:" exit 0 ;; *) echo "!! CONFIG_FROM=$_val is not one of:" echo " running last-good prev-good saved keep file:"; exit 1 ;; esac case "$_val" in last-good|good) _kcfg_readable "$KCFG_DIR/last-good.config" \ || { echo "!! there is no $KCFG_DIR/last-good.config yet." echo " Make one from the kernel running now:" echo " lfs-kernel config save --good"; exit 1; } ;; esac _conf_set_var CONFIG_FROM "$_val" || exit 1 exit 0 ;; set|unset) _items=("${_CFGARGS[@]:1}") [ "${#_items[@]}" -gt 0 ] || { echo "!! lfs-kernel config $_sub SYMBOL[=VALUE] ..."; exit 1; } # the current list, as words _new="" for _w in ${CONFIG_SET:-}; do _keep=1 for _i in "${_items[@]}"; do _isym="CONFIG_${_i%%=*}"; _isym="CONFIG_${_isym#CONFIG_}" _wsym="CONFIG_${_w%%=*}"; _wsym="CONFIG_${_wsym#CONFIG_}" [ "$_isym" = "$_wsym" ] && _keep=0 done [ "$_keep" = 1 ] && _new="$_new $_w" done if [ "$_sub" = set ]; then for _i in "${_items[@]}"; do case "$_i" in *=*) ;; *) echo "!! '$_i' has no value -- SYMBOL=y|m|n||\"text\""; exit 1 ;; esac _val="${_i#*=}" case "$_val" in y|m|n|[0-9]*|\"*) ;; *) echo "# note: '$_val' is taken as a string value" ;; esac _new="$_new ${_i#CONFIG_}" done fi _new="$(printf '%s' "$_new" | sed 's/^ *//')" _conf_set_var CONFIG_SET "$_new" || exit 1 echo echo "# nothing is built yet. Apply it:" echo "# lfs-kernel # rebuild and install" echo "# Each entry is read back from the final .config: one the" echo "# kernel refuses (missing dependency, no such symbol) is" echo "# named and stops the build." exit 0 ;; *) echo "!! lfs-kernel config: show | list | diff | search | set | unset" echo " | save [--good] | from " exit 1 ;; esac fi # -------------------------------------------------------------------------- # HARDWARE BY NAME. # # "I have an AR9271 USB adapter" is the question; CONFIG_ATH9K_HTC plus # WLAN_VENDOR_ATH plus the htc_9271 firmware is the answer, and nothing on # the machine connects the two. menuconfig will not either: it wants the # symbol you do not know yet. # # Each line is: ||| # `enable ` puts the symbols into CONFIG_SET and the firmware into # FIRMWARE_ONLY, both in kernel.conf, both backed up. A symbol this kernel # refuses is reported by the build, which reads every CONFIG_SET entry back. _KDEV_TABLE=' ath9k-htc|WLAN_VENDOR_ATH=y ATH9K_HTC=m|ath9k_htc htc_9271*|Atheros AR9271/AR7010 USB wifi (TP-Link TL-WN722N v1, Alfa AWUS036NHA) ath9k|WLAN_VENDOR_ATH=y ATH9K=m ATH9K_PCI=y|ath9k_htc|Atheros AR9xxx PCI/PCIe wifi ath10k|WLAN_VENDOR_ATH=y ATH10K=m ATH10K_PCI=m|ath10k|Qualcomm Atheros QCA988x PCI wifi ath11k|WLAN_VENDOR_ATH=y ATH11K=m ATH11K_PCI=m|ath11k|Qualcomm QCA6390/WCN685x wifi alx|NET_VENDOR_ATHEROS=y ALX=m|-|Atheros AR816x/AR817x, Killer E2200 ethernet atl1c|NET_VENDOR_ATHEROS=y ATL1C=m|-|Atheros AR813x/AR815x ethernet iwlwifi|WLAN_VENDOR_INTEL=y IWLWIFI=m IWLMVM=m|intel iwlwifi-*.ucode|Intel wifi (AX200 and friends) rtl8xxxu|WLAN_VENDOR_REALTEK=y RTL8XXXU=m|rtlwifi rtw88|Realtek RTL8188/8192 USB wifi bluetooth-usb|BT=m BT_HCIBTUSB=m BT_RFCOMM=m BT_BNEP=m|rtl_bt intel|USB Bluetooth (most laptop and dongle radios) btrfs|BTRFS_FS=m BTRFS_FS_POSIX_ACL=y|-|the btrfs filesystem (stack: btrfs-progs) exfat|EXFAT_FS=m|-|exFAT, the usual format of big SD cards fuse|FUSE_FS=m|-|FUSE, userspace filesystems (ntfs-3g, sshfs) ntfs3|NTFS3_FS=m NTFS3_LZX_XPRESS=y|-|NTFS read/write (the in-kernel driver) nvme|BLK_DEV_NVME=y|-|NVMe SSDs usb-serial|USB_SERIAL=m USB_SERIAL_CP210X=m USB_SERIAL_FTDI_SIO=m USB_SERIAL_CH341=m|-|USB serial adapters (CP210x, FTDI, CH340) v4l2-usb|MEDIA_SUPPORT=m MEDIA_USB_SUPPORT=y USB_VIDEO_CLASS=m|-|USB webcams (UVC) ' _kdev_line() { # -- the table line, or empty printf '%s\n' "$_KDEV_TABLE" | awk -F'|' -v n="$1" 'NF && $1 == n {print; exit}' } _kdev_list() { echo "# hardware this tool knows by name:" printf '%s\n' "$_KDEV_TABLE" | awk -F'|' 'NF { printf " %-15s %s\n", $1, $4 }' echo echo "# enable one: lfs-kernel enable ath9k-htc" echo "# or a symbol: lfs-kernel enable ATH9K_HTC" echo "# what is set: lfs-kernel config show modules: lfs-kernel modules" } if [ "${_ENABLE:-0}" = 1 ]; then [ -d "$tree" ] || tree="$(_kcfg_tree)" _args=("${_ENARGS[@]}") if [ "${#_args[@]}" = 0 ] || [ "${_args[0]}" = "--list" ] || [ "${_args[0]}" = "list" ]; then _kdev_list; exit 0 fi _syms=""; _fw=""; _unknown="" for _a in "${_args[@]}"; do _row="$(_kdev_line "$_a")" if [ -n "$_row" ]; then _syms="$_syms $(printf '%s' "$_row" | cut -d'|' -f2)" _f="$(printf '%s' "$_row" | cut -d'|' -f3)" [ "$_f" = "-" ] || _fw="$_fw $_f" echo "# $_a: $(printf '%s' "$_row" | cut -d'|' -f4)" continue fi # not a name in the table: take it as a symbol, and ask the tree # whether it can be a module before saying =m case "$_a" in *=*) _syms="$_syms ${_a#CONFIG_}"; continue ;; esac _sym="$(printf '%s' "${_a#CONFIG_}" | tr '[:lower:]' '[:upper:]')" case "$(_ksym_type "$_sym")" in tristate) _syms="$_syms $_sym=m" echo "# $_sym: a tristate symbol -- set to m (a module)" ;; bool) _syms="$_syms $_sym=y" echo "# $_sym: a bool symbol -- set to y (built in)" ;; "") _unknown="$_unknown $_a" ;; *) _syms="$_syms $_sym=y" ;; esac done if [ -n "$_unknown" ]; then echo "!! not a name I know, and not a symbol in this tree:$_unknown" echo " lfs-kernel enable --list # the names" echo " lfs-kernel config search # the symbols" echo " lfs-kernel enable SYM=m # if you are sure" exit 1 fi # merge into CONFIG_SET, last value wins, order kept _new="" for _w in ${CONFIG_SET:-}; do _keep=1 for _i in $_syms; do [ "CONFIG_${_i%%=*}" = "CONFIG_${_w%%=*}" ] && _keep=0 done [ "$_keep" = 1 ] && _new="$_new $_w" done for _i in $_syms; do _new="$_new $_i"; done _new="$(printf '%s' "$_new" | sed 's/^ *//')" _conf_set_var CONFIG_SET "$_new" || exit 1 # the firmware, if this hardware needs some and it is not already named if [ -n "$_fw" ] && [ "${FIRMWARE_FROM:-none}" != none ]; then _fwnew="${FIRMWARE_ONLY:-}" if [ -n "$_fwnew" ]; then # empty means "everything": leave it for _f in $_fw; do case " $_fwnew " in *" $_f "*) ;; *) _fwnew="$_fwnew $_f" ;; esac done [ "$_fwnew" = "${FIRMWARE_ONLY:-}" ] \ || _conf_set_var FIRMWARE_ONLY "$_fwnew" fi fi echo echo "# nothing is built yet. Apply it:" echo "# lfs-kernel # rebuild, install, and the firmware" echo "# then, after the reboot:" echo "# lfs-kernel modules ${_args[0]}" echo "# lfs-kernel firmware --missing" exit 0 fi if [ "${_MODCMD:-0}" = 1 ]; then _mver="$(_kmod_ver)" [ -n "$_mver" ] || { echo "!! no /lib/modules -- no kernel is installed here"; exit 1; } _md="/lib/modules/$_mver" _pat="${_MODARGS[0]-}" _loaded="$(cut -d' ' -f1 /proc/modules 2>/dev/null | tr '_' '-' | sort -u)" _all="$(find "$_md" -name '*.ko' -o -name '*.ko.*' 2>/dev/null \ | sed 's:.*/::; s:\.ko.*::' | tr '_' '-' | sort -u)" _builtin="$(sed 's:.*/::; s:\.ko.*::' "$_md/modules.builtin" 2>/dev/null \ | tr '_' '-' | sort -u)" _nall="$(printf '%s\n' "$_all" | grep -c . || true)" _nld="$(printf '%s\n' "$_loaded" | grep -c . || true)" _nbi="$(printf '%s\n' "$_builtin" | grep -c . || true)" echo "# kernel $_mver: $_nall module(s) built, $_nld loaded, $_nbi built in" [ "$_mver" = "$(uname -r)" ] \ || echo "# (the running kernel is $(uname -r) -- this is another one)" if [ -z "$_pat" ]; then echo "# loaded now:" printf '%s\n' "$_loaded" | grep . | tr '\n' ' ' | fold -s -w 72 | sed 's/^/ /' echo echo "# one subject: lfs-kernel modules ath" echo "# not built yet: lfs-kernel config search ath9k" exit 0 fi _hit="$(printf '%s\n' "$_all" "$_builtin" | sort -u \ | grep -i -- "$(printf '%s' "$_pat" | tr '_' '-')" || true)" if [ -z "$_hit" ]; then echo "# no module of this kernel is called anything like '$_pat'." echo "# is it even configured? lfs-kernel config search $_pat" echo "# what it would be called: lfs-kernel enable --list" exit 1 fi echo "# modules matching '$_pat'" printf '%s\n' "$_hit" | while read -r _m; do _st="built" printf '%s\n' "$_builtin" | grep -qx -- "$_m" && _st="built-in" printf '%s\n' "$_loaded" | grep -qx -- "$_m" && _st="LOADED" _desc="$(modinfo -F description "$(printf '%s' "$_m" | tr '-' '_')" \ 2>/dev/null | head -n1)" printf ' %-24s %-9s %s\n' "$_m" "$_st" "${_desc:-}" done echo echo "# load one now: lfs-kernel load " echo "# and at boot: lfs-kernel load --boot" echo "# (a USB device loads its driver when you plug it in, if udev runs)" exit 0 fi # -------------------------------------------------------------------------- # WHERE ARE WE? # # A kernel build is long enough to be interrupted -- Ctrl-C, a reboot, a # session that timed out -- and afterwards nothing on the machine says how # far it got. The tree holds a .config that may or may not carry your # answers, object files that may or may not be a finished image, and # /lib/modules and the ESP each hold a kernel from some earlier day. # # `lfs-kernel status` reads all four and says, in order: what is running, # what the tree has, what is installed, and what to do next. It builds # nothing, needs no network, and does not care whether the ESP is mounted. # -------------------------------------------------------------------------- # -------------------------------------------------------------------------- # PROMOTE: THE TEST KERNEL BECOMES THE ONE THAT BOOTS. # # With KERNEL_NAME=vmlinuz_new every build lands beside the working kernel, # so a bad one cannot cost the machine -- and the machine keeps booting the # old one until someone copies files around on the ESP by hand, which is # where a vmlinuz_new_old and a vmlinuz.back come from. This is that copy, # checked: the image's own version (read from its header, as file(1) does), # its modules present, the previous default kept as vmlinuz_old. # -------------------------------------------------------------------------- _kimg_version() { # -> its kernel release, or nothing local f="$1" off [ -s "$f" ] || return 0 off="$(od -An -tu2 -j526 -N2 "$f" 2>/dev/null | tr -d ' ')" [ -n "$off" ] || return 0 dd if="$f" bs=1 skip=$((off + 512)) count=128 2>/dev/null \ | tr '\0' '\n' | head -n1 | awk '{print $1}' } if [ "${_PROMOTE:-0}" = 1 ]; then _def="vmlinuz" [ -n "${ESP:-}" ] || ESP="$(_detect_esp)" [ -d "${ESP:-}" ] || { echo "!! no ESP directory: ${ESP:-(not set, none found)} (is it mounted?)"; exit 1; } case "$_PROMOTE_ARG" in "") _src="$ESP/${KERNEL_NAME}" ;; */*) _src="$_PROMOTE_ARG" ;; [0-9]*) _src="" for _c in "$ESP/${KERNEL_NAME}-$_PROMOTE_ARG" "$ESP/vmlinuz_new-$_PROMOTE_ARG" \ "$ESP/vmlinuz-$_PROMOTE_ARG"; do [ -s "$_c" ] && { _src="$_c"; break; } done ;; *) _src="$ESP/$_PROMOTE_ARG" ;; esac if [ -z "$_src" ] || [ ! -s "$_src" ]; then echo "!! nothing to promote: ${_src:-no image for $_PROMOTE_ARG} is not there" echo " kernels on the ESP:" for _k in "$ESP"/vmlinuz*; do [ -s "$_k" ] && printf ' %-28s %s\n' "${_k##*/}" "$(_kimg_version "$_k")" done exit 1 fi _sv="$(_kimg_version "$_src")" _dv="$(_kimg_version "$ESP/$_def")" echo "# promote ${_src##*/} ($_sv) to $_def" echo " now booting by default: $_def (${_dv:-none})" echo " running: $(uname -r)" [ -n "$_sv" ] || { echo "!! ${_src##*/} has no readable kernel version -- not a bzImage?"; exit 1; } if [ "$_src" = "$ESP/$_def" ] || { [ -f "$ESP/$_def" ] && cmp -s "$_src" "$ESP/$_def"; }; then echo "# $_def already is $_sv -- nothing to do" exit 0 fi if [ ! -d "/lib/modules/$_sv" ]; then echo "!! /lib/modules/$_sv is missing -- that kernel would boot without" echo " its modules. Install them first (lfs-kernel $_sv --run)." exit 1 fi echo " plan:" [ -f "$ESP/$_def" ] && echo " $_def -> ${_def}_old (your way back: pick it in the boot menu)" echo " ${_src##*/} -> $_def" echo " ${_src##*/} -> $_def-$_sv (so verify finds it)" _strays="" for _x in vmlinuz- config- System.map-; do [ -e "$ESP/$_x" ] && _strays="$_strays $ESP/$_x" done if [ "$_RUN" != 1 ]; then echo "# dry run -- add --run" else if [ -f "$ESP/$_def" ]; then cp -a "$ESP/$_def" "$ESP/${_def}_old" || { echo "!! could not keep $_def as ${_def}_old"; exit 1; } fi cp -a "$_src" "$ESP/$_def.tmp" && mv -f "$ESP/$_def.tmp" "$ESP/$_def" \ || { echo "!! could not write $ESP/$_def"; exit 1; } [ -f "$ESP/$_def-$_sv" ] || cp -a "$_src" "$ESP/$_def-$_sv" if cmp -s "$_src" "$ESP/$_def"; then echo "# $_def is now $_sv; the previous default is ${_def}_old" else echo "!! $ESP/$_def does not match ${_src##*/} after the copy"; exit 1 fi fi if [ "$KERNEL_NAME" != "$_def" ]; then echo "# future builds still land as $KERNEL_NAME. To install them as the" echo "# default directly (previous kept as ${_def}_old), in $CONF:" echo "# KERNEL_NAME=$_def" fi if [ -n "$_strays" ]; then echo "# left over from a run with no version (safe to delete):" echo "# rm$_strays" fi exit 0 fi if [ "${_STATUS:-0}" = 1 ]; then [ -d "$tree" ] || tree="$(_kcfg_tree)" _ver="$(basename "${tree:-}" 2>/dev/null | sed 's/^linux-//')" _run="$(uname -r)"; _runv="$(echo "$_run" | sed 's/-.*//')" echo "### lfs-kernel status" echo echo "# running now" echo " kernel $_run" if [ -s /etc/pkgusr/kernel.last-good ]; then echo " known to boot $(cat /etc/pkgusr/kernel.last-good)" else echo " known to boot -- nothing recorded yet" fi if [ -d "/lib/modules/$_run" ]; then echo " its modules /lib/modules/$_run" else echo " its modules MISSING (/lib/modules/$_run is not there)" fi echo echo "# the config" echo " CONFIG_FROM $CONFIG_FROM" for _f in last-good prev-good; do if [ -f "$KCFG_DIR/$_f.config" ]; then echo " $_f $(_kcfg_ver "$KCFG_DIR/$_f.config") ($(date -r "$KCFG_DIR/$_f.config" '+%Y-%m-%d %H:%M'))" else echo " $_f -- none" fi done # DOES THE TREE'S CONFIG CARRY WHAT YOU ASKED FOR? This is the question # after an interrupted run: the answers are applied BEFORE the compile, # so they are in the file even when no kernel came out of it. if [ -n "${CONFIG_SET:-}" ] && [ -f "${tree:-}/.config" ]; then _miss="" for _cs in $CONFIG_SET; do _sym="CONFIG_${_cs%%=*}"; _sym="CONFIG_${_sym#CONFIG_}"; _val="${_cs#*=}" case "$_val" in n) grep -qE "^$_sym=(y|m)" "$tree/.config" && _miss="$_miss $_cs" ;; *) grep -qx "$_sym=$_val" "$tree/.config" || _miss="$_miss $_cs" ;; esac done if [ -z "$_miss" ]; then echo " CONFIG_SET all $(echo $CONFIG_SET | wc -w) in $tree/.config" else echo " CONFIG_SET NOT in the tree's .config:$_miss" echo " (the next build applies them)" fi fi echo echo "# the tree" if [ -z "${tree:-}" ] || [ ! -d "$tree" ]; then echo " none in $SRC -- nothing has been unpacked here" else echo " $tree" _bz="$tree/arch/x86/boot/bzImage" if [ ! -f "$tree/.config" ]; then echo " config none yet" elif [ -f "$_bz" ] && [ "$_bz" -nt "$tree/.config" ]; then echo " build FINISHED -- bzImage is newer than .config" echo " $(date -r "$_bz" '+%Y-%m-%d %H:%M')" elif [ -f "$tree/vmlinux" ] || find "$tree" -maxdepth 2 -name '*.o' -print -quit 2>/dev/null | grep -q .; then echo " build UNFINISHED -- object files, no current image" echo " (an interrupted make; lfs-kernel continues" echo " from here, it does not start over)" else echo " build not started" fi _nko="$(find "$tree" -name '*.ko' 2>/dev/null | wc -l)" echo " modules built $_nko in the tree" fi echo echo "# installed" if [ -n "$_ver" ] && [ -d "/lib/modules/$_ver" ]; then echo " /lib/modules/$_ver $(test -s "/lib/modules/$_ver/modules.dep" && echo "(depmod has run)" || echo "-- NO modules.dep")" echo " $(find "/lib/modules/$_ver" -name '*.ko*' 2>/dev/null | wc -l) module(s)" elif [ -n "$_ver" ]; then echo " /lib/modules/$_ver -- not there: this kernel was never installed" fi if [ -d "${ESP:-}" ]; then if [ -f "$ESP/$KERNEL_NAME" ]; then echo " $ESP/$KERNEL_NAME $(date -r "$ESP/$KERNEL_NAME" '+%Y-%m-%d %H:%M')" fi for _k in "$ESP/$KERNEL_NAME"-*; do [ -f "$_k" ] && echo " $(basename "$_k")" done if [ -n "$_ver" ] && [ -f "$ESP/config-$_ver" ] && [ -f "$tree/.config" ]; then if cmp -s "$ESP/config-$_ver" "$tree/.config"; then echo " the installed kernel was built from the tree's CURRENT config" else echo " the installed config DIFFERS from the tree's .config --" echo " what is in the tree has not been built and installed yet" fi fi else echo " $ESP is not mounted -- cannot say what is on the EFI partition" fi echo echo "# next" if [ -n "$_ver" ] && [ -f "$tree/.config" ] \ && { [ ! -f "$ESP/config-$_ver" ] || ! cmp -s "$ESP/config-$_ver" "$tree/.config"; }; then echo " lfs-kernel # build and install what the tree now has" echo " an interrupted build continues; nothing is thrown away." else echo " nothing owed: the tree, /lib/modules and the ESP agree." fi echo " lfs-kernel modules ath # is a driver there, is it loaded" echo " lfs-kernel config show # what the next build would use" exit 0 fi # -------------------------------------------------------------------------- # LOADING A MODULE, NOW AND AT EVERY BOOT. # # A driver built as a module does nothing until something loads it. For a # USB device udev does that when you plug it in; for everything else, and # on a machine where udev is not running, it is modprobe -- and then the # same name in /etc/sysconfig/modules, which is the file LFS's `modules` # bootscript reads. Two places, one command. # -------------------------------------------------------------------------- if [ -n "${_LOADCMD:-}" ]; then _MODFILE="${MODULES_FILE:-/etc/sysconfig/modules}" _boot=0; _names=() for _a in ${_LOADARGS[@]+"${_LOADARGS[@]}"}; do case "$_a" in --boot) _boot=1 ;; -*) echo "!! lfs-kernel $_LOADCMD: unknown option '$_a'"; exit 1 ;; *) _names+=("$_a") ;; esac done if [ "${#_names[@]}" = 0 ]; then echo "!! lfs-kernel $_LOADCMD [...] [--boot]" echo " which modules there are: lfs-kernel modules " exit 1 fi _rc=0 for _m in "${_names[@]}"; do _m="$(printf '%s' "$_m" | tr '-' '_')" if [ "$_LOADCMD" = unload ]; then if modprobe -r "$_m" 2>&1; then echo "# $_m: unloaded" else echo "!! $_m: could not be unloaded (in use? lsmod | grep $_m)" _rc=1 fi if [ -f "$_MODFILE" ] && grep -qE "^[[:space:]]*$_m([[:space:]]|$)" "$_MODFILE"; then cp -p "$_MODFILE" "$_MODFILE.bak-$(date +%Y%m%d-%H%M%S)" _t="$(mktemp)" grep -vE "^[[:space:]]*$_m([[:space:]]|$)" "$_MODFILE" > "$_t" \ && cat "$_t" > "$_MODFILE" rm -f "$_t" echo "# and taken out of $_MODFILE" fi continue fi if grep -qE "^${_m}[[:space:]]" /proc/modules 2>/dev/null; then echo "# $_m: already loaded" elif modprobe "$_m" 2>&1; then if grep -qE "^${_m}[[:space:]]" /proc/modules 2>/dev/null; then echo "# $_m: loaded" else # BUILT IN IS NOT A FAILURE. modprobe succeeds silently for # a symbol that is =y; the module simply never appears in # /proc/modules because it is part of the image. echo "# $_m: modprobe returned success but it is not in" echo "# /proc/modules -- it is probably built INTO the kernel" echo "# (lfs-kernel modules $_m says which)" fi else echo "!! $_m: modprobe refused it." echo " is it built? lfs-kernel modules $_m" echo " is it set? lfs-kernel config search $_m" _rc=1 continue fi # WHAT THE KERNEL SAID ABOUT IT, which is where a missing firmware # blob or an unsupported device id turns up. if command -v dmesg >/dev/null 2>&1; then _say="$(dmesg 2>/dev/null | tail -n 40 \ | grep -iE "${_m}|${_m//_/-}|firmware" | tail -n 5)" [ -n "$_say" ] && printf '%s\n' "$_say" | sed 's/^/ /' fi if [ "$_boot" = 1 ]; then if [ -f "$_MODFILE" ] && grep -qE "^[[:space:]]*$_m([[:space:]]|$)" "$_MODFILE"; then echo "# $_MODFILE already lists it" else [ -f "$_MODFILE" ] \ && cp -p "$_MODFILE" "$_MODFILE.bak-$(date +%Y%m%d-%H%M%S)" if [ ! -f "$_MODFILE" ]; then install -d "$(dirname "$_MODFILE")" { echo "# /etc/sysconfig/modules -- one module per line," echo "# loaded at boot by the 'modules' bootscript." echo "# Arguments may follow the name." } > "$_MODFILE" fi printf '%s\n' "$_m" >> "$_MODFILE" echo "# added to $_MODFILE (loaded at every boot)" fi else echo "# at every boot as well: lfs-kernel load $_m --boot" fi done exit $_rc fi # -------------------------------------------------------------------------- # the ESP has to be the EFI partition, not a directory of the same name. # Every copy below would SUCCEED on the root filesystem and the firmware # would find nothing -- a failure that only appears at the next boot. # -------------------------------------------------------------------------- [ -n "${ESP:-}" ] || { echo "!! ESP is not set in $CONF. Find it with: findmnt -t vfat"; exit 1; } if [ ! -d "$ESP" ]; then echo "!! $ESP does not exist. Mount the EFI partition first, or fix ESP" echo " in $CONF -- creating it here would put vmlinuz where the" echo " firmware cannot read it." exit 1 fi case "$(stat -f -c %T "$ESP" 2>/dev/null)" in msdos|vfat|fat*|exfat) ;; "") echo "# could not identify the filesystem under $ESP -- check it before rebooting" ;; *) echo "!! $ESP is not on a vfat filesystem, so it is not the EFI partition." echo " findmnt -t vfat # and set ESP in $CONF"; exit 1 ;; esac # BUILDING IN A CHROOT IS FINE -- IF THE PATHS ARE THIS TREE'S. # The chroot usually IS the system that will boot. What can go astray is # each path, so name them and carry on. Judge only on positive evidence: # where /proc/1/root cannot be read, the comparison says nothing. _realroot="$(stat -c %d:%i /proc/1/root/. 2>/dev/null || true)" _thisroot="$(stat -c %d:%i / 2>/dev/null || true)" if [ "${_VERIFY:-0}" != 1 ] \ && [ -n "$_realroot" ] && [ -n "$_thisroot" ] && [ "$_realroot" != "$_thisroot" ]; then echo "# a chroot: fine, as long as these are the tree you boot --" echo "# $ESP (EFI partition, mounted here)" echo "# /lib/modules (modules_install and depmod write here)" echo "# /proc/config.gz (shared with the host: the RUNNING kernel)" echo "# set CONFIG_FROM=file: in $CONF to start from another config" fi # ---- verify: is everything where it should be? --------------------------- # # "is the kernel part done? do we have a validation command so we know # everything is installed as expected?" Every piece this tool installs is # checked here, by looking at the system rather than at what the build # printed: the image and its companions on the ESP, the modules and their # dependency index, the headers, the embedded firmware, and the two package # manifests. if [ "${_VERIFY:-0}" = 1 ]; then _bad=0 _ck() { # _ck local what="$1"; shift if "$@" >/dev/null 2>&1; then echo " ok $what" else echo " FAIL $what" _bad=$((_bad + 1)) fi } if [ -z "$VERSION" ] || [ "$VERSION" = latest ]; then # whatever was installed most recently, rather than whatever # kernel.org offers today VERSION="$(ls -1t "$ESP/$KERNEL_NAME"-* 2>/dev/null | head -1)" VERSION="${VERSION##*/$KERNEL_NAME-}" fi [ -n "$VERSION" ] || { echo "!! nothing installed as $ESP/$KERNEL_NAME-* yet"; exit 1; } echo "### verifying kernel $VERSION" _ck "$ESP/$KERNEL_NAME (what the firmware loads)" test -s "$ESP/$KERNEL_NAME" _ck "$ESP/$KERNEL_NAME-$VERSION" test -s "$ESP/$KERNEL_NAME-$VERSION" _ck "the two are the same image" cmp -s "$ESP/$KERNEL_NAME" "$ESP/$KERNEL_NAME-$VERSION" _ck "$ESP/System.map-$VERSION" test -s "$ESP/System.map-$VERSION" _ck "$ESP/config-$VERSION" test -s "$ESP/config-$VERSION" _ck "/lib/modules/$VERSION" test -d "/lib/modules/$VERSION" _ck "modules.dep (depmod has run)" test -s "/lib/modules/$VERSION/modules.dep" _ck "headers in /usr/include/linux" test -s /usr/include/linux/version.h # the firmware this config embeds must be on disk, or the image is # missing pieces it thinks it has _cfg="$ESP/config-$VERSION" _fwlist="$(sed -n 's/^CONFIG_EXTRA_FIRMWARE="\(.*\)"$/\1/p' "$_cfg" 2>/dev/null)" if [ -n "$_fwlist" ]; then for _f in $_fwlist; do _ck "firmware /lib/firmware/$_f" test -e "/lib/firmware/$_f" done fi # every directory FIRMWARE_ONLY asked for should be on disk. The # embedded list above only covers blobs the IMAGE carries; the wireless # firmware is loaded at run time and was checked by nothing at all -- # which is why "what about the iwl stuff?" had to be asked. for _d in ${FIRMWARE_ONLY:-}; do case "$_d" in *[*?[]*) _ck "firmware matching $_d" \ sh -c "ls -d /lib/firmware/$_d >/dev/null 2>&1" ;; *) _ck "firmware /lib/firmware/$_d" test -e "/lib/firmware/$_d" ;; esac done # named for what it actually is, since intel/ is where the wireless lives [ -d /lib/firmware/intel/iwlwifi ] \ && echo " note $(ls /lib/firmware/intel/iwlwifi 2>/dev/null | wc -l) Intel wireless blob(s) in intel/iwlwifi" # ...and the out-of-tree module, when the config asks for one. verify # checked everything the tool installs EXCEPT this -- the one piece whose # build failed three times, which is precisely the thing a report should # not be silent about. case "${V4L2LOOPBACK:-no}" in yes|1|true) _ck "v4l2loopback.ko in /lib/modules/$VERSION" \ sh -c "find /lib/modules/'$VERSION' -name 'v4l2loopback.ko*' \ -print -quit 2>/dev/null | grep -q ." _ck "modules.dep knows v4l2loopback" \ sh -c "grep -q v4l2loopback /lib/modules/'$VERSION'/modules.dep 2>/dev/null" ;; esac # and the package side: both accounts should have a manifest for _a in "p_$PKG" p_linux-firmware; do _h="$(getent passwd "$_a" 2>/dev/null | cut -d: -f6)" if [ -n "$_h" ]; then _ck "$_a records its files" test -s "$_h/pkg.lst" fi done # A manifest with a source tree in it is a manifest that would delete a # source tree. 266k entries was the symptom; anything above a few # thousand for the kernel means the home slipped back in. _h="$(getent passwd "p_$PKG" 2>/dev/null | cut -d: -f6)" if [ -n "$_h" ] && [ -s "$_h/pkg.lst" ]; then _n="$(grep -c . "$_h/pkg.lst")" echo " note p_$PKG records $_n file(s)" # ...and if that is implausible, SHOW the lines rather than guessing # at which path let them in. Two releases were spent reasoning about # a filter without ever looking at what it failed to match. if [ "$_n" -gt 50000 ]; then echo " FAIL that is far too many for a kernel -- a source tree is in it." echo " the first few entries are:" head -3 "$_h/pkg.lst" | sed 's/^/ /' echo " and around the middle:" sed -n "$((_n / 2))p" "$_h/pkg.lst" | sed 's/^/ /' _bad=$((_bad + 1)) fi # Two ways a source tree gets in: directly, or through # /lib/modules//build, which is a SYMLINK to it. The second is # the one that actually happened -- and the check that looked only # for the home said nothing about 266k entries. if grep -q "^$_h/" "$_h/pkg.lst" 2>/dev/null; then echo " FAIL the manifest includes the account's own home" echo " (sources, not installed files)" _bad=$((_bad + 1)) fi if grep -qE "^/lib/modules/[^/]+/(build|source)/." "$_h/pkg.lst" 2>/dev/null; then echo " FAIL the manifest was scanned THROUGH" echo " /lib/modules/$VERSION/build, a symlink to the source" echo " tree -- so it lists the whole tree as installed files." _bad=$((_bad + 1)) fi [ "$_bad" = 0 ] || echo " fix both with: packagemanager reload-pkg-list $PKG" fi _lg=/etc/pkgusr/kernel.last-good if [ -s "$_lg" ]; then _lgv="$(cat "$_lg")" if [ "$_lgv" = "$(uname -r | sed 's/-.*//')" ]; then echo " note last kernel known to boot: $_lgv (it is running now)" else echo " note last kernel known to boot: $_lgv" fi else echo " note no kernel confirmed to boot yet -- $VERSION is untested," echo " and the running $(uname -r) was not built by this tool" fi echo if [ "$_bad" = 0 ]; then echo "everything the kernel step installs is present." echo " running kernel: $(uname -r) installed: $VERSION" exit 0 fi echo "$_bad check(s) failed -- run lfs-kernel to (re)install." exit 1 fi # ---- firmware, as its own package ---------------------------------------- # # The drivers need it at RUN time and CONFIG_EXTRA_FIRMWARE needs it at BUILD # time, so it is installed before the check below rather than skipped. It # gets its own package user: linux-firmware is a separate upstream project # with its own release pace, and `packagemanager verify` should see it as # one thing. : "${FIRMWARE_FROM:=none}" # A pattern is anything with a wildcard; cone mode cannot express one. _sparse_mode() { case "$*" in *[*?[]*) printf '%s' "--no-cone" ;; *) printf '' ;; esac } # In --no-cone mode each entry is a gitignore-style pattern, and a bare # directory name has to become /dir/ to mean "that directory". _sparse_args() { local mode e out="" mode="$(_sparse_mode "$@")" for e in $*; do if [ -n "$mode" ]; then case "$e" in */|*[*?[]*) out="$out $e" ;; *) out="$out /$e/" ;; esac else out="$out $e" fi done # WHENCE lists the flat-name links (see _fw_whence_links). Cone mode # always checks out top-level files; pattern mode only what it is told. [ -n "$mode" ] && out="$out /WHENCE" printf '%s' "${out# }" } # WHAT IS THERE, NOT JUST WHAT IS NOT. linux-firmware moves blobs between # the top level and subdirectories between releases, so a name or pattern # from last year can simply be gone. "iwlwifi does not exist" sends the # reader to go and look; the search below is the answer. _firmware_not_found() { local src="$1" want="$2" stem stem="$(printf '%s' "$want" | sed 's/[*?].*//; s/[-_]*$//')" echo "!! FIRMWARE_ONLY asks for '$want', which is not in $src" [ -n "$stem" ] || return 0 # ASK THE INDEX, NOT THE WORKING TREE. # # A sparse checkout REMOVES everything it did not select, so searching # the directory finds only what is already installed -- amdgpu and intel # -- and reports "nothing" for the very file being looked for. git's # index lists every path the repository has, selected or not, which is # the question actually being asked. local _hits="" if [ -d "$src/.git" ]; then _hits="$(_fw_index "$src" | grep -i -- "$stem" | head -12)" if [ -n "$_FW_INDEX_ERR" ]; then echo " (could not read the repository index, so this is what is on" echo " disk only: $_FW_INDEX_ERR)" fi fi [ -n "$_hits" ] || _hits="$(find "$src" -maxdepth 2 -iname "*$stem*" \ -printf '%P\n' 2>/dev/null | head -12)" if [ -n "$_hits" ]; then echo " the repository has these with '$stem' in the name:" printf ' %s\n' $_hits echo " Put a directory or a pattern covering them in FIRMWARE_ONLY" echo " in $CONF -- a directory name is the simplest thing that works." else echo " nothing in the repository matches '$stem' at all." if [ -d "$src/.git" ]; then echo " its top level holds:" git -C "$src" ls-tree --name-only HEAD 2>/dev/null | head -12 \ | sed 's/^/ /' fi fi } # THE REGULATORY DATABASE IS NOT IN linux-firmware. # # cfg80211 asks the loader for regulatory.db, linux-firmware does not carry # it, and no FIRMWARE_ONLY entry can conjure it -- worse, an entry naming it # makes the whole firmware install FAIL, because a pattern matching nothing # in the repository is an error. It is released on its own as # wireless-regdb, and it belongs to the same package user as the rest of # /lib/firmware: one directory, one owner. REGDB_BASE="${REGDB_BASE:-https://www.kernel.org/pub/software/network/wireless-regdb}" _install_regdb() { local acct=p_linux-firmware home tar url ver work case "${REGDB:-yes}" in yes|1|true) ;; *) return 0 ;; esac [ -e /lib/firmware/regulatory.db ] && [ "${1:-}" != force ] && return 0 id "$acct" >/dev/null 2>&1 || packagemanager add-user linux-firmware || return 1 home="$(getent passwd "$acct" | cut -d: -f6)" # ASK, DO NOT ASSUME A VERSION: the directory listing names the releases, # and a hard-coded date goes stale between them. ver="$(wget -qO- "$REGDB_BASE/" 2>/dev/null \ | sed -n 's/.*wireless-regdb-\([0-9.]*\)\.tar\.xz.*/\1/p' \ | sort -V | tail -n1)" if [ -z "$ver" ]; then echo "!! could not read $REGDB_BASE/ -- no network, or the layout moved." echo " Fetch a wireless-regdb tarball by hand, then:" echo " su - $acct -c 'cp regulatory.db regulatory.db.p7s /lib/firmware/'" return 1 fi url="$REGDB_BASE/wireless-regdb-$ver.tar.xz" tar="$home/src/wireless-regdb-$ver.tar.xz" install -d -o "$acct" -g "$acct" -m 755 "$home/src" 2>/dev/null # CONFIRM THE DIRECTORY BEFORE DOWNLOADING INTO IT. `install -d -o` on # an account that does not exist fails quietly, and the next line then # reports a download failure for a URL that was never the problem. [ -d "$home/src" ] || { echo "!! $home/src does not exist and could not be created" echo " (does the account $acct have that home? lfs-helper fix-home $acct --run)" return 1; } echo "# wireless-regdb $ver (the regulatory database, not in linux-firmware)" [ -s "$tar" ] || su - "$acct" -c "wget -qO '$tar' '$url'" || { echo "!! could not download $url"; rm -f "$tar"; return 1; } work="$home/src/wireless-regdb-$ver" su - "$acct" -c "cd '$home/src' && tar -xf '$tar'" || return 1 [ -s "$work/regulatory.db" ] || { echo "!! $tar unpacked without a regulatory.db in it"; return 1; } install -d -m 755 /lib/firmware 2>/dev/null [ "$(stat -c %U /lib/firmware 2>/dev/null)" = "$acct" ] \ || chown "$acct:$acct" /lib/firmware 2>/dev/null su - "$acct" -c "cp -a '$work/regulatory.db' '$work/regulatory.db.p7s' /lib/firmware/" \ || { echo "!! could not install the database as $acct"; return 1; } if [ -s /lib/firmware/regulatory.db ]; then echo "# installed /lib/firmware/regulatory.db ($(stat -c %s /lib/firmware/regulatory.db) bytes)" echo "# /lib/firmware/regulatory.db.p7s" packagemanager reload-pkg-list linux-firmware >/dev/null 2>&1 || true return 0 fi echo "!! /lib/firmware/regulatory.db is absent after the copy" return 1 } _install_firmware() { local acct="p_linux-firmware" src="" only="${FIRMWARE_ONLY:-}" id "$acct" >/dev/null 2>&1 || packagemanager add-user linux-firmware || return 1 lfs-helper fix-home "$acct" --run >/dev/null 2>&1 || true local home; home="$(getent passwd "$acct" | cut -d: -f6)" case "$FIRMWARE_FROM" in none) return 0 ;; copy:*) src="${FIRMWARE_FROM#copy:}" [ -d "$src" ] || { echo "!! $src does not exist (FIRMWARE_FROM)"; return 1; } ;; git) src="$home/src/linux-firmware" install -d -o "$acct" -g "$acct" -m 755 "$home/src" # linux-firmware is over ten gigabytes. With FIRMWARE_ONLY set # a SPARSE checkout fetches those directories and nothing else -- # amdgpu alone is a few hundred megabytes. Without it, the whole # thing, which is what "empty means everything" costs. if [ -d "$src/.git" ]; then echo "# updating linux-firmware" su - "$acct" -c "cd '$src' && git pull --ff-only" || return 1 if [ -n "$only" ]; then # not `|| true`: if this fails the checkout keeps whatever # the last run selected, and the missing firmware is then # blamed on the pattern su - "$acct" -c "cd '$src' && git sparse-checkout set $(_sparse_mode "$only") $(_sparse_args "$only")" \ || { echo "!! could not narrow the firmware checkout to: $only"; return 1; } fi elif [ -n "$only" ]; then echo "# cloning linux-firmware, only: $only" su - "$acct" -c "git clone --depth 1 --filter=blob:none --sparse '$FIRMWARE_GIT' '$src'" || return 1 # --no-cone when any entry is a PATTERN rather than a # directory: Intel wireless firmware is iwlwifi-cc-a0-*.ucode # at the top level, and cone mode (the default) matches # directories only, so those files would never arrive. su - "$acct" -c "cd '$src' && git sparse-checkout set $(_sparse_mode "$only") $(_sparse_args "$only")" || return 1 else echo "# cloning ALL of linux-firmware -- this is over 10 GB." echo "# set FIRMWARE_ONLY in $CONF to fetch only what you need." su - "$acct" -c "git clone --depth 1 '$FIRMWARE_GIT' '$src'" || return 1 fi ;; *) echo "!! FIRMWARE_FROM=$FIRMWARE_FROM not understood"; return 1 ;; esac # LIB/FIRMWARE IS THIS PACKAGE'S DIRECTORY. # # It already existed, owned by root or by whoever made it first, so every # copy failed with "Permission denied" -- two hundred lines of it. A # package user cannot chown, and firmware has exactly one owner, so root # hands the directory over here. (Where another package genuinely shares # a directory, the collector groups do this; that is not the case for # /lib/firmware.) local d install -d -m 755 /lib/firmware || return 1 if [ "$(stat -c %U /lib/firmware 2>/dev/null)" != "$acct" ]; then echo "# handing /lib/firmware to $acct (it owns the firmware)" chown "$acct:$acct" /lib/firmware || return 1 fi if [ -z "$only" ]; then # An EMPTY value is nearly always a quoting accident, not a request # for ten gigabytes: `FIRMWARE_ONLY=amdgpu intel iwlwifi-cc-*` # unquoted runs `intel` as a command and assigns nothing. Ask. echo "!! FIRMWARE_ONLY is empty, which means ALL of linux-firmware" echo " (over 10 GB). If that is really what you want, set" echo " FIRMWARE_ALL=yes" echo " in $CONF. If not, the value probably needs quoting:" echo ' FIRMWARE_ONLY="amdgpu intel iwlwifi-cc-*"' [ "${FIRMWARE_ALL:-no}" = yes ] || return 1 echo "# installing ALL of linux-firmware (FIRMWARE_ALL=yes)" su - "$acct" -c "cp -a '$src'/* /lib/firmware/" || return 1 else for d in $only; do # AN ENTRY FOR SOMETHING THIS REPOSITORY DOES NOT HAVE IS NOT AN # ERROR WHEN WE KNOW WHERE IT DOES LIVE. `regulatory.db*` in # FIRMWARE_ONLY failed the entire firmware install, and the # message blamed the pattern. _skip=0 while IFS='|' read -r _ep _epkg _eurl; do [ -n "${_ep:-}" ] || continue case "$_ep" in $d) _skip=1 ;; esac done < <(_fw_elsewhere) if [ "$_skip" = 1 ]; then echo "# '$d' is not in linux-firmware -- it is wireless-regdb;" echo "# installing that instead (REGDB=${REGDB:-yes})" _install_regdb || echo "# ...which did not work; see above" continue fi # a pattern matches files at the top level; a name is a directory case "$d" in *[*?[]*) # A PATTERN MATCHES AT ANY DEPTH, AND A LINK NAME COUNTS. # # linux-firmware moved iwlwifi-*.ucode into intel/iwlwifi/ # and keeps the flat names the driver asks for as `Link:` # lines in WHENCE. The sparse checkout (gitignore rules) # already fetched them at their new depth; this looked # only at the top level and stopped the kernel build. local _hits _links _hits="$(cd "$src" && find . -name .git -prune -o \( -type f -o -type l \) -name "$d" -print 2>/dev/null | sed 's|^\./||')" _links="$(_fw_whence_link_names "$src" | while IFS= read -r _ln; do case "$_ln" in $d) echo "$_ln" ;; esac; done)" if [ -z "$_hits" ] && [ -z "$_links" ]; then _firmware_not_found "$src" "$d"; return 1 fi echo "# installing firmware: $d ($(printf '%s\n' "$_hits" | grep -c .) file(s)$([ -n "$_links" ] && printf ', %s flat link(s) from WHENCE' "$(printf '%s\n' "$_links" | grep -c .)"))" # --parents, and from inside the checkout: `cp -a # $src/intel/iwlwifi /lib/firmware/` dropped the intel/ # and landed as /lib/firmware/iwlwifi, where nothing # looks for it. An entry keeps its path. if [ -n "$_hits" ]; then printf '%s\n' "$_hits" \ | su - "$acct" -c "cd '$src' && xargs -r -d '\\n' cp -a --parents -t /lib/firmware/" || return 1 fi ;; *) # ...the same help for a NAME. Only the pattern case # searched, so `iwlwifi` (a directory that does not # exist in this release) said "does not exist" and left # the reader exactly where the pattern had. [ -e "$src/$d" ] || { _firmware_not_found "$src" "$d"; return 1; } echo "# installing firmware: $d" su - "$acct" -c "cd '$src' && cp -a --parents '$d' /lib/firmware/" || return 1 ;; esac done fi [ -n "$only" ] && _fw_whence_links "$src" "$acct" _install_regdb || true packagemanager reload-pkg-list linux-firmware >/dev/null 2>&1 || true return 0 } # WHENCE's `Link: -> ` lines are how linux-firmware keeps the # names drivers ask for after moving a blob (iwlwifi-cc-a0-77.ucode -> # intel/iwlwifi/iwlwifi-cc-a0-77.ucode). Its own copy-firmware.sh makes # them; a selective install has to as well. The target is relative to the # link's directory. _fw_whence_link_names() { [ -r "$1/WHENCE" ] || return 0 sed -n 's/^Link:[[:space:]]*\([^[:space:]]*\)[[:space:]]*->.*/\1/p' "$1/WHENCE" } _fw_whence_links() { local src="$1" acct="$2" name target made=0 [ -r "$src/WHENCE" ] || { echo "# (no WHENCE in the checkout -- flat firmware names not linked)"; return 0; } while read -r name target; do [ -n "$name" ] && [ -n "$target" ] || continue # only for blobs that were installed, and never over a real file [ -e "/lib/firmware/$(dirname "$name")/$target" ] || continue [ -e "/lib/firmware/$name" ] || [ -L "/lib/firmware/$name" ] && continue su - "$acct" -c "mkdir -p '/lib/firmware/$(dirname "$name")' && ln -s '$target' '/lib/firmware/$name'" \ && made=$((made + 1)) done < <(sed -n 's/^Link:[[:space:]]*\([^[:space:]]*\)[[:space:]]*->[[:space:]]*\([^[:space:]]*\).*/\1 \2/p' "$src/WHENCE") [ "$made" -gt 0 ] && echo "# linked $made flat firmware name(s) from WHENCE" return 0 } # REPORT WHAT YOU FOUND, NOT WHAT YOU EXPECTED. # # FIRMWARE_ONLY="... intel" installed intel/, and the AX200 still had no # firmware, because the driver asks for a FLAT path: # iwlwifi 0000:03:00.0: Direct firmware load for iwlwifi-cc-a0-77.ucode failed # iwlwifi 0000:03:00.0: no suitable firmware found! # Four releases went into guessing linux-firmware's layout from the outside. # The running kernel already knows the answer -- it printed the exact path it # wanted -- and the checkout's git index knows where that file lives. So ask # both, instead of reasoning about directory names. _fw_requests() { # every firmware path THIS boot asked for and did not get dmesg 2>/dev/null | sed -n \ -e 's/.*Direct firmware load for \([^ ]*\) failed.*/\1/p' \ -e 's/.*firmware: failed to load \([^ ]*\).*/\1/p' \ | sed 's/[,:]$//' | sort -u } # ASK THE INDEX -- AND NOTICE WHEN THE ASKING FAILED. # # `git -C "$src" ls-files 2>/dev/null` returned nothing and rc 128, because # the repository belongs to p_linux-firmware and this runs as root: # fatal: detected dubious ownership in repository at '...' # The empty output was then reported as a FACT -- "linux-firmware does not # have a file by that name at all" -- about a file that is right there. An # error hidden behind 2>/dev/null becomes a confident wrong answer. _FW_INDEX_ERR="" _fw_index() { # _fw_index [] local src="$1" want="${2:-}" out="" err="" _FW_INDEX_ERR="" if [ -d "$src/.git" ]; then # -c safe.directory: the checkout is the firmware account's and this # runs as root, which git refuses to touch without being told. # the scratch file goes in /tmp, not in the repository: leaving # untracked files in someone else's checkout is not this tool's business local tmp; tmp="$(mktemp)" # ls-TREE, not ls-files. With a sparse index git collapses every # excluded directory into one entry, so `ls-files` answered `intel/` # and every nested blob under it looked absent -- 56 files reported # as "this checkout has no file by that name" while they were all # right there. ls-tree reads the commit, which no sparse setting # touches. err="$(git -c "safe.directory=$src" -C "$src" ls-tree -r --name-only HEAD 2>&1 >"$tmp")" if [ -n "$err" ] && [ ! -s "$tmp" ]; then _FW_INDEX_ERR="$err" rm -f "$tmp" return 1 fi out="$(cat "$tmp")" rm -f "$tmp" else out="$(cd "$src" && find . -type f -printf '%P\n' 2>/dev/null)" fi if [ -n "$want" ]; then printf '%s\n' "$out" | grep -- "\(^\|/\)$want$" else printf '%s\n' "$out" fi } _fw_repo() { # the linux-firmware checkout, or empty local home home="$(getent passwd p_linux-firmware 2>/dev/null | cut -d: -f6)" case "$FIRMWARE_FROM" in copy:*) printf '%s' "${FIRMWARE_FROM#copy:}" ;; *) [ -n "$home" ] && printf '%s' "$home/src/linux-firmware" ;; esac } # WHICH KERNEL'S MODULES? In a chroot the running kernel is the host's, so # `uname -r` is the wrong answer. Prefer the version this tool is configured # for, then whatever is installed under /lib/modules, and SAY which one. _fw_modver() { local v for v in "${VERSION:-}" "$(uname -r)"; do [ -n "$v" ] && [ -d "/lib/modules/$v" ] && { printf '%s' "$v"; return 0; } done ls -1d /lib/modules/*/ 2>/dev/null | sed 's|.*/modules/||; s|/$||' \ | sort -V | tail -n1 } # Firmware that is NOT in linux-firmware, so "not in the checkout" is the # right answer and updating the checkout will never help. _fw_elsewhere() { cat <<'ELSE' regulatory.db|wireless-regdb|https://kernel.org/pub/software/network/wireless-regdb/ regulatory.db.p7s|wireless-regdb|https://kernel.org/pub/software/network/wireless-regdb/ ELSE } # WHAT WILL BE ASKED FOR -- from sources that also work in a chroot. # # dmesg answers "what did the kernel that is running ask for", which in a # chroot is the host's boot: it reported one missing file for 7.2.0 while the # kernel of interest was 7.2.4, and said nothing about the wireless at all. # The tree can be asked directly instead: the modules it installed declare # their firmware (modinfo -F firmware), and the hardware is right here in # /sys, so the two together give the list for THIS machine and THIS kernel # without booting it. # # Prints "|" so every line can say where the fact came from. _fw_wanted() { # _fw_wanted local ver="${1:-}" a m f cfg # 1. the boot that is running -- _fw_requests is the one dmesg parser _fw_requests | sed 's/^/boot|/' # 2. this tree's modules, for the hardware that is actually present if [ -n "$ver" ] && [ -d "/lib/modules/$ver" ] \ && command -v modinfo >/dev/null 2>&1; then find /sys/devices -name modalias -print 2>/dev/null \ | xargs -r cat 2>/dev/null | sort -u \ | while IFS= read -r a; do [ -n "$a" ] || continue # modprobe resolves an alias through the TREE's # modules.alias when told which version to use modprobe -S "$ver" -R -- "$a" 2>/dev/null || true done | sort -u \ | while IFS= read -r m; do [ -n "$m" ] || continue # the MODULE's name travels with the request: a module # declares firmware for every chip it supports, so these # are candidates, and which module they came from is the # only thing that makes them readable modinfo -k "$ver" -F firmware -- "$m" 2>/dev/null \ | sed "s|^|modules:$m\||" done fi # 3. what the config embeds -- those must be on disk to BUILD, and are # the one part a running kernel never complains about for cfg in "$ESP/config-$ver" "/boot/config-$ver" /proc/config.gz; do [ -r "$cfg" ] || continue case "$cfg" in *.gz) zcat "$cfg" 2>/dev/null ;; *) cat "$cfg" ;; esac | sed -n 's/^CONFIG_EXTRA_FIRMWARE="\(.*\)"$/\1/p' \ | tr ' ' '\n' | sed '/^$/d; s/^/config|/' break done } # A MODULE DECLARES FIRMWARE FOR EVERY CHIP IT SUPPORTS. # # The first version of the modules scan printed 56 lines of "MISSING", each # with its own remedy: amdgpu blobs for Vega, Sienna Cichlid and Aldebaran, # iwlwifi blobs for five RF families this laptop does not have. Every one of # them was a fact -- `modinfo -F firmware amdgpu` really does list them -- # and the conclusion drawn from them ("56 missing") was wrong: the driver # loads what its own chip needs and ignores the rest. So a boot failure and # a module declaration are not the same kind of statement and must not be # printed the same way. # WHAT IS THERE, NEXT TO WHAT IS NOT. # # `iwlwifi-cc-a0-77.ucode` missing, on a machine whose wifi worked under the # previous kernel, means one of two things -- the file is on disk under # another directory, or an OLDER API version of it is installed and the new # driver will not take it -- and the report said neither. Both are one # `find` and one glob away, and both change what you do next. _FW_LOCAL="" _fw_neighbours() { # _fw_neighbours [] # THE EXPLANATION GOES TO THE READER, THE PATH GOES IN A VARIABLE. # Returning the path on stdout meant the caller's `$(...)` swallowed # every line of the explanation with it, and the report said only # "installed" with no word about where the file had been. local req="$1" src="${2:-}" base stem ext found sib _api _FW_LOCAL="" base="$(basename "$req")" # 1. the same file, somewhere else under /lib/firmware. FIRMWARE_ONLY # entries keep the repository's path, and a driver asks the loader for # its own -- so `intel` installs intel/iwlwifi/x.ucode while the # driver wants x.ucode at the top. found="$(find /lib/firmware -name "$base" -printf '%P\n' 2>/dev/null \ | grep -v "^$req$" | head -3)" if [ -n "$found" ]; then echo " it IS on disk, at another path:" printf ' /lib/firmware/%s\n' $found echo " the loader asks for /lib/firmware/$req and nothing else," echo " so --fix puts a copy there." _FW_LOCAL="$(printf '%s' "$found" | head -n1)" return 0 fi # 2. an API-versioned sibling: iwlwifi-cc-a0-72.ucode when -77 is asked # for. The number is the firmware API the driver demands, so an older # one on disk is the whole story: this kernel needs a newer blob. case "$base" in *-[0-9][0-9].*|*-[0-9][0-9][0-9].*) ext="${base##*.}" stem="${base%-*}" sib="$(ls -1 "/lib/firmware/$(dirname "$req")/$stem"-*."$ext" \ 2>/dev/null | xargs -r -n1 basename | tr '\n' ' ')" [ -n "$sib" ] && { echo " installed already, for the same device but an older" echo " firmware API: $sib" _api="${base##*-}"; _api="${_api%.*}" echo " This kernel's driver requires API $_api and will not" echo " fall back, so the checkout needs to be newer."; } if [ -n "$src" ] && [ -d "$src" ]; then sib="$(_fw_index "$src" | grep -- "\(^\|/\)$stem-[0-9]*\.$ext$" \ | xargs -r -n1 basename | sort -V | tail -3 | tr '\n' ' ')" [ -n "$sib" ] && echo " the checkout has: $sib" fi ;; esac return 1 } _firmware_missing() { local fix="${1:-0}" all="${2:-0}" src req f hits n=0 gone=0 fixed=0 failed=0 local sugg="" ver source chroot=0 _rr _tr seen="" cand=0 cdir mod src="$(_fw_repo)" ver="$(_fw_modver)" cdir="$(mktemp -d)" _rr="$(stat -c %d:%i /proc/1/root/. 2>/dev/null || true)" _tr="$(stat -c %d:%i / 2>/dev/null || true)" [ -n "$_rr" ] && [ -n "$_tr" ] && [ "$_rr" != "$_tr" ] && chroot=1 echo "### firmware this machine will ask for, and has not got" echo "# modules scanned: ${ver:-(none installed under /lib/modules)}" echo "# running kernel: $(uname -r)$([ "$chroot" = 1 ] && printf ' (the HOST kernel -- this is a chroot)')" if [ "$chroot" = 1 ]; then echo "# in a chroot, 'boot' lines describe the HOST's boot -- possibly" echo "# another distribution on other hardware, in which case they can" echo "# neither confirm nor deny anything about this tree. 'modules'" echo "# and 'config' lines are this tree's." fi [ -z "$ver" ] && echo "# no modules to scan, so only the running boot can be asked." echo while IFS='|' read -r source req; do [ -n "${req:-}" ] || continue case "$req" in */) continue ;; esac case " $seen " in *" $req "*) continue ;; esac seen="${seen:-} $req" n=$((n + 1)) if [ -e "/lib/firmware/$req" ]; then # SAY WHAT IS SATISFIED, especially here. A boot line that is # now present is the answer to "will this work when I boot it", # and it was skipped silently -- so a run with everything in # place looked identical to one that had not looked at all. case "$source" in modules:*) printf '%s\n' "$req" >> "$cdir/${source#modules:}.ok" ;; *) echo " ok $req ($source) -- installed now" ;; esac continue fi # A DECLARATION IS A CANDIDATE, NOT A FAILURE. Collected per module # and summarised below; only boot and config lines are treated as # things that must be fixed. case "$source" in modules:*) mod="${source#modules:}" printf '%s\n' "$req" >> "$cdir/$mod" cand=$((cand + 1)) [ "$all" = 1 ] || continue ;; esac gone=$((gone + 1)) echo " MISSING /lib/firmware/$req ($source)" while IFS='|' read -r _ep _epkg _eurl; do [ "$_ep" = "$req" ] || continue echo " not part of linux-firmware: it is released as $_epkg" echo " $_eurl" if [ "$fix" = 1 ] && [ "$_epkg" = wireless-regdb ]; then _install_regdb force && fixed=$((fixed + 1)) else echo " install it: lfs-kernel firmware --missing --fix" echo " (without it cfg80211 falls back to the most" echo " conservative channels and power -- wifi still works)" fi continue 2 done < <(_fw_elsewhere) # WHAT IS THERE, before what is not: a copy under another path, or an # older API version, answers the question without the repository. _fw_neighbours "$req" "$src" if [ -n "$_FW_LOCAL" ] && [ -e "/lib/firmware/$_FW_LOCAL" ]; then if [ "$fix" = 1 ]; then _fw_place "/lib/firmware" "$req" "$_FW_LOCAL" \ && fixed=$((fixed + 1)) fi continue fi if [ -z "$src" ] || [ ! -d "$src" ]; then echo " no linux-firmware checkout to look in" echo " (FIRMWARE_FROM=$FIRMWARE_FROM; run lfs-kernel firmware first)" continue fi hits="$(_fw_index "$src" "$(basename "$req")" | head -4)" if [ -n "$_FW_INDEX_ERR" ]; then echo " could not read the repository, so where this file" echo " lives is UNKNOWN -- not absent:" printf ' %s\n' "$_FW_INDEX_ERR" failed=$((failed + 1)) continue fi if [ -z "$hits" ]; then echo " this checkout has no file by that name." echo " su - p_linux-firmware -c 'git -C $src pull'" continue fi printf ' the repository has it at: %s\n' $hits for f in $hits; do if [ "$f" = "$req" ]; then case "$f" in */) ;; */*) sugg="$sugg ${f%/*}" ;; *) sugg="$sugg $f" ;; esac else echo " no FIRMWARE_ONLY entry can express this: the" echo " repository keeps it at '$f', the driver asks for" echo " '$req'. --fix installs it where the loader looks." fi done [ "$fix" = 1 ] && _fw_place "$src" "$req" "$(echo $hits | cut -d" " -f1)" \ && fixed=$((fixed + 1)) done < <(_fw_wanted "$ver") [ "$gone" = 0 ] && [ "$n" -gt 0 ] \ && echo " nothing that has actually been asked for is missing." # ---- the candidates, per module, in a few lines rather than fifty ---- if [ "$cand" -gt 0 ] && [ "$all" != 1 ]; then echo echo "Declared but not installed -- CANDIDATES, not failures. A module" echo "declares firmware for every chip it supports; yours loads only" echo "the ones it needs, and never asks for the others:" for f in "$cdir"/*; do [ -f "$f" ] || continue case "$f" in *.ok) continue ;; esac mod="$(basename "$f")" echo " $mod: $(grep -c . "$f") of $(( $(grep -c . "$f") \ + $( [ -f "$cdir/$mod.ok" ] && grep -c . "$cdir/$mod.ok" || echo 0) )) \ declared file(s) not installed, e.g. $(head -n1 "$f")" done if [ "$chroot" = 1 ]; then echo " If a driver you need is silent, boot THIS kernel and run this" echo " again there: the 'boot' list above is the host's, and says" echo " nothing about the driver you are asking about." else echo " If a driver you need is silent, look at what it actually asked" echo " for -- the 'boot' list above, which is this kernel's." fi echo " To install every declared file anyway (large, mostly unused):" echo " lfs-kernel firmware --missing --fix --all" fi # WHICH FILE A DRIVER PICKS IS DECIDED AT RUN TIME. iwlwifi derives the # name from the RF and CRF ids it reads off the card, so no amount of # looking at modules and directories can promise it will find one. Say # that, rather than implying a green run is a working driver. if [ "$cand" -gt 0 ] || [ "$gone" -gt 0 ]; then echo echo " Only a boot of THIS kernel settles it: a driver derives the" echo " firmware name from the ids it reads off the card, so what is" echo " installed can be checked here, and what will be asked for" echo " cannot." fi rm -rf "$cdir" echo if [ "$n" = 0 ]; then echo "Nothing asks for firmware here: no failed load in this boot, and" echo "no module under /lib/modules${ver:+/$ver} declares any." return 0 fi if [ "$gone" = 0 ]; then return 0 fi if [ "$fix" = 1 ]; then echo "Placed $fixed of $gone file(s) at the exact path the driver asked for." if [ -n "$sugg" ]; then echo "Make it survive the next full run -- in $CONF:" echo " FIRMWARE_ONLY=\"$FIRMWARE_ONLY$(printf '%s' "$sugg")\"" fi [ "$fixed" -gt 0 ] && { echo "Then load the driver again (or reboot):" echo " modprobe -r # iwlwifi, amdgpu, ..."; } if [ "$failed" -gt 0 ]; then echo "$failed lookup(s) failed, so nothing is known about those files." return 1 fi [ "$fixed" = "$gone" ] && return 0 echo "Still missing: $((gone - fixed))." return 1 fi echo "Nothing was changed. Install what linux-firmware has:" echo " lfs-kernel firmware --missing --fix" if [ -n "$sugg" ]; then echo "or add them to FIRMWARE_ONLY in $CONF and re-run lfs-kernel firmware:" echo " FIRMWARE_ONLY=\"$FIRMWARE_ONLY$(printf '%s' "$sugg")\"" fi return 1 } # Install ONE blob at the path the driver actually asked for. The driver's # path and the repository's path are not the same thing (iwlwifi asks for a # flat name; the repository may keep it under a directory), and it is the # driver's that has to exist. # # ONE FILE MEANS ONE FILE. The first version widened the sparse checkout, # and fell back to `sparse-checkout disable` when the add was refused -- in # cone mode a file path is refused, so the fallback would have materialised # the whole of linux-firmware: over ten gigabytes, to fix one 2 MB blob. # `git show HEAD:` reads exactly one blob out of the repository and # needs no sparse mode at all (in a partial clone it fetches that blob and # nothing else). _fw_place() { # local src="$1" req="$2" rel="$3" acct=p_linux-firmware # SEPARATE STATEMENT. `local a="$2" d="/lib/firmware/$a"` reads $a before # this local has assigned it, and `set -u` kills the function: # lfs-kernel: line ...: req: unbound variable local dst="/lib/firmware/$req" id "$acct" >/dev/null 2>&1 || packagemanager add-user linux-firmware || return 1 # /lib/firmware is this package's directory (the full run hands it over); # the account has to be able to write the file it is about to own. install -d -m 755 /lib/firmware 2>/dev/null [ "$(stat -c %U /lib/firmware 2>/dev/null)" = "$acct" ] \ || chown "$acct:$acct" /lib/firmware 2>/dev/null case "$req" in */*) install -d -o "$acct" -g "$acct" -m 755 "/lib/firmware/$(dirname "$req")" \ 2>/dev/null || true ;; esac if [ -e "$src/$rel" ]; then local _cerr _cerr="$(su - "$acct" -c "cp -a '$src/$rel' '$dst'" 2>&1)" || { echo " !! could not copy it as $acct:" printf ' %s\n' "${_cerr:-(no message)}"; return 1; } elif [ -d "$src/.git" ]; then echo " reading it out of the repository (it is in the index," echo " not in the sparse checkout)" local _gerr # git's own message, not a guess about what went wrong: "does not # exist in HEAD" and "unable to read sha1 file" (a partial clone with # no network) need different answers from the reader. _gerr="$(su - "$acct" -c "git -c 'safe.directory=$src' -C '$src' show 'HEAD:$rel' > '$dst'" 2>&1)" || { echo " !! git could not produce $rel:" printf ' %s\n' "${_gerr:-(no message)}" echo " a partial clone fetches the blob on demand, so" echo " this needs the network; check with:" echo " su - $acct -c 'git -C $src fetch --depth 1'" rm -f "$dst"; return 1; } else echo " !! $rel is not in $src" return 1 fi # CONFIRM YOUR OWN EFFECT: a failed `git show` can leave an empty file # behind, which looks exactly like a successful install. if [ -s "$dst" ]; then echo " installed $dst ($(stat -c %s "$dst") bytes)" packagemanager reload-pkg-list linux-firmware >/dev/null 2>&1 || true return 0 fi echo " !! $dst is empty or absent after the copy" rm -f "$dst" return 1 } # ---- firmware without a kernel build -------------------------------------- if [ "${_FWONLY:-0}" = 1 ]; then case "${_FWARG:-}" in --missing|missing) _firmware_missing "${_FWFIX:-0}" "${_FWALL:-0}"; exit $? ;; "") [ "$FIRMWARE_FROM" = none ] \ && { echo "!! FIRMWARE_FROM=none in $CONF -- nothing to install."; exit 1; } _install_firmware || exit 1 echo "# firmware installed; no kernel was built or touched." exit 0 ;; *) echo "!! lfs-kernel firmware takes --missing [--fix] or nothing"; exit 1 ;; esac fi echo "# kernel $VERSION -> $ESP (tree $tree, $JOBS)" # ---- the plan, when --run was not given ---------------------------------- if [ "${_RUN:-0}" != 1 ]; then echo echo "### what a build would do (nothing has been built)" echo " version $VERSION" if [ -d "$tree" ]; then _bz="$tree/arch/x86/boot/bzImage" if [ -f "$tree/.config" ] && [ -f "$_bz" ] && [ "$_bz" -nt "$tree/.config" ]; then echo " tree $tree (already built -- make would do little)" elif [ -f "$tree/.config" ]; then echo " tree $tree (a build was started; make CONTINUES)" else echo " tree $tree (unpacked, not configured)" fi else echo " tree $tree -- would be fetched and unpacked" fi echo " config from $CONFIG_FROM, refreshed with ${CONFIG_REFRESH:-olddefconfig}" [ -n "${CONFIG_SET:-}" ] && echo " then set $CONFIG_SET" echo " installs $ESP/$KERNEL_NAME (previous kept as ${KERNEL_NAME}_old)" echo " /lib/modules/$VERSION, and the config beside the image" case "${FIRMWARE_FROM:-none}" in none) echo " firmware not touched (FIRMWARE_FROM=none)" ;; *) echo " firmware ${FIRMWARE_FROM}: ${FIRMWARE_ONLY:-everything}" ;; esac [ "${HEADERS:-no}" = yes ] \ && echo " headers /usr/include, as ${HEADERS_PKG:-linux-headers}" case "${V4L2LOOPBACK:-no}" in yes|1|true) echo " also v4l2loopback, built against this tree" ;; esac echo echo " This is an hour of compiling on most machines, and it REPLACES" echo " what $ESP/$KERNEL_NAME boots." echo " Go ahead: lfs-kernel${_CLI_VERSION:+ $_CLI_VERSION} --run" echo " Where we are: lfs-kernel status" exit 0 fi # -------------------------------------------------------------------------- # THE COMMANDS. Everything above chooses the version and checks the paths; # everything below is the build, and is meant to be read. # -------------------------------------------------------------------------- install -d "$SRC" if [ ! -d "$tree" ]; then [ -f "$tarball" ] || { echo "# fetching $MIRROR_TAR/v$major.x/linux-$VERSION.tar.xz" wget -q --show-progress -O "$tarball.part" \ "$MIRROR_TAR/v$major.x/linux-$VERSION.tar.xz" \ && mv "$tarball.part" "$tarball" \ || { rm -f "$tarball.part" echo "!! download failed -- KERNEL_LIST=1 shows what exists"; exit 1; } } tar -xf "$tarball" -C "$SRC" || exit 1 chown -R "$acct:$acct" "$tree" "$tarball" 2>/dev/null || true fi [ -d "$tree" ] || { echo "!! $tree is missing"; exit 1; } # boot logo (optional) if [ -n "${BOOTLOGO:-}" ]; then logo="$SRC/$(basename "$BOOTLOGO")" [ -f "$logo" ] || { wget -qO "$logo" "$BOOTLOGO" && chown "$acct:$acct" "$logo"; } || logo="" [ -n "$logo" ] && _as_pkg "cp -v '$logo' drivers/video/logo/logo_linux_clut224.ppm" fi # .config # running /proc/config.gz -- the kernel carrying the machine # last-good the config of the last kernel that BOOTED ($KCFG_DIR) # prev-good the one before it -- the way back from a bad answer # saved the newest config this tool built with, booted or not # file: any file; keep the tree's own .config _cfg_src="" case "$CONFIG_FROM" in running) if [ -r /proc/config.gz ]; then _as_pkg "gunzip < /proc/config.gz > .config" || exit 1 elif _kcfg_readable "$KCFG_DIR/last-good.config"; then # NOT A FAILURE WORTH STOPPING FOR. A kernel built without # CONFIG_IKCONFIG_PROC has no /proc/config.gz, and the config of # the last kernel that booted is the same answer from a file. echo "# /proc/config.gz is not readable (no CONFIG_IKCONFIG_PROC?)" _cfg_src="$KCFG_DIR/last-good.config" else echo "!! /proc/config.gz unreadable, and no saved config either." echo " Set CONFIG_FROM=file: in $CONF, or see:" echo " lfs-kernel config list" exit 1 fi ;; last-good|good) _cfg_src="$KCFG_DIR/last-good.config" ;; prev-good|prev) _cfg_src="$KCFG_DIR/prev-good.config" ;; saved|last) _cfg_src="$(_kcfg_newest)" ;; file:*) _as_pkg "cp -v '${CONFIG_FROM#file:}' .config" || exit 1 ;; keep) [ -f "$tree/.config" ] || { echo "!! no .config in $tree"; exit 1; } ;; *) echo "!! CONFIG_FROM=$CONFIG_FROM not understood"; exit 1 ;; esac if [ -n "$_cfg_src" ]; then _kcfg_readable "$_cfg_src" || { echo "!! CONFIG_FROM=$CONFIG_FROM, but $_cfg_src is not a config." echo " lfs-kernel config list # what there is" exit 1; } echo "# starting from $_cfg_src (kernel $(_kcfg_ver "$_cfg_src"))" _as_pkg "cp '$_cfg_src' .config" || exit 1 fi # ---- refreshing the config for a NEW kernel ------------------------------ # # A config from one kernel is never complete for another: every version adds # symbols the old file has no answer for. Something must decide them, and # which something matters: # # olddefconfig take the DEFAULT for each new symbol, no questions. The # right choice for an unattended run, and what this tool has # always done -- but the defaults are upstream's, not yours. # oldconfig ask about each new symbol, one at a time. Slow, and needs # someone at the terminal, but nothing is decided silently. # menuconfig open the editor and decide everything at once. # none use the .config exactly as it is; `make` will then ask, or # fail, depending on what is missing. # # It also says how many symbols the refresh had to answer, because "47 new # symbols were set to their defaults" is worth knowing before a boot. : "${CONFIG_REFRESH:=olddefconfig}" _cfg_before="$(mktemp)"; cp "$tree/.config" "$_cfg_before" 2>/dev/null || true case "$CONFIG_REFRESH" in none) echo "# not refreshing the config (CONFIG_REFRESH=none)" ;; olddefconfig|oldconfig|menuconfig|*config) echo "# refreshing the config for $VERSION (make $CONFIG_REFRESH) ..." _as_pkg "make $CONFIG_REFRESH" || exit 1 ;; *) echo "!! CONFIG_REFRESH=$CONFIG_REFRESH is not understood"; exit 1 ;; esac if [ -s "$_cfg_before" ] && [ "$CONFIG_REFRESH" != none ]; then _newsym="$(diff <(sort "$_cfg_before") <(sort "$tree/.config") 2>/dev/null \ | grep -c '^>' || true)" [ "${_newsym:-0}" -gt 0 ] \ && echo "# $_newsym config line(s) differ from the config you started from" fi rm -f "$_cfg_before" # ---- CONFIG_SET: this machine's own answers, applied last ---------------- # # The config comes from the running kernel (or a file), then a refresh # answers the new symbols. An option you decided on has to survive both, so # it is applied AFTER them, resolved once more (olddefconfig -- a forced # symbol can pull or drop others), and READ BACK: `scripts/config` writes # what it is told, and the resolve then silently reverts anything whose # dependencies are not met. if [ -n "${CONFIG_SET:-}" ]; then _cs_args="" for _cs in $CONFIG_SET; do _sym="${_cs%%=*}"; _val="${_cs#*=}"; _sym="${_sym#CONFIG_}" [ "$_sym" != "$_cs" ] || { echo "!! CONFIG_SET entry '$_cs' has no '=' -- SYMBOL=VALUE"; exit 1; } case "$_val" in y) _cs_args="$_cs_args --enable $_sym" ;; m) _cs_args="$_cs_args --module $_sym" ;; n) _cs_args="$_cs_args --disable $_sym" ;; [0-9]*) _cs_args="$_cs_args --set-val $_sym $_val" ;; *) _cs_args="$_cs_args --set-str $_sym ${_val//\"/}" ;; esac done echo "# applying CONFIG_SET: $CONFIG_SET" _as_pkg "./scripts/config $_cs_args && make olddefconfig" >/dev/null || exit 1 _cs_bad="" for _cs in $CONFIG_SET; do _sym="${_cs%%=*}"; _val="${_cs#*=}"; _sym="CONFIG_${_sym#CONFIG_}" case "$_val" in n) grep -qE "^$_sym=(y|m)" "$tree/.config" && _cs_bad="$_cs_bad $_sym=n" ;; y|m) grep -qx "$_sym=$_val" "$tree/.config" || _cs_bad="$_cs_bad $_sym=$_val" ;; *) grep -qx "$_sym=$_val" "$tree/.config" \ || grep -qx "$_sym=\"${_val//\"/}\"" "$tree/.config" \ || _cs_bad="$_cs_bad $_sym=$_val" ;; esac done if [ -n "$_cs_bad" ]; then echo "!! CONFIG_SET did not take -- the kernel reverted these:" for _cs in $_cs_bad; do echo " $_cs (now: $(grep -E "^(# )?${_cs%%=*}[= ]" "$tree/.config" | head -1))" done echo " a dependency is missing, or $VERSION has no such symbol." echo " Look it up: cd $tree && make menuconfig (then / and the name)" exit 1 fi echo "# all $(echo $CONFIG_SET | wc -w) set, read back from .config" fi # THE CONFIG IS KEPT BEFORE ANYTHING IS COMPILED. It is finished here -- # source, refresh and CONFIG_SET all applied -- and a build that dies at # hour two must not take the answers with it. _kcfg_archive "$tree/.config" "$VERSION" # what a Wayland compositor needs; a warning, not a refusal for opt in CONFIG_DRM CONFIG_DRM_FBDEV_EMULATION CONFIG_INPUT_EVDEV CONFIG_SYSVIPC; do grep -qE "^$opt=(y|m)" "$tree/.config" || echo "!! $opt is not set (sway needs it)" done # Drivers that need firmware are no use without the driver. These are the # ones a laptop misses most often; the firmware for each is in FIRMWARE_ONLY. for opt in CONFIG_IWLWIFI:"Intel wireless (AX200 and friends)" \ CONFIG_IWLMVM:"the AX200's own MVM firmware layer" \ CONFIG_BT_HCIBTUSB:"USB Bluetooth (the AX200's other half)" \ CONFIG_DRM_AMDGPU:"AMD graphics"; do _o="${opt%%:*}"; _why="${opt#*:}" grep -qE "^$_o=(y|m)" "$tree/.config" \ || echo "!! $_o is not set -- no $_why" done # FIRMWARE TROUBLE SHOULD NOT COST YOU THE KERNEL. # # This ran before the build and exited on failure, so a wrong FIRMWARE_ONLY # meant no kernel was built at all -- and the message did not say so. A # missing blob only BLOCKS the build when the config embeds it # (CONFIG_EXTRA_FIRMWARE); otherwise it is a run-time concern, and the check # further down catches the case that matters. _fw_ok=1 if [ "$FIRMWARE_FROM" != none ]; then _install_firmware || _fw_ok=0 fi if [ "$_fw_ok" = 0 ]; then if grep -q '^CONFIG_EXTRA_FIRMWARE="..*"$' "$tree/.config"; then echo "!! this config builds firmware INTO the kernel, so the build" echo " cannot proceed without it. Fix FIRMWARE_ONLY above." exit 1 fi echo "!! carrying on WITHOUT the firmware above: this config does not" echo " embed any (no CONFIG_EXTRA_FIRMWARE), so the kernel will build." echo " The drivers that wanted it will fail at run time until it is" echo " installed -- re-run lfs-kernel once FIRMWARE_ONLY is right." fi # FIRMWARE BUILT INTO THE KERNEL MUST BE ON DISK. # # CONFIG_EXTRA_FIRMWARE embeds blobs in the image, so the build reads them # from CONFIG_EXTRA_FIRMWARE_DIR (/lib/firmware by default). A config taken # from a distribution kernel usually lists a dozen, and an LFS tree has no # linux-firmware -- so the build dies deep in drivers/base, ten minutes in, # with a message that names one file and no reason: # No rule to make target '/lib/firmware/amdgpu/renoir_asd.bin' # Check them all before compiling anything, and name every one that is # missing. _fw="$(sed -n 's/^CONFIG_EXTRA_FIRMWARE="\(.*\)"$/\1/p' "$tree/.config")" if [ -n "$_fw" ]; then _fwdir="$(sed -n 's/^CONFIG_EXTRA_FIRMWARE_DIR="\(.*\)"$/\1/p' "$tree/.config")" : "${_fwdir:=/lib/firmware}" _missing_fw="" for _f in $_fw; do [ -f "$_fwdir/$_f" ] || _missing_fw="$_missing_fw $_f" done if [ -n "$_missing_fw" ]; then echo "!! this config builds firmware INTO the kernel" echo " (CONFIG_EXTRA_FIRMWARE), and these are not in $_fwdir:" for _f in $_missing_fw; do echo " $_f"; done echo " The build would fail in drivers/base after compiling for" echo " several minutes. Two ways out:" echo " * add them to FIRMWARE_ONLY in $CONF -- this tool installs" echo " firmware itself (FIRMWARE_FROM=$FIRMWARE_FROM). The" echo " subdirectory of each file above is what to list." echo " * build without it:" echo " cd $tree && ./scripts/config --set-str EXTRA_FIRMWARE ''" echo " then run lfs-kernel again" echo " the drivers then load it at RUN time from $_fwdir instead." exit 1 fi fi # /lib/modules, /usr/include and the doc directory end up owned and recorded # like any other package's files. echo "# compiling ($JOBS) -- this is the long one; expect many minutes ..." _as_pkg "make $JOBS" || exit 1 _as_pkg "make modules_prepare" || exit 1 echo "# installing modules into /lib/modules/$VERSION ..." _as_pkg "make modules_install" || exit 1 # KERNEL_NAME is what the boot manager loads (rEFInd reads the file name); # with ESP it is the pair that decides whether this is a test kernel or the # real one. # THE VERSION OF WHAT WAS BUILT, NOT OF WHAT WAS ASKED FOR. An empty # VERSION installed `vmlinuz-`, `config-` and `System.map-` beside the real # ones. The tree knows its own release once it has compiled. _built="$(cat "$tree/include/config/kernel.release" 2>/dev/null)" if [ -n "$_built" ] && [ "$_built" != "$VERSION" ]; then [ -n "$VERSION" ] && echo "# the tree says $_built (asked for $VERSION) -- installing as $_built" VERSION="$_built" fi [ -n "$VERSION" ] || { echo "!! no kernel version known for $tree -- not installing it under an empty name"; exit 1; } [ -f "$ESP/$KERNEL_NAME" ] && cp -v "$ESP/$KERNEL_NAME" "$ESP/${KERNEL_NAME}_old" cp -v "$tree/arch/x86/boot/bzImage" "$ESP/$KERNEL_NAME" cp -v "$tree/arch/x86/boot/bzImage" "$ESP/$KERNEL_NAME-$VERSION" cp -v "$tree/System.map" "$ESP/System.map-$VERSION" cp -v "$tree/.config" "$ESP/config-$VERSION" # SAY IT BEFORE IT TAKES A WHILE, NOT AFTER. # # Documentation is about ten thousand small files and the copy runs for a # minute or two in complete silence; so does the manifest scan below, which # walks the filesystem. A tool that goes quiet for two minutes is # indistinguishable from one that has hung. echo "# copying Documentation to /usr/share/doc/linux-$VERSION (~10k files, a minute or two) ..." _as_pkg "install -d /usr/share/doc/linux-$VERSION" || exit 1 _as_pkg "cp -r Documentation -T /usr/share/doc/linux-$VERSION" || exit 1 # DEPMOD THE KERNEL WE BUILT, NOT THE ONE THAT IS RUNNING. # # `depmod -a` with no argument means `uname -r` -- 7.2.0, the kernel this # chroot booted, whose /lib/modules does not exist here: # depmod: ERROR: could not open directory /lib/modules/7.2.0 # The modules that were just installed are 7.2.4's, and that is what needs # an index. depmod -a "$VERSION" || exit 1 # RECORD THE FILES NOW, WHILE THEY ARE INSTALLED. # # This used to run at the very end, after v4l2loopback and the headers -- so # when the module build failed, the manifest was never written and `verify` # reported "p_linux records its files: FAIL" about a kernel that was # perfectly installed. The kernel is on disk at this point; record it. # (Run again after the headers, which add /usr/include.) _record() { echo "# recording what $acct owns (a scan AS $acct, skipping /proc, the" echo "# package source trees and the caches; --root if something is missed)" packagemanager reload-pkg-list "$PKG" >/dev/null 2>&1 || return 0 local _h; _h="$(getent passwd "$acct" | cut -d: -f6)" echo "# recorded $(grep -c . "$_h/pkg.lst" 2>/dev/null) file(s) for $acct" } _record # v4l2loopback (optional, out of tree) case "$V4L2LOOPBACK" in yes|1|true) d="$SRC/v4l2loopback" # THE REPOSITORY IS THE PACKAGE USER'S, FROM THE FIRST COMMAND. # # git pull/clone ran as ROOT and the chown came afterwards, so the next # command -- run as the package user -- met a repository owned by # somebody else: # fatal: detected dubious ownership in repository at '.../v4l2loopback' # Owning it first and cloning as the account avoids the whole class, # rather than papering over it with safe.directory. install -d -o "$acct" -g "$acct" -m 755 "$d" || exit 1 [ -d "$d/.git" ] && chown -R "$acct:$acct" "$d" 2>/dev/null if [ -d "$d/.git" ]; then su - "$acct" -c "cd '$d' && git pull --ff-only" || exit 1 else su - "$acct" -c "git clone --depth 1 '$V4L2LOOPBACK_GIT' '$d'" || exit 1 fi # BUILD IT AGAINST THE TREE, NOT AGAINST /lib/modules/$(uname -r). # # An out-of-tree module's own Makefile defaults to # /lib/modules/`uname -r`/build, which is the RUNNING kernel: # make[1]: *** /lib/modules/7.2.0/build: No such file or directory # Driving kbuild from the tree directly sidesteps every such default and # works for any module, not just this one. su - "$acct" -c "make -C '$tree' M='$d' clean" || exit 1 su - "$acct" -c "make -C '$tree' M='$d' modules" || exit 1 su - "$acct" -c "make -C '$tree' M='$d' modules_install" || exit 1 depmod -a "$VERSION" || exit 1 ;; esac # THE HEADERS BELONG TO THE HEADERS PACKAGE. # # /usr/include/asm/*.h is owned by p_linux-headers -- the LFS step that # installed them -- and /usr/include is sticky, so p_linux cannot replace a # single one: # cp: cannot create regular file '/usr/include/asm/types.h': Permission denied # Installing a newer kernel's headers is an UPDATE OF THAT PACKAGE, not a # second package writing over it. So the copy runs as that account, and its # manifest is refreshed afterwards. (HEADERS=no in kernel.conf leaves # /usr/include alone entirely, which is right when the running system's # headers are deliberately older than the kernel being built.) : "${HEADERS:=yes}" : "${HEADERS_PKG:=linux-headers}" case "$HEADERS" in yes|1|true) _hacct="p_$HEADERS_PKG" if ! id "$_hacct" >/dev/null 2>&1; then packagemanager add-user "$HEADERS_PKG" \ || { echo "!! could not create $_hacct"; exit 1; } lfs-helper fix-home "$_hacct" --run >/dev/null 2>&1 || true fi echo "# generating headers ..." _as_pkg "make headers" || exit 1 _as_pkg "find usr/include -type f ! -name '*.h' -delete" # the tree is p_linux's; the destination is p_linux-headers'. Copy # via a place both can read. # ROOT COPIES, THEN HANDS THE FILES OVER. # # Running the copy AS p_linux-headers cannot work: the source is # inside p_linux's home, which is 0750, so it cannot even stat it -- # cp: cannot stat '.../linux-7.2.4/usr/include': Permission denied # and opening one account's home to another would undo the point of # having accounts. Root can read both sides; ownership afterwards is # what makes the files the headers package's. Same shape as the ESP # copies and the setuid bit. echo "# installing headers into /usr/include (as root, owned by $_hacct) ..." # WHAT IT INSTALLED, NOT WHERE IT INSTALLED. # # `chown -R $_hacct /usr/include` gave this package every header on # the system. /usr/include/gstreamer-1.0/gst/mse then belonged to # p_linux-headers, so gst-plugins-bad could not install into its own # directory, the repair handed it one file per round, and after three # rounds the bundled-copy heuristic concluded meson was vendoring # linux-headers. Every step of that was correct reasoning from a # wrong fact. # # So: copy, remember exactly which paths came out of the tree, and # chown only those. _hlist="$(mktemp)" ( cd "$tree/usr/include" && find . -mindepth 1 -printf '%P\n' ) > "$_hlist" cp -r "$tree/usr/include" /usr || { echo "!! could not install the headers into /usr/include" echo " if some of them belong to a third package:" echo " packagemanager reclaim $HEADERS_PKG --run" rm -f "$_hlist"; exit 1; } ( cd /usr/include && tr '\n' '\0' < "$_hlist" \ | xargs -0 -r chown -h "$_hacct:$_hacct" ) 2>/dev/null || true # the directory itself stays root's: it is shared by every package # that ships a header, and one owner for it is what caused this echo "# $(grep -c . "$_hlist") path(s) now owned by $_hacct;" echo "# /usr/include itself left as $(stat -c %U /usr/include)" rm -f "$_hlist" packagemanager reload-pkg-list "$HEADERS_PKG" >/dev/null 2>&1 || true echo "# headers installed and recorded for $_hacct" ;; *) echo "# headers: skipped (HEADERS=$HEADERS)" ;; esac # THE LAST WORKING SOURCE IS NEVER DELETED. # # A kernel that boots is the only way back from one that does not, and its # TREE is what you need to rebuild or bisect -- not just the bzImage. The # version that is running right now is recorded as last-good before this one # is; nothing here ever removes that tree. (An older comment here promised # a `lfs-kernel prune`; there is none, and before 1.14.324 typing it started # a full build instead of saying so.) # ONLY A KERNEL THAT HAS BOOTED IS KNOWN TO BOOT. # # The old `elif` wrote the version just built into last-good whenever the # file was empty -- so `verify` reported "last kernel known to boot: 7.2.4" # about a kernel that has never run, while 7.2.0 was the one carrying the # machine. A fallback that records a guess as a fact is worse than no # record: the whole point of this file is to name something you can go back # to. _lastgood=/etc/pkgusr/kernel.last-good _running="$(uname -r | sed 's/-.*//')" if [ "$_running" = "$VERSION" ]; then # the kernel just built is the one running: it demonstrably boots printf '%s\n' "$VERSION" > "$_lastgood" echo "# $VERSION is running, so it is now the kernel known to boot" _kcfg_mark_good "$tree/.config" "$VERSION" elif [ -d "$SRC/linux-$_running" ]; then printf '%s\n' "$_running" > "$_lastgood" echo "# keeping $SRC/linux-$_running as the last kernel known to boot" # ...and ITS config is the one known to work, not the one just built _kcfg_mark_good "$SRC/linux-$_running/.config" "$_running" else echo "# the running kernel is $_running, and there is no tree for it here," echo "# so nothing is recorded as known-good. $VERSION will be recorded" echo "# the first time lfs-kernel runs while $VERSION is booted." [ -s "$_lastgood" ] && echo "# (unchanged: $(cat "$_lastgood"))" fi _record echo echo "# $VERSION installed as $ESP/$KERNEL_NAME" echo "# previous kernel kept as $ESP/${KERNEL_NAME}_old" echo "# its source kept at $SRC/linux-$(cat "$_lastgood" 2>/dev/null)" echo "# owned by $acct (packagemanager verify | grep $PKG)" echo "# its config kept in $KCFG_DIR (lfs-kernel config list)" echo "# running kernel is $(uname -r) -- reboot to use $VERSION" [ "$_fw_ok" = 1 ] || echo "# NOTE: firmware was NOT installed (see the message above)"