#!/bin/sh -ef
#
# mbuto: Minimal Builder Using Terse Options
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Copyright (c) 2020-2022 Red Hat GmbH
# Authors: Stefano Brivio <sbrivio@redhat.com>
# Sevinj Aghayeva <sevinj.aghayeva@gmail.com>
#
# This script builds Linux initramfs images suitable for lightweight VM
# environments, without relying on distribution-specific tools (dracut,
# debootstrap, mkinitramfs, etc.) or containerised environments.
#
# Programs are sourced from the host, together with required dynamic libraries.
# Kernel modules, links, and initial device nodes are configured manually. A
# convenience support for distribution packages is supplied, with the sole
# function of extracting packages, ignoring dependencies, without an actual
# installation process.
#
# shellcheck disable=SC2016,SC2046,SC2048,SC2059,SC2086,SC2154
# ^ FIXUP template, word splitting helpers, printf wrappers,
# arguments to du -c, cmd_check assigning via eval
### Configuration ##############################################################
# Programs: see profile_base() below for an example.
# Libraries commonly loaded via dlopen(3) (strictly needed for basic tasks)
LIBS_DLOPEN="${LIBS_DLOPEN:-
libc.so.6 libnss_dns.so.2
libc.so.6 libnss_files.so.2
}"
# Links: see profile_base().
# Kernel modules: see profile_base().
# Device nodes, NAME,TYPE,MAJOR,MINOR supported, copied otherwise
NODES="${NODES:-console kmsg null ptmx random urandom zero}"
# Empty directories to create
DIRS="${DIRS:-/proc /sys}"
# Copies of full paths, attributes and parents directories preserved
COPIES="${COPIES:-}"
# Workers for time-consuming tasks such as stripping modules, see workers()
THREADS="$(nproc)"
# Fix-up script to run before /init, can be omitted
[ -z "${FIXUP}" ] && FIXUP='#!/bin/sh
export PATH=/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/sbin:/usr/sbin:
mount -t proc proc /proc
mount -t sysfs sys /sys
for m in __KMODS__; do
modprobe ${m}
done
mount -t devtmpfs dev /dev
mkdir /dev/pts
mount -t devpts pts /dev/pts
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=968199
ln -sf /proc/self/fd /dev/fd
ln -sf /dev/fd/0 /dev/stdin
ln -sf /dev/fd/1 /dev/stdout
ln -sf /dev/fd/2 /dev/stderr
'
# Start a shell, nothing else
profile_base() {
# Programs sourced with linked libraries, alternatives with commas
PROGS="${PROGS:-ash,dash,bash mount mkdir ln}"
# Links: installed target program, then link name
LINKS="${LINKS:-
ash,dash,bash /bin/sh
ash,dash,bash /init}"
# List of kernel modules
KMODS="${KMODS:-}"
FIXUP="${FIXUP}
sh +m
"
# Output (stdout) template keywords: __INITRD__ and __KERNEL__
OUTPUT="__INITRD__
"
}
# Profile spawning bash, for general usage
profile_bash() {
PROGS="${PROGS:-bash cat chmod lsmod modprobe find grep mkdir mknod mv
ls ip ln rm mount umount ps strace}"
LINKS="${LINKS:-
bash /bin/sh
bash /init}"
KMODS="${KMODS:-virtio_net virtio_pci}"
NODES="${NODES} tty ttyS0"
OUTPUT="__INITRD__
"
}
# Base profile for Kata Containers (https://katacontainers.io/)
profile_kata() {
PROGS="${PROGS:-kata-agent ash,dash,bash mount modprobe mkdir ln}"
KMODS="${KMODS:-vmw_vsock_virtio_transport virtio_net virtiofs vfio
virtio_pci}"
LINKS="${LINKS:-
ash,dash,bash /bin/sh
kata-agent /init}"
FIXUP="${FIXUP}"'
mkdir -p /sys/fs/cgroup
mount -t tmpfs cgroup /sys/fs/cgroup
mkdir /sys/fs/cgroup/unified
mount -o rw,nsdelegate -t cgroup2 cgroup /sys/fs/cgroup/unified
for t in cpu,cpuacct blkio memory perf_event pids cpuset \
freezer devices; do
mkdir /sys/fs/cgroup/${t}
mount -o rw,${t} -t cgroup cgroup /sys/fs/cgroup/${t}
done
'
OUTPUT="Kata Containers [hypervisor.qemu] configuration:
kernel = \"__KERNEL__\"
initrd = \"__INITRD__\"
"
}
# Debugging profile for Kata Containers: start shell before kata-agent
profile_kata_debug() {
PROFILE_LIST="${PROFILE_LIST} kata_debug"
profile_kata
PROGS="${PROGS} ash,dash,bash cat ip ls strace insmod nsenter"
LINKS="${LINKS}
ash,dash,bash /bin/sh"
FIXUP="${FIXUP}
echo Starting interactive shell, exit to spawn kata-agent
sh +m
"
}
# Profile for passt (https://passt.top) tests
profile_passt() {
PROGS="${PROGS:-ash,dash,bash ip mount ls insmod mkdir ln cat chmod
lsmod modprobe find grep mknod mv rm umount udhcpc jq iperf3
dhclient busybox logger sed tr chown sipcalc cut md5sum
nc dd strace ping tail killall sleep sysctl nproc
tcp_rr tcp_crr udp_rr which tee seq bc}"
KMODS="${KMODS:- virtio_net virtio_pci}"
LINKS="${LINKS:-
ash,dash,bash /init
ash,dash,bash /bin/sh
ash,dash,bash /usr/bin/bash}"
DIRS="${DIRS} /tmp"
COPIES="${COPIES}
/etc/udhcpc/default.script
/sbin/dhclient-script"
FIXUP="${FIXUP}
:> /etc/fstab
sh +m
"
OUTPUT="KERNEL=__KERNEL__
INITRD=__INITRD__
"
}
# Profile for kselftests (https://kselftest.wiki.kernel.org/)
profile_kselftests() {
PROGS="${PROGS:-addr2line awk basename bash bc bridge cat chmod chown cp
cpupower cut date dd diff dirname dmesg du env ethtool find gcc
grep head id ifconfig insmod ip ip6tables iperf3 iptables ipvsadm
jq killall ln logger ls lsmod make mausezahn md5sum mkdir mknod
mktemp modprobe mount mv nc nft nproc nstat pidof ping python3
realpath rm rmdir sed seq sipcalc sleep socat sort ss strace
sysctl tail taskset tc tee timeout tput tr traceroute traceroute6
true umount uname uniq uuidgen wc which}"
if [ ! -f "include/config/kernel.release" ]; then
err "This profile needs to run from a kernel tree, exiting"
exit 1
fi
KERNEL="$(${CAT} include/config/kernel.release)"
__testpath="./tools/testing/selftests/"
__makefile="${__testpath}Makefile"
__colls=
if [ -n "${SUBOPT_list+x}" ]; then
__colls=$("${AWK}" '/^TARGETS \+= / {print $3}' ${__makefile} | "${TR}" '\n' ' ')
printf "Available collections:\n "
echo "${__colls}" | "${FMT}" -t
exit 0
fi
for __c in ${SUBOPT_collection}; do
if ! ${GREP} -q "^TARGETS += ${__c}$" ${__makefile}; then
notice "WARNING: collection ${__c} doesn't exist"
continue
fi
__colls="${__c} ${__colls}"
done
for __t in ${SUBOPT_test}; do
__colls="${__colls} $(echo ${__t} | ${AWK} -F':' '{print $1}')"
done
if [ -z "${__colls}" ]; then
__colls="$(${SED} -n 's/^TARGETS += \(.*\)$/\1/p' \
"${__makefile}")"
fi
__colls="$(echo ${__colls} | ${TR} ' ' '\n' | ${SORT} -u)"
__pattern="$(list_join "${__colls}" '^' '$' '|')"
__skip_targets="$(${AWK} '/^TARGETS/ { print $3}' ${__makefile} | \
${EGREP} -v "${__pattern}" | ${TR} '\n' ' ')"
${RM} -rf "${__testpath}/kselftest_install"
${MAKE} SKIP_TARGETS="${__skip_targets}" -C ${__testpath} install \
>/dev/null 2>&1
MODDIR="$(${REALPATH} .mbuto_mods)"
${RM} -rf "${MODDIR}"
${MKDIR} -p "${MODDIR}"
INSTALL_MOD_PATH="${MODDIR}" ${MAKE} modules_install -j ${THREADS} \
>/dev/null
__files="$(list_join "${__colls}" "${__testpath}" '/config' ' ')"
__mods="$(${CAT} ${__files} 2>/dev/null | \
${AWK} -F'=' '/=m/ {print $1}' | ${SORT} -u)"
KMODS=
if [ ! -z "${__mods}" ]; then
__pattern="$(list_join "${__mods}" '^obj-\$\(' '\).*.o$' '|')"
__result="$(${BASENAME} -a -s .o \
$(${EGREP} -rn --include "*Makefile" ${__pattern} | \
${TR} -d '+' | ${AWK} -F'=' '/=/ {print $2}'))"
__find_pattern="$(list_join "${__result}" '-name ' '.ko ' '-o ')"
KMODS="$(${FIND} "${MODDIR}" ${__find_pattern} | ${TR} '\n' ' ')"
[ "${NOSTRIP}" != "y" ] && workers kmod_strip_worker
KMODS="$(${BASENAME} -a -s .ko ${KMODS})"
__kmods_needed="$(list_join "${__result}" '' '' ' ')"
__kmods_missing="$(list_diff "${__kmods_needed}" "${KMODS}")"
if [ ! -z "${__kmods_missing}" ]; then
printf "WARNING: the following modules are missing:\n "
echo "${__kmods_missing}" | "${FMT}" -t
fi
fi
LINKS="${LINKS:-
bash /init
bash /bin/sh
bash /bin/bash
bash /usr/bin/bash}"
NODES="${NODES} tty ttyS0"
DIRS="${DIRS} /tmp /run/netns /var/run"
COPIES="${COPIES} ${__testpath}kselftest_install/*,"
__run_args=
[ ! -z "${SUBOPT_collection}" ] && __run_args="-c ${SUBOPT_collection}"
[ ! -z "${SUBOPT_test}" ] && __run_args="${__run_args} -t ${SUBOPT_test}"
FIXUP="${FIXUP}"'
mkdir /dev/shm
mount -t tmpfs shm /dev/shm
set +m
:> /etc/fstab
echo 3 > /proc/sys/kernel/printk
echo "Press s for shell, any other key to run selftests"
read a
if [ "${a}" != "s" ]; then
if ./run_kselftest.sh '"${__run_args}"'; then
echo "All tests passed, shutting down guest..."
echo o > /proc/sysrq-trigger
fi
fi
'
for __f in $(${FIND} ${__testpath}kselftest_install/ -executable); do
case $("${FILE}" -bi "${__f}") in
"application/"*) libs_copy "${__f}" ;;
esac
done
OUTPUT="__INITRD__
"
}
################################################################################
### Helpers ####################################################################
# List of tools used here, assigned to uppercase variable names
TOOLS="awk basename bc cat cd chmod cp cpio depmod diff dirname du file find fmt
grep egrep gzip ldconfig ldd ln ls make mkdir mknod mktemp modprobe mv
printf readlink realpath rm rmdir sed seq sleep sort strip sync tr umount
uname wget which"
# err() - Print error and exit
# $@: Error message, optionally with printf format and arguments
err() {
( printf "Error: "; printf "$@"; echo ) 1>&2
exit 1
}
# warn() - Warn to standard error
# $@: Warning message, optionally with printf format and arguments
warn() {
( printf "Warning: "; printf "$@"; echo ) 1>&2
}
# notice() - Print notice-level messages
# $@: Notice, optionally with printf format and arguments, can be empty
notice() {
( { [ -z "${1}" ] && echo; } || { printf "$@"; echo; } ) 1>&2
}
# info() - Print informational messages for verbose operation
# $@: Message, optionally with printf format and arguments
info() {
{ [ "${VERBOSE}" = "y" ] && ( { printf "$@"; echo; } ) 1>&2; } || :
}
# cmd_check() - Check that a command exists, assign to uppercase variable
# $1: Command name, can be an alias
cmd_check() {
if ! eval $(echo "${1}" |
tr 'a-z-' 'A-Z_')="\$(command -v ${1})"; then
err "${1} not found"
fi
}
# cleanup() - Remove left-overs on exit, used from trap
cleanup() {
"${RM}" -rf "${wd}"
[ -n "${pkg_tmp}" ] && "${RM}" -f "${pkg_tmp}"
[ -n "${compress_test1}" ] && "${RM}" -f "${compress_test1}"
[ -n "${compress_test2}" ] && "${RM}" -f "${compress_test2}"
return 0
}
# dir_size() - Print summed size of directories with SI suffix
# $*: Paths, can be names of files and directories, mixed
dir_size() {
IFS='
'
for __l in $("${CD}" "${wd}"; "${DU}" -c --si "${@}" 2>/dev/null); do
__last_size="${__l}"
done
echo "${__last_size%[ ]*}"
unset IFS
}
# strip_all() - Strip all executables in working directory, $wd
strip_all() {
for __f in $("${FIND}" "${wd}"); do
case $("${FILE}" -bi "${__f}") in
"application/x-sharedlib;"*|"application/x-executable;"*)
"${STRIP}" -s "${__f}" -R .comment 2>/dev/null || :
;;
*)
;;
esac
done
}
# fixup_apply() - Apply fix-up script to /init, from $FIXUP or passed in options
fixup_apply() {
[ -n "${SCRIPT}" ] && [ "${SCRIPT}" = "-" ] && return
[ -z "${SCRIPT}" ] && [ -z "${FIXUP}" ] && return
__call="$("${READLINK}" "${wd}/init")"
rm "${wd}/init"
if [ -n "${SCRIPT}" ]; then
"${CP}" "${SCRIPT}"
else
KMODS="$(echo ${KMODS} | tr -d '\n')"
printf "%s" "${FIXUP}" | \
sed 's,__KMODS__,'"${KMODS}"',g' > "${wd}/init"
fi
echo "${__call}" >> "${wd}/init"
"${CHMOD}" 755 "${wd}/init"
}
# workers() - Run $THREADS instances of $1 in subshells, wait for completion
# $1: Function to call
workers() {
__sync_dir="$(${MKTEMP} -d)"
for __t in $(${SEQ} 1 ${THREADS}); do
${MKDIR} "${__sync_dir}/${__t}"
( ${1} ${__t}; ${RMDIR} "${__sync_dir}/${__t}"; ) &
done
while ! ${RMDIR} "${__sync_dir}" 2>/dev/null; do ${SLEEP} 1; done
}
# list_diff() - Given two lists, $1 and $2, print tokens from $1 not in $2
# $1: List with elements to be checked
# $2: List to check against
list_diff() {
__diff=
for __e1 in ${1}; do
__found=0
for __e2 in ${2}; do
[ ${__e1} = ${__e2} ] && __found=1
done
[ ${__found} -eq 0 ] && __diff="${__e1} ${__diff}"
done
printf '%s' "${__diff}"
}
# list_join() - Add prefix and suffix, with separator, to all tokens in list
# $1: List of tokens
# $2: Prefix
# $3: Suffix
# $4: Separator, used after prefix and before suffix
list_join() {
__s=
for __t in ${1}; do
__s="${2}${__t}${3}${4}${__s}"
done
__s=${__s%${4}}
printf '%s' "${__s}"
}
################################################################################
### Suboption Parsing #########################################################
SUBOPTS='
kselftests C collection 2 Select a collection of kernel tests
kselftests T test 2 Select a test from a collection
kselftests L list 1 List available collections
'
subopts_profile=
# subopts_usage() - Print suboption usage for a single, passed SUBOPTS line
# $1: Profile name from SUBOPTS line
# $2: Short option name from SUBOPTS line
# $3: Unused
# $4: Option description from SUBOPTS line
subopts_usage_one() {
__profile="${1}"
__short="${2}"
__help="${5}"
if [ "${usage_subopts_profile}" != ${__profile} ]; then
usage_subopts_profile="${__profile}"
printf "\tSub-options for profile ${__profile}:\n"
fi
printf "\t\t-%s: %s\n" ${__short} "${__help}"
}
# subopts_usage() - Print suboption usage for all SUBOPTS
subopts_usage() {
IFS='
'
for __line in ${SUBOPTS}; do
IFS=' '
subopts_usage_one ${__line}
IFS='
'
done
unset IFS
}
# subopts_get_one() - Sub-option value from arguments, check one SUBOPTS line
# $1: Short option name as passed on command line
# $2: Value passed on command line
# $3: Profile name from SUBOPTS
# $4: Short option name from SUBOPTS
# $5: Long option name from SUBOPTS, used in SUBOPT_name assignment
subopts_get_one() {
__passed="${1}"
__value="${2}"
__profile="${3}"
__short="${4}"
__name="${5}"
[ "${__profile}" != "${PROFILE}" ] && return 1
[ "${__passed}" != "${__short}" ] && return 1
eval $(echo SUBOPT_${__name}=\"${__value}\")
}
# subopts_get() - Parse sub-options, validating against SUBOPTS
# $1: Short option name as passed on command line
# $2: Value passed on command line
# Return: argument count including option switch, doesn't return on failure
subopts_get() {
IFS='
'
for __line in ${SUBOPTS}; do
IFS=' '
if subopts_get_one "${1}" "${2}" ${__line}; then
unset IFS
return $(echo "${__line}" | "${AWK}" '{print $4}')
fi
IFS='
'
done
usage
}
### CPIO #######################################################################
# compress_select() - Try compressors and pick the fastest to decompress image
# $1: Existing CPIO archive
compress_select() {
if [ ! -f "/boot/config-${KERNEL}" ]; then
echo "gzip"
return
fi
compress_test1="$("${MKTEMP}")"
compress_test2="$("${MKTEMP}")"
__min_time=
for __a in gzip lz4 lzo lzma xz; do
[ "${__a}" = "lzo" ] && __cmd="lzop" || __cmd="${__a}"
__kcfg="CONFIG_RD_$(echo "${__a}" | tr '[:lower:]' '[:upper:]')"
if ! command -v "${__cmd}" > /dev/null 2>&1 ||
! "${GREP}" -q "${__kcfg}" "/boot/config-${KERNEL}"; then
continue
fi
"${__cmd}" -q -9 -c "${1}" > "${compress_test1}"
__start=$("${CAT}" /proc/uptime)
__start="${__start% *}"
for _ in $("${SEQ}" 1 5); do
"${CP}" "${compress_test1}" "${compress_test2}"
${__cmd} --force -d -c "${compress_test2}" > /dev/null
done
__end=$("${CAT}" /proc/uptime)
__end="${__end% *}"
__time="$(echo "${__end} - ${__start}" | "${BC}" -l)"
__size="$("${LS}" -s --block-size=1 "${compress_test1}")"
__size="${__size%% *}"
if [ -z "${__min_time}" ]; then
__min_time="${__time}"
__pick="${__a}"
else
__cmp="$(echo "${__time} < ${__min_time}" | "${BC}" -l)"
if [ "${__cmp}" = "1" ]; then
__min_time="${__time}"
__pick="${__a}"
fi
fi
notice "%5s %15s, 5 cycles in %7s" \
"${__a}:" "${__size} bytes" "${__time}s"
done
notice "Picked ${__pick} compression for CPIO"
rm ${compress_test1} ${compress_test2}
echo "${__pick}"
}
# cpio_compress() - Compress archive, test available methods if none is selected
# $1: Existing CPIO archive
cpio_compress() {
{ [ -z "${COMPRESS}" ] || [ "${COMPRESS}" = "none" ]; } && return
[ "${COMPRESS}" = "auto" ] && COMPRESS=$(compress_select "$1")
info "Compressing CPIO archive ${1}"
[ "${COMPRESS}" = "lzo" ] && __cmd="lzop" || __cmd="${COMPRESS}"
cmd_check "${__cmd}"
if [ "${__cmd}" = "lz4" ]; then
"${__cmd}" -l -f -q -9 "${1}" "${1}.lz4"
else
"${__cmd}" -f -q -9 -S .lz4 "${1}"
fi
mv "${1}.lz4" "${1}"
}
################################################################################
### Shared libraries ###########################################################
# libs_path_add() - Add library path to dynamic linker configuration
# $1: Path to be added, skipped if already present
libs_path_add() {
for __l in $("${CAT}" "${wd}/etc/ld.so.conf" 2>/dev/null); do
[ "${__l}" = "${1}" ] && return
done
"${MKDIR}" -p "${wd}/etc"
echo "${1}" >> "${wd}/etc/ld.so.conf"
"${LDCONFIG}" -r "${wd}"
}
# libs_copy_ld_so() - Copy run-time linker program, mimic location from host
# $1: Path to run-time linker
libs_copy_ld_so() {
[ -f "${wd}/${1}" ] && return
__destdir="$("${DIRNAME}" "${wd}/${1}")"
"${MKDIR}" -p "${__destdir}"
"${CP}" --parents --preserve=all "${1}" "${wd}"
}
# libs_dlopen_copy() - Recursively copy matching libraries from LIBS_DLOPEN
# $1: Library (base path) that loads further libraries using dlopen(3)
libs_dlopen_copy() {
__match=
for __t in ${LIBS_DLOPEN}; do
[ -z "${__match}" ] && __match="${__t}" && continue
if [ "${__match}" = "$(basename ${1})" ]; then
__path="$(dirname "${1}")/${__t}"
"${CP}" --parents --preserve=all "${__path}" "${wd}"
libs_copy "${__path}"
__match=
fi
done
}
# __libs_copy() - Recursively copy shared dependencies for programs, libraries
# $1: Host path to program or library
__libs_copy() {
info "Sourcing shared object dependencies for ${1}"
# ldd might succeed and print errors on stdout when paths are not found.
# Skip those lines, as those paths are not actually needed on the host.
for __l in $("${LDD}" "${1}" 2>/dev/null | "${GREP}" -v "ERROR:"); do
__path="${__l##*=> }"
[ "${__path}" = "not found" ] && continue
[ "${__l}" = "${__path}" ] && continue
__path="${__path%% *}"
# fakeroot dependency from LD_PRELOAD will always appear, skip
[ "$("${BASENAME}" "${__path}")" = "libfakeroot-sysv.so" ] && \
continue
__destpath="${wd}${__path}"
[ -f "${__destpath}" ] && continue
__destdir="$("${DIRNAME}" "${__destpath}")"
"${MKDIR}" -p "${__destdir}"
"${CP}" --parents --preserve=all "${__path}" "${wd}"
libs_path_add "${__destdir##${wd}}"
# Recurse over all shared object dependencies
libs_copy "${__path}"
done
libs_dlopen_copy "${1}"
# Dynamic linker is listed as single path, make sure we copy it once
__ld_so=
for __l in $("${LDD}" "${1}" 2>/dev/null); do
case ${__l} in "/"*" "*) __ld_so="${__l% *}" ;; *) ;; esac
done
if [ -n "${__ld_so}" ]; then
libs_copy_ld_so "${__ld_so}"
libs_path_add "${__ld_so##${wd}}"
fi
}
# libs_copy() - Call __libs_copy with tabs and newlines as IFS
# $@: Arguments to __libs_copy
libs_copy() {
IFS='
'
__libs_copy "${@}"
unset IFS
}
################################################################################
### Programs ###################################################################
# prog_link() - Link from $LINKS to sourced program from $PROGS
# $1: Program name from $PROGS
# $2: Target on working directory $wd
# $3: Even-numbered token from $LINKS, skip if not matching
# $4: Link name, next token from $LINKS
prog_link() {
[ "${1}" != "${3}" ] && return
"${MKDIR}" -p "$("${DIRNAME}" "${wd}${4}")"
"${LN}" -s "${2}" "${wd}${4}" 2>/dev/null || :
}
# prog_add() - Add a program from $PROGS
# $1: Program name seen from shell
prog_add() {
info "Adding program ${1}"
IFS=','
for __a in ${1}; do
# We might have alias definitions too, look for a path
IFS='
'
__bin=
for __path in $(${WHICH} -a "${__a}" 2>/dev/null); do
[ -x "${__path}" ] && __bin="${__path}" && break
done
[ -n "${__bin}" ] && break
IFS=','
done
unset IFS
[ -z "${__bin}" ] && err "Can't source ${1}"
# Binaries in /usr/libexec are meant to run on other hosts only, so they
# can't reside in /usr/libexec on the target image. Move to /usr/bin.
if [ "$("${DIRNAME}" "${__bin}")" = "/usr/libexec" ]; then
__bindir="${wd}/usr/bin"
else
__bindir="${wd}$("${DIRNAME}" "${__bin}")"
fi
"${MKDIR}" -p "${__bindir}"
"${CP}" --preserve=all "${__bin}" "${__bindir}"
__target=
for __l in ${LINKS}; do
[ -z "${__target}" ] && __target="${__l}" && continue
prog_link "${1}" \
"${__bindir##${wd}}/$("${BASENAME}" "${__bin}")" \
"${__target}" "${__l}"
__target=
done
libs_copy "${__bin}"
}
################################################################################
### Kernel modules #############################################################
# kmod_init() - Set $KERNEL, create modules directory on working image $wd
kmod_init() {
KERNEL="${KERNEL:-$("${UNAME}" -r)}"
"${MKDIR}" -p "${wd}/lib/modules/${KERNEL}/kernel"
}
# __kmod_node() - Actually create device node for path found in modules.devname
# $1: Module name, not used here, indicates grep matched in caller
# $2: Name of device node
# $3: Type and device major as a single token, from modules.devname
# $4: Device minor
__kmod_node() {
{ [ -z "${1}" ] || [ -e "${wd}/dev/${2}" ]; } && return
"${MKDIR}" -p "$("${DIRNAME}" "${wd}/dev/${2}")"
"${MKNOD}" "${wd}/dev/${2}" "${3%%[0-9]*}" "${3#[a-z]*}" "${4}"
}
# kmod_node() - Add device nodes for on-demand module loading
# $1: Module name
kmod_node() {
__devname="${MODDIR}/lib/modules/${KERNEL}/modules.devname"
IFS=' :'
__kmod_node $("${GREP}" "^${1} " "${__devname}")
unset IFS
}
# kmod_add() - Add a kernel module to working directory
# $1: Module name
kmod_add() {
info "Adding kernel module ${1}"
if [ -z "${MODDIR}" ]; then
__d_opt="/"
__find_path="/lib/modules/${KERNEL}"
else
__d_opt="${MODDIR}"
__find_path="${MODDIR}/lib/modules"
fi
__f=$("${MODPROBE}" -S "${KERNEL}" -d "${__d_opt}" -q \
--show-depends -- "$(${BASENAME} -- ${1})")
case ${__f} in
"builtin "*)
kmod_node "${1}"
;;
*)
for __t in ${__f}; do
__t="$(${BASENAME} -- "${__t}")"
__t="${__t%%.*}"
if ${MODPROBE} -S "${KERNEL}" -d "${__d_opt}" -q \
--show-depends -- "$(${BASENAME} -- "${__t}")" >/dev/null; then
__src="$(${FIND} ${__find_path} -name "${__t}.*")"
[ -z "${__src}" ] && continue
__dst="${wd}${__src##${MODDIR}}"
if ! "${DIFF}" "${__src}" "${__dst}" 2>/dev/null; then
"${MKDIR}" -p "$("${DIRNAME}" "${__dst}")"
"${CP}" -a "${__src}" "${__dst}"
fi
kmod_node "${__t}"
fi
done
esac
}
# kmod_post() - Copy files for modprobe on target, run depmod
kmod_post() {
"${CP}" -a "${MODDIR}/lib/modules/${KERNEL}/modules.order" \
"${MODDIR}/lib/modules/${KERNEL}/modules.builtin" \
"${wd}/lib/modules/${KERNEL}"
"${DEPMOD}" -b "${wd}" "${KERNEL}"
}
# kmod_strip_worker() - Strip debug information from modules, call via workers()
# $1: Worker thread number, used to select paths from $KMODS
kmod_strip_worker() {
__i=1
for __kmod in ${KMODS}; do
[ ${__i} -eq ${1} ] && ${STRIP} --strip-debug ${__kmod}
__i=$((__i + 1))
[ ${__i} -eq ${THREADS} ] && __i=1
done
return 0
}
################################################################################
### Device nodes ###############################################################
# node_add() - Add device node from $NODES
node_add() {
info "Adding device node ${1}"
"${MKDIR}" -p "$("${DIRNAME}" "${wd}/dev/${1}")"
if [ -n "${2}" ]; then
"${MKNOD}" "${wd}/dev/${1}" "${2}" "${3}" "${4}"
else
"${CP}" -a "/dev/${1}" "${wd}/dev/"
fi
}
################################################################################
### Packages ###################################################################
# pkg_deb_add() - Extract Debian package, source shared object dependencies
# $1: Package file
pkg_deb_add() {
cmd_check dpkg-deb
info "Adding Debian package ${1}"
for __f in $("${DPKG_DEB}" -X "${1}" "${wd}"); do
__type="$("${FILE}" -bi "${wd}/${__f}")"
if [ "${__type%%;*}" = "application/x-sharedlib" ] ||
[ "${__type%%;*}" = "application/x-executable" ]; then
libs_copy "${wd}/${__f}"
fi
done
}
# pkg_deb_add() - Extract RPM package, source shared object dependencies
# $1: Package file
pkg_rpm_add() {
cmd_check rpm2cpio
info "Adding RPM package ${1}"
for __f in $("${RPM2CPIO}" "${1}" |
cpio -D "${wd}" --quiet -idvmu 2>&1 |
"${GREP}" -v "cpio:"); do
__type="$("${FILE}" -bi "${wd}/${__f}")"
if [ "${__type%%;*}" = "application/x-sharedlib" ] ||
[ "${__type%%;*}" = "application/x-executable" ]; then
libs_copy "${wd}/${__f}"
fi
done
}
# pkg_add() - Source and extract a package into working directory
# $1: Package file or URL
pkg_add() {
info "Adding package ${1}"
pkg_tmp="$("${MKTEMP}")"
if [ ! -f "${1}" ]; then
info "Downloading ${1}"
"${WGET}" --quiet -O "${pkg_tmp}" "${1}" || \
err "${1} not found"
__file="${pkg_tmp}"
else
__file="${1}"
fi
case $("${FILE}" -bi "${__file}") in
"application/vnd.debian.binary-package;"*)
pkg_deb_add "${__file}"
;;
"application/x-rpm;"*)
pkg_rpm_add "${__file}"
;;
*)
err "package format not supported"
;;
esac
rm "${pkg_tmp}"
}
################################################################################
### Commands and user interface ################################################
# build() - Build a new image, sourcing contents
build() {
kmod_init
for __d in ${DIRS}; do
"${MKDIR}" -p "${wd}${__d}"
done
for __p in ${PROGS}; do
PATH="${PATH}:/usr/libexec:/sbin:/usr/sbin" prog_add "${__p}"
done
for __k in ${KMODS}; do
kmod_add "${__k}"
done
kmod_post
for __n in ${NODES}; do
IFS=','
node_add ${__n}
unset IFS
done
for __c in ${COPIES}; do
set +f
case ${__c} in
*","*) "${CP}" -a ${__c%,*} "${wd}${__c#*,}" ;;
*) "${CP}" --parent -a "${__c}" "${wd}" ;;
esac
set -f
done
for __p in ${PKGS}; do
pkg_add "${__p}"
done
fixup_apply
}
# add() - Add contents to existing image
# $1: Item to be added
add() {
kmod_init
[ -e "${1}" ] && __path="${1}" || __path=$(command -v "${1}") || :
if [ ! -e "${__path}" ]; then
{ pkg_add "${1}" && return; } || err "${1} not found"
fi
case $("${FILE}" -bi "${__path}") in
"application/x-sharedlib;"* | "application/x-pie-executable;"*)
PATH="${PATH}:/usr/libexec:/sbin:/usr/sbin" prog_add "${__path}"
;;
"application/x-object;"*)
kmod_add "${__path}"
kmod_post
;;
"application/vnd.debian.binary-package;"*)
pkg_deb_add "${__path}"
;;
"application/x-rpm;"*)
pkg_rpm_add "${__path}"
;;
*)
"${CP}" --parents -a "$(${REALPATH} "${__path}")" "${wd}"
;;
esac
set +x
}
# stats() - Print size information about created image
stats() {
__bin="$(dir_size bin usr/bin sbin usr/sbin usr/local/bin \
usr/local/sbin)"
__lib="$(dir_size --exclude='lib/modules/*' lib lib64 usr/lib \
usr/lib32 usr/lib64 usr/local/lib)"
__mod="$(dir_size lib/modules)"
__sum="$(dir_size .)"
__compressed="$("${DU}" --si "${OUT}")"
__compressed="${__compressed%[ ]*}"
notice "Size: bin %5s lib %5s kmod %5s total %5s compressed %5s" \
"${__bin}" "${__lib}" "${__mod}" "${__sum}" "${__compressed}"
}
# cmds() - Loop over non-option arguments if any, just build image otherwise
# $@: Command arguments
cmds() {
{ [ ! -f "${OUT}" ] || [ -z "${1}" ]; } && build
for arg do
add "${arg}"
done
[ "${NOSTRIP}" != "y" ] && strip_all
(
"${CD}" "${wd}"
"${FIND}" . | "${CPIO}" --create -H newc --quiet
) > "${OUT}"
cpio_compress "${OUT}"
stats
printf "%s" "${OUTPUT}" | \
sed 's,__INITRD__,'"$("${REALPATH}" "${OUT}")"',g' | \
sed 's,__KERNEL__,/boot/vmlinuz-'"${KERNEL}"',g'
}
# usage() - Print usage and exit
usage() {
echo "${0} [OPTIONS] [ADD_ON]..."
echo
echo "Options:"
echo " -c gzip|lz4|lzma|lzo|auto|none"
echo " compression method for CPIO file. Default: none"
echo " -d"
echo " don't strip binary objects"
echo " -f PATH"
echo " path for initramfs. Default: temporary file"
echo " -k VERSION"
echo " kernel version. Default: $(uname -r)"
echo " -m PATH"
echo " relative root for /lib/modules. Default: /"
echo " -p PROFILE|FILE"
echo " select profile for add-ons, built-in PROFILEs are:"
echo " base bash kata kata_debug passt kselftests"
echo
echo " alternatively, an executable shell script FILE"
echo " including assignments of configuration variables can be"
echo " used. To avoid collisions with built-in profiles, FILE"
echo " needs to be as a command, e.g. './passt', not 'passt'"
echo
echo " Default: base"
echo
echo " kselftests sub-options (same as run_kselftests.sh):"
echo " -L"
echo " list available collections"
echo " -C collection"
echo " select a collection of tests to run"
echo " -T collection:test"
echo " select a specific test to run"
echo " Examples:"
echo " -p kselftests -C net"
echo " run all the tests in net collection"
echo " -p kselftests -T net:ip_defrag.sh"
echo " run ip_defrag.sh from net collection"
echo " -s SCRIPT|-"
echo " fix-up script to run before init, '-' for none"
echo " -v: verbose"
echo " -h: show this help"
echo
echo "Build initramfs image. Additional programs, kernel modules,"
echo "device nodes, generic files can be passed as a list of ADD_ON"
echo "parameters."
echo
echo "Distribution packages (deb and RPM currently supported) can be"
echo "added too, but they will simply be extracted, not installed."
echo "Package dependencies are also not sourced."
echo
echo "Environmental variables can be used to replace:"
echo " PROGS"
echo " base programs"
echo " LIBS_DLOPEN"
echo " additional libraries, not directly linked from"
echo " programs, typically loaded via dlopen(3) by matching"
echo " libraries, in the form"
echo " MATCHING_LIBRARY ADDITIONAL_LIBRARY"
echo " KMODS"
echo " base kernel modules"
echo " NODES"
echo " device nodes, copied from host if just name is given, "
echo " created if NAME,TYPE,MAJOR,MINOR"
echo " LINKS"
echo " link to programs by name, pairs of"
echo " PROGRAM PATH"
echo " DIRS"
echo " list of initial set of empty directories"
echo " COPIES"
echo " full copy of path, preserving attributes and parents"
echo
echo "Examples:"
echo " ${0}"
echo " Build a base image as a temporary file"
echo " ${0} grep"
echo " Build a new image including grep and needed libraries"
echo " ${0} -v -f kata.img -p kata_debug -c lz4"
echo " Use lz4 compression, run a shell before proceeding"
exit 1
}
################################################################################
# If we're not running as root, re-run with fakeroot
if [ "$(id -u)" -eq 0 ] && \
[ "$(cat /proc/self/uid_map)" = " 0 0 4294967295" ]; then
__as_root=1
else
__as_root=0
fi
if [ "${LD_PRELOAD}" != "libfakeroot-sysv.so" ] && [ ${__as_root} -eq 0 ]; then
if ! FAKEROOT="$(command -v fakeroot)"; then
__fakeroot_missing="y"
else
PATH="${PATH}:/usr/local/sbin:/sbin:/usr/sbin" \
"${FAKEROOT}" "${0}" "${@}"
exit ${?}
fi
fi
# Check needed tools, exit if any is missing
for __l in ${TOOLS}; do
cmd_check "${__l}"
done
# Parse options
while getopts :c:df:k:m:p:s:vh __opt; do
case ${__opt} in
c) COMPRESS="${OPTARG}" ;;
d) NOSTRIP="y" ;;
f) OUT="${OPTARG}" ;;
k) KERNEL="${OPTARG}" ;;
m) MODDIR="${OPTARG}" ;;
p) PROFILE="${OPTARG}" ;;
s) SCRIPT="${OPTARG}" ;;
v) VERBOSE="y" ;;
?)
eval arg=\${$((OPTIND))}
subopts_get "${OPTARG}" "${arg}" || __subopt_args=$?
OPTIND=$((OPTIND + ${__subopt_args} - 1))
;;
h|*) usage ;;
esac
done
shift $((OPTIND - 1))
[ -z "${PROFILE}" ] && PROFILE="base"
if [ "${__fakeroot_missing}" = "y" ]; then
err "Not root and no fakeroot available, exiting"
exit 1
fi
# Create working directory
wd="$("${MKTEMP}" -d)"
# Apply profile requested from file or matching profile_*() function
if [ -x "${PROFILE}" ] && __profile_file="$(command -v ${PROFILE})" && \
[ "${PROFILE}" = "${__profile_file}" ]; then
notice "Applying profile from file ${PROFILE}"
. "${PROFILE}"
else
notice "Applying built-in profile ${PROFILE}"
eval "profile_${PROFILE}" || err "profile ${PROFILE} not found"
fi
trap cleanup EXIT
cmds "$@"
exit 0