#!/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 # Sevinj Aghayeva # # 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