#!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-only # # mkinitcpio - modular tool for building an initramfs images # declare -r version='35.2.503.g6ce2239' shopt -s extglob ### globals within mkinitcpio, but not intended to be used by hooks # needed files/directories _f_functions='/usr/local/lib/initcpio/functions' _f_config='/etc/mkinitcpio.conf' _d_config='/etc/mkinitcpio.conf.d' _d_hooks='/etc/initcpio/hooks:/usr/local/lib/initcpio/hooks' _d_install='/etc/initcpio/install:/usr/local/lib/initcpio/install' _d_post='/etc/initcpio/post:/usr/local/lib/initcpio/post' _d_flag_hooks= _d_flag_install= _d_flag_post= _d_firmware=({/usr,}/lib/firmware/updates {/usr,}/lib/firmware) _d_fwpath=() # the actual firmware lookup path, a subset of _d_firmware listed above _d_presets='/etc/mkinitcpio.d' if (( MESON_TEST )); then _f_functions="$PWD/functions" _f_config="$PWD/mkinitcpio.conf" _d_config="$PWD/mkinitcpio.conf.d" _d_hooks="$PWD/hooks" _d_install="$PWD/install" _d_post="$PWD/post" _d_presets="$PWD/mkinitcpio.d" fi # options and runtime data _optmoduleroot='' _optgenimg='' _optcompress='' _opttargetdir='' _optosrelease='' _optukiconfig='' _optuki='' _optcmdline='' _optsplash='' _optkernelimage='' _optuefistub='' _optshowautomods=0 _optsavetree=0 _optshowmods=0 _optremove=0 _optnocmdline=0 _optnoukify=0 _optnopost=0 _optquiet=1 _optcolor=1 _optconfd=1 _optskiphooks=() _optaddhooks=() _hooks=() _optpreset=() _tmpfiles=() _generated=() declare -A _runhooks _addedmodules _modpaths _autodetect_cache # export a sane PATH export PATH="/usr/local/bin:/bin" # Sanitize environment further # GREP_OPTIONS="--color=always" will break everything # CDPATH can affect cd and pushd # LIBMOUNT_* options can affect findmnt and other tools unset GREP_OPTIONS CDPATH "${!LIBMOUNT_@}" usage() { cat < Add specified hooks, comma separated, to image -c, --config Use alternate config file. (default: /etc/mkinitcpio.conf) -g, --generate Generate cpio image and write to specified path -H, --hookhelp Display help for given hook and exit -h, --help Display this message and exit -k, --kernel Use specified kernel version (default: $(uname -r)) -L, --listhooks List all available hooks -M, --automods Display modules found via autodetection -n, --nocolor Disable colorized output messages --nopost Disable post hooks -p, --preset Build specified preset from /etc/mkinitcpio.d -P, --allpresets Process all preset files in /etc/mkinitcpio.d -R, --remove Remove specified preset images This option can only be used with either '-p|--presets' or '-P|--allpresets' -r, --moduleroot Root directory for modules (default: /) -S, --skiphooks Skip specified hooks, comma-separated, during build -s, --save Save build directory. (default: no) -d, --generatedir Write generated image into -t, --builddir Use DIR as the temporary build directory -D, --hookdir Specify where to look for hooks -U, --uki Build a unified kernel image -V, --version Display version information and exit -v, --verbose Verbose output (default: no) -z, --compress Use an alternate compressor on the image (cat, xz, lz4, zstd) Options for unified kernel image (-U, --uki): --cmdline Set kernel command line from file (default: /etc/kernel/cmdline or /proc/cmdline) --no-cmdline Do not embed a kernel command line --osrelease Include os-release (default: /etc/os-release) --splash Include bitmap splash --kernelimage Kernel image --uefistub Location of UEFI stub loader --ukiconfig Location of configuration file for UKIs, used for signing, etc. EOF } version() { cat <"$_d_workdir/autodetect_modules" msg "build directory saved in '%s'" "$_d_workdir" else rm -rf -- "$_d_workdir" fi fi exit "$err" } resolve_kernver() { local kernel="$1" arch='' if [[ -z "$kernel" ]]; then uname -r return 0 fi if [[ "${kernel:0:1}" != / ]]; then echo "$kernel" return 0 fi if [[ ! -e "$kernel" ]]; then error "specified kernel image does not exist: '%s'" "$kernel" return 1 fi kver "$kernel" && return error "invalid kernel specified: '%s'" "$1" arch="$(uname -m)" if [[ "$arch" != @(i?86|x86_64) ]]; then error "kernel version extraction from image not supported for '%s' architecture" "$arch" error "there's a chance the generic version extractor may work with a valid uncompressed kernel image" fi return 1 } hook_help() { local resolved script script="$(PATH="$_d_install" type -p "$1")" # this will be true for broken symlinks as well if [[ -z "$script" ]]; then error "Hook '%s' not found" "$1" return 1 fi if resolved="$(readlink "$script")" && [[ "${script##*/}" != "${resolved##*/}" ]]; then msg "This hook is deprecated. See the '%s' hook" "${resolved##*/}" return 0 fi # shellcheck disable=SC1090 . "$script" if ! declare -f help >/dev/null; then error "No help for hook $1" return 1 fi msg "Help for hook '$1':" help list_hookpoints "$1" } hook_list() { local p hook resolved local -a paths hooklist depr local ss_ordinals=(¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹) IFS=: read -ra paths <<<"$_d_install" for path in "${paths[@]}"; do for hook in "$path"/*; do [[ -e "$hook" || -L "$hook" ]] || continue # handle deprecated hooks and point to replacement if resolved="$(readlink "$hook")" && [[ "${hook##*/}" != "${resolved##*/}" ]]; then resolved="${resolved##*/}" if ! index_of "$resolved" "${depr[@]}"; then # deprecated hook depr+=("$resolved") _idx=$(( ${#depr[*]} - 1 )) fi hook+=${ss_ordinals[_idx]} fi hooklist+=("${hook##*/}") done done msg "Available hooks" printf '%s\n' "${hooklist[@]}" | LC_ALL=C.UTF-8 sort -u | column -c"$(tput cols)" if (( ${#depr[*]} )); then echo for p in "${!depr[@]}"; do printf $'\'%s\' This hook is deprecated in favor of \'%s\'\n' \ "${ss_ordinals[p]}" "${depr[p]}" done fi } compute_d_fwpath() { local fw_dir for fw_dir in "${_d_firmware[@]}"; do fw_dir="$(realpath -qe -- "$fw_dir")" if [[ -d "$fw_dir" ]]; then # only add if not already in array ! in_array "$fw_dir" "${_d_fwpath[@]}" && _d_fwpath+=("$fw_dir") fi done } compute_hookset() { local h for h in "${HOOKS[@]}" "${_optaddhooks[@]}"; do in_array "$h" "${_optskiphooks[@]}" && continue _hooks+=("$h") done } create_image() { # create_image local root="$1" out="$2" compress="$3" compress_cmd="$3" pipestatus pipeprogs if [[ "$compress" == 'early' ]]; then # early image should never be compressed compress_cmd='cat' unset COMPRESSION_OPTIONS fi pushd "$root" >/dev/null || return # Reproducibility: set all timestamps to 0 LC_ALL=C.UTF-8 find . -mindepth 1 -execdir touch -hcd '@0' '{}' + # If this pipeline changes, |pipeprogs| below needs to be updated as well. LC_ALL=C.UTF-8 find . -mindepth 1 -printf '%P\0' \ | LC_ALL=C.UTF-8 sort -z \ | LC_ALL=C.UTF-8 bsdtar --uid 0 --gid 0 --null -cnf - -T - \ | LC_ALL=C.UTF-8 bsdtar --null -cf - --format=newc @- \ | "$compress_cmd" "${COMPRESSION_OPTIONS[@]}" >>"$out" pipestatus=("${PIPESTATUS[@]}") pipeprogs=('find' 'sort' 'bsdtar (step 1)' 'bsdtar (step 2)' "$compress_cmd") popd >/dev/null || return for (( i = 0; i < ${#pipestatus[*]}; ++i )); do if (( pipestatus[i] )); then printf '%s' "${pipeprogs[i]}" return 1 fi done } # Decompress firmware files decompress_firmware() { local buildroot_fwpath=() fw_files=() fw_file_symlinks=() file linkobject symlink_message buildroot_fwpath=("${_d_fwpath[@]/#/$BUILDROOT}") mapfile -d '' -t fw_files < <(LC_ALL=C.UTF-8 find "${buildroot_fwpath[@]:?}" -type f -name '*.zst' -print0 2>/dev/null | LC_ALL=C.UTF-8 sort -uz) if (( ${#fw_files[@]} )); then msg 'Decompressing zstd-compressed firmware files' for file in "${fw_files[@]}"; do quiet "Decompressing '%s'" "${file#"$BUILDROOT"}" zstd -qd --rm -- "$file" done mapfile -d '' -O "${#fw_file_symlinks[@]}" -t fw_file_symlinks \ < <(LC_ALL=C.UTF-8 find "${buildroot_fwpath[@]:?}" -type l -name '*.zst' -print0 2>/dev/null | LC_ALL=C.UTF-8 sort -uz) fi mapfile -d '' -t fw_files < <(LC_ALL=C.UTF-8 find "${buildroot_fwpath[@]:?}" -type f -name '*.xz' -print0 2>/dev/null | LC_ALL=C.UTF-8 sort -uz) if (( ${#fw_files[@]} )); then msg 'Decompressing xz-compressed firmware files' quiet "Decompressing '%s'" "${fw_files[@]#"$BUILDROOT"}" xz -q -d -- "${fw_files[@]}" mapfile -d '' -O "${#fw_file_symlinks[@]}" -t fw_file_symlinks \ < <(LC_ALL=C.UTF-8 find "${buildroot_fwpath[@]:?}" -type l -name '*.xz' -print0 2>/dev/null | LC_ALL=C.UTF-8 sort -uz) fi # Fix symlinks to point to the uncompressed files if (( ${#fw_file_symlinks[@]} )); then msg2 'Fixing firmware file symlinks' for file in "${fw_file_symlinks[@]}"; do linkobject="$(find "$file" -prune -printf '%l')" symlink_message="Rewriting '${file#"${BUILDROOT}"} -> ${linkobject#"${BUILDROOT}"}' to" # Remove the now invalid symlink rm -- "${file:?}" file="${file%.zst}" file="${file%.xz}" linkobject="${linkobject%.zst}" linkobject="${linkobject%.xz}" symlink_message="${symlink_message} '${file#"${BUILDROOT}"} -> ${linkobject#"${BUILDROOT}"}'" quiet "$symlink_message" # Create a new valid symlink ln -sfTn -- "${linkobject:?}" "${file:?}" done fi } build_image() { local out="$1" compressout="$1" compress="$2" errprog if [[ "$MODULES_DECOMPRESS" == 'yes' ]] && (( ${#_d_fwpath[@]} )); then decompress_firmware fi case "$compress" in cat) if (( initcpio_is_temporary && _optquiet )); then msg 'Creating uncompressed initcpio image' else msg "Creating uncompressed initcpio image: '%s'" "$out" fi unset COMPRESSION_OPTIONS ;; *) if (( initcpio_is_temporary && _optquiet )); then msg "Creating %s-compressed initcpio image" "$compress" else msg "Creating %s-compressed initcpio image: '%s'" "$compress" "$out" fi # move all compressed files to the early cpio to avoid double compression local compressed_files=() mapfile -td '' compressed_files < <(find "$BUILDROOT" -type f \ -regextype posix-extended -regex '.*\.(bz2|gz|lz4|lzma|lzo|xz|zst)' -printf './%P\0') if (( ${#compressed_files[@]} )) && pushd "$BUILDROOT" >/dev/null; then quiet "Moving to early root: '%s'" "${compressed_files[@]#.}" # try to hard link files, fall back to copy if that fails cp -flt "$EARLYROOT" --parents "${compressed_files[@]}" 2>/dev/null \ || cp -fat "$EARLYROOT" --parents "${compressed_files[@]}" rm -f "${compressed_files[@]}" # delete moved directories if they are empty, sorted by depth ascending local moved_dirs=() mapfile -td '' moved_dirs < <(printf '%s\0' "${compressed_files[@]%/*}" \ | awk -vRS='\0' '{printf("%d\t%s\0", split($0, _, "/"), $0)}' \ | LC_ALL=C.UTF-8 sort -uz -k1n,1 -k2 | cut -zf2-) while (( ${#moved_dirs[@]} )); do mapfile -td '' moved_dirs < <(find "${moved_dirs[@]}" -maxdepth 0 -empty -delete -printf '%h\0' | uniq -z) done popd >/dev/null || return fi ;;& xz) COMPRESSION_OPTIONS=('--check=crc32' "${COMPRESSION_OPTIONS[@]}") ;; lz4) COMPRESSION_OPTIONS=('-l' "${COMPRESSION_OPTIONS[@]}") ;; zstd) COMPRESSION_OPTIONS=('-T0' "${COMPRESSION_OPTIONS[@]}") ;; esac if [[ -f "$out" ]]; then local curr_size space_left_on_device curr_size="$(stat --format="%s" "$out")" space_left_on_device="$(($(stat -f --format="%a*%S" "$out")))" # check if there is enough space on the device to write the image to a tempfile, fallback otherwise # this assumes that the new image is not more than 1¼ times the size of the old one (( $((curr_size + (curr_size/4))) < space_left_on_device )) && compressout="$out".tmp fi # initialize an empty output file : >"$compressout" # remove empty dirs and symlinks from EARLYROOT find "$EARLYROOT" -depth '(' -type d -empty -delete ')' -or '(' -xtype l -delete ')' # if the EARLYROOT is not empty (except for the flag file), prepend it to the image if [[ -n "$(LC_ALL=C.UTF-8 find "$EARLYROOT" -mindepth 1 -not -name early_cpio)" ]]; then errprog="$(create_image "$EARLYROOT" "$compressout" early)" ret="$?" if (( ret )); then error "Early uncompressed CPIO image generation FAILED: '%s' reported an error" "$errprog" return 1 elif (( _builderrors == 0 )); then msg2 "Early uncompressed CPIO image generation successful" fi fi errprog="$(create_image "$BUILDROOT" "$compressout" "$compress")" ret="$?" if (( ret )); then error "Initcpio image generation FAILED: '%s' reported an error" "$errprog" return 1 elif (( _builderrors == 0 )); then msg "Initcpio image generation successful" fi if (( _builderrors )); then warning "errors were encountered during the build. The image may not be complete." fi # sync and rename as we only wrote to a tempfile so far to ensure consistency if [[ "$compressout" != "$out" ]]; then sync -d -- "$compressout" mv -f -- "$compressout" "$out" fi } validate_compression() { local major minor compression_fallback='zstd' if [[ -z "$_optcompress" && -v COMPRESSION_OPTIONS && ! -v COMPRESSION ]]; then warning 'COMPRESSION_OPTIONS is set without also setting COMPRESSION. Configure COMPRESSION explicitly!' fi # Linux < 5.9 does not support zstd-compressed initramfs. Fall back to gzip. IFS=.- read -r major minor _ <<<"$KERNELVERSION" if ! (( major > 5 || (major == 5 && minor >= 9) )); then compression_fallback='gzip' # Show a warning if zstd compression was explicitly configured. if [[ "${_optcompress:-${COMPRESSION}}" == 'zstd' ]]; then _optcompress='' unset COMPRESSION COMPRESSION_OPTIONS warning 'Kernel %s does not support zstd-compressed initramfs. Using gzip compression instead.' "$KERNELVERSION" fi fi _optcompress="${_optcompress:-"${COMPRESSION:-${compression_fallback}}"}" if ! type -P "$_optcompress" >/dev/null; then warning "Unable to locate compression method: '%s'" "$_optcompress" _optcompress='cat' fi } uki_init() { local uefistub="$1" ukiconfig="$2" declare -ga _uki_build_args=() command -v ukify &>/dev/null || _optnoukify=1 if (( ! _optnoukify )); then msg2 'Using ukify to build UKI' declare -gA _ukify_section_map=( [.linux]='--linux=' [.initrd]='--initrd=' [.cmdline]='--cmdline=@' [.osrel]='--os-release=@' [.splash]='--splash=' ) if [[ -z "$ukiconfig" ]]; then for ukiconfig in {/etc,/usr/lib}/kernel/uki.conf ''; do if [[ -f "$ukiconfig" ]]; then quiet "Using ukify config: '%s'" "$ukiconfig" break fi done fi _uki_build_args+=(ukify build --uname="$KERNELVERSION") [[ -z "$ukiconfig" ]] || _uki_build_args+=("--config=$ukiconfig") [[ -z "$uefistub" ]] || warning '--uefistub given but using ukify' return 0 fi quiet 'Using objcopy to build UKI' [[ -z "$ukiconfig" ]] || warning '--ukiconfig given but not using ukify' if [[ -z "$uefistub" ]]; then local cpuarch stub uefiarch cpuarch="$(uname -m)" case "$cpuarch" in x86_64) uefiarch='x64' # Detect 64-bit x86_64 systems with 32-bit IA32 UEFI if [[ -e /sys/firmware/efi/fw_platform_size ]]; then if (( $(< /sys/firmware/efi/fw_platform_size) == 32 )); then uefiarch='ia32' fi else warning 'Cannot determine UEFI bitness. Assuming x64 UEFI.' fi ;; i386|i686) uefiarch='ia32' ;; aarch64*|arm64|armv8*) uefiarch='aa64' ;; arm*) uefiarch='arm' ;; *) uefiarch="$cpuarch" ;; esac for stub in /usr/lib/{systemd/boot/efi,gummiboot}/"linux${uefiarch}.efi.stub"; do if [[ -f "$stub" ]]; then uefistub="$stub" quiet "Using UEFI stub: '%s'" "$uefistub" break fi done if [[ -z "$uefistub" ]]; then error "UEFI stub for architecture '%s' not found" "$uefiarch" return 1 fi fi if [[ ! -f "$uefistub" ]]; then error "UEFI stub '%s' not found" "$uefistub" return 1 fi # global variables shared between uki functions _uki_build_args+=(objcopy "$uefistub" -p) declare -gi _uki_offset=0 _uki_alignment=0 _uki_increase_offset() { local step="$1" _uki_offset+="$(((step + _uki_alignment - 1) / _uki_alignment * _uki_alignment))" } _uki_alignment="$(LC_ALL=C.UTF-8 objdump -p "${uefistub}" \ | awk '/SectionAlignment/ {print strtonum("0x"$2)}')" _uki_increase_offset "$(LC_ALL=C.UTF-8 objdump -h "${uefistub}" \ | awk 'NF==7 {size=strtonum("0x"$3); offset=strtonum("0x"$4)} END {print size + offset}')" # always add the uname section uki_add_section '.uname' <(printf '%s' "$KERNELVERSION") } uki_add_section() { local secname="$1" filename="${2:-/dev/stdin}" if [[ ! -f "$filename" ]]; then local tmpfile tmpfile="$(mktemp -t 'mkinitcpio.XXXXXX')" _tmpfiles+=("$tmpfile") cat -- "$filename" > "$tmpfile" filename="$tmpfile" fi if (( ! _optnoukify )); then _uki_build_args+=("${_ukify_section_map["$secname"]:-"--section=$secname:@"}$filename") else _uki_build_args+=(--add-section "$secname=$filename" --change-section-vma "$secname=$(printf '0x%x' "$_uki_offset")") _uki_increase_offset "$(stat -Lc '%s' "$filename")" fi } uki_assemble() { local out="$1" if (( ! _optnoukify )); then _uki_build_args+=("--output=$out") else _uki_build_args+=("$out") fi quiet 'Assembling UKI: %s' "${_uki_build_args[*]}" if (( _optquiet )); then (umask 0177 && "${_uki_build_args[@]}" >/dev/null) else (umask 0177 && "${_uki_build_args[@]}") fi } build_uki() { local out="$1" initramfs="$2" cmdline="$3" osrelease="$4" splash="$5" kernelimg="$6" uefistub="$7" ukiconfig="$8" msg "Creating unified kernel image: '%s'" "$out" uki_init "$uefistub" "$ukiconfig" || return 1 if [[ -z "$osrelease" ]]; then if [[ -f "/etc/os-release" ]]; then osrelease="/etc/os-release" elif [[ -f "/usr/lib/os-release" ]]; then osrelease="/usr/lib/os-release" fi quiet "Using os-release file: '%s'" "$osrelease" fi if [[ ! -f "$osrelease" ]]; then error "os-release file '%s' not found" "$osrelease" return 1 fi uki_add_section '.osrel' <( suffix="${MKINITCPIO_PROCESS_PRESET##*-}" [[ "$suffix" == 'default' ]] && suffix="" printf 'VERSION_ID=%s%s\n' "$KERNELVERSION" "${suffix:+"~$suffix"}" grep -v '^VERSION_ID=' "$osrelease" ) local cmdline_files=() if [[ -n "$cmdline" ]]; then if [[ -f "$cmdline" ]]; then cmdline_files+=("$cmdline") else error "Kernel cmdline file '%s' not found" "$cmdline" return 1 fi fi if (( ! _optnocmdline )); then if [[ -z "$cmdline" ]]; then if [[ -f "/etc/kernel/cmdline" ]]; then cmdline_files+=("/etc/kernel/cmdline") elif [[ -f "/usr/lib/kernel/cmdline" ]]; then cmdline_files+=("/usr/lib/kernel/cmdline") fi if [[ -d '/etc/cmdline.d' ]]; then mapfile -d '' -O "${#cmdline_files[@]}" cmdline_files < <(LC_ALL=C.UTF-8 find "/etc/cmdline.d" -xtype f -name '*.conf' -print0 | LC_ALL=C.UTF-8 sort -zVu) fi fi if (( ! ${#cmdline_files[@]} )); then warning "Note: no cmdline files were found and --cmdline is not set!" cmdline_files=("/proc/cmdline") warning "Reusing current kernel cmdline from ${cmdline_files[*]}" else msg2 "Using cmdline file: '%s'" "${cmdline_files[@]}" fi uki_add_section '.cmdline' <(printf '%s\n\0' "$(grep -ha -- '^[^#]' "${cmdline_files[@]}" | tr -s '\n' ' ')") else quiet 'Kernel cmdline will not be embedded.' fi if [[ -n "$splash" ]]; then uki_add_section '.splash' "$splash" quiet "Using splash image: '%s'" "$splash" fi if [[ -z "$kernelimg" ]]; then # FIXME: fallback to /boot/vmlinuz-linux can probably be removed as # $KERNELIMAGE should point to an image with the correct version for img in "$KERNELIMAGE" "/boot/vmlinuz-linux"; do if [[ -f "$img" ]]; then kernelimg="$img" quiet "Using kernel image: '%s'" "$kernelimg" break fi done fi if [[ ! -f "$kernelimg" ]]; then error "Kernel image '%s' not found" "$kernelimg" return 1 fi uki_add_section '.linux' "$kernelimg" if [[ -z "$initramfs" ]]; then error "Initramfs '%s' not found" "$initramfs" return 1 fi uki_add_section '.initrd' "$initramfs" if uki_assemble "$out"; then msg 'Unified kernel image generation successful' else error 'Unified kernel image generation FAILED' return 1 fi } # The function is invoked via `map process_preset` # shellcheck disable=SC2317,SC2329 process_preset() ( local preset="$1" preset_image='' preset_options='' local -a preset_mkopts preset_cmd preset_remove_cmd if [[ -n "$MKINITCPIO_PROCESS_PRESET" ]]; then error "You appear to be calling a preset from a preset. This is a configuration error." exit 1 fi # allow path to preset file, else resolve it in $_d_presets if [[ $preset != */* ]]; then printf -v preset '%s/%s.preset' "$_d_presets" "$preset" fi # shellcheck disable=SC1090 . "$preset" || die "Failed to load preset: '%s'" "$preset" (( ! ${#PRESETS[@]} )) && warning "Preset file '%s' is empty or does not contain any presets." "$preset" # Use -m and -v options specified earlier (( _optquiet )) || preset_mkopts+=(-v) (( _optcolor )) || preset_mkopts+=(-n) (( _optsavetree )) && preset_mkopts+=(-s) ret=0 for p in "${PRESETS[@]}"; do if (( _optremove )); then msg "Removing image for preset: $preset: '$p'" else msg "Building image from preset: $preset: '$p'" fi preset_cmd=("${preset_mkopts[@]}") preset_remove_cmd=() preset_kver="${p}_kver" if [[ -n "${!preset_kver:-$ALL_kver}" ]]; then preset_cmd+=(-k "${!preset_kver:-$ALL_kver}") else warning "No kernel version specified. Skipping image '%s'" "$p" continue fi preset_config="${p}_config" if [[ -n "${!preset_config:-$ALL_config}" ]]; then preset_cmd+=(-c "${!preset_config:-$ALL_config}") msg "Using configuration file: '%s'" "${!preset_config:-$ALL_config}" else msg "Using default configuration file: '%s'" "$_f_config" fi preset_uki="${p}_uki" if [[ ! -v "${p}_uki" && -v "${p}_efi_image" ]]; then preset_uki="${p}_efi_image" warning "Deprecated option '%s' found. Update '%s' to use '%s' instead." "${p}_efi_image" "$preset" "${p}_uki" fi if [[ -n "${!preset_uki}" ]]; then preset_cmd+=(-U "${!preset_uki}") preset_remove_cmd+=("${!preset_uki}") fi preset_cmdline="${p}_cmdline" if [[ -n "${!preset_cmdline:-$ALL_cmdline}" ]]; then preset_cmd+=(--cmdline "${!preset_cmdline:-$ALL_cmdline}") fi preset_splash="${p}_splash" if [[ -n "${!preset_splash:-$ALL_splash}" ]]; then preset_cmd+=(--splash "${!preset_splash:-$ALL_splash}") fi preset_image="${p}_image" if [[ -n "${!preset_image}" ]]; then preset_cmd+=(-g "${!preset_image}") preset_remove_cmd+=("${!preset_image}") elif [[ -z "${!preset_uki}" ]]; then warning "No image or UKI specified. Skipping image '%s'" "$p" continue fi local -n preset_options="${p}_options" if [[ "${preset_options@a}" == *a* ]]; then preset_cmd+=("${preset_options[@]}") elif [[ -n "$preset_options" ]]; then mapfile -d ' ' -O "${#preset_cmd[@]}" -t preset_cmd < <( printf '%s' "$preset_options" ) fi local -n preset_microcode="${p}_microcode" if [[ -n "${preset_microcode[*]}" ]]; then warning "Deprecated option '%s' found. Update '%s' to use the 'microcode' hook instead." "$preset_microcode" "$preset" elif [[ -n "${ALL_microcode[*]}" ]]; then warning "Deprecated option 'ALL_microcode' found. Update '%s' to use the 'microcode' hook instead." "$preset" fi if (( _optremove )); then if (( ${#preset_remove_cmd[*]} )); then for pr in "${preset_remove_cmd[@]}"; do if [[ ! -f "${pr}" ]]; then warning "Image not found: '%s'" "$pr" elif [[ ! -w "${pr}" ]]; then error "Image not writable: '%s'" "$pr" else rm -f -- "${pr}" msg2 "Removed: '%s'" "$pr" warning "Image not found: '%s'" "$pr" fi done fi else local preset_name="${preset##*/}"; preset_name="${preset_name%.preset}-$p" preset_cmd+=("${OPTREST[@]}") msg2 "${preset_cmd[*]}" # we won't be calling mkinitcpio recursively, so no need to set MKINITCPIO_PROCESS_PRESET MKINITCPIO_PROCESS_PRESET="$preset_name" "$0" "${preset_cmd[@]}" fi # shellcheck disable=SC2181 (( $? )) && ret=1 done exit "$ret" ) preload_builtin_modules() { local modname field value local -a path # Prime the _addedmodules list with the builtins for this kernel. We prefer # the modinfo file if it exists, but this requires a recent enough kernel # and kmod>=27. if [[ -r $_d_kmoduledir/modules.builtin.modinfo ]]; then while IFS=.= read -rd '' modname field value; do _addedmodules[${modname//-/_}]=2 case "$field" in alias) _addedmodules["${value//-/_}"]=2 ;; esac done <"$_d_kmoduledir/modules.builtin.modinfo" elif [[ -r "$_d_kmoduledir/modules.builtin" ]]; then while IFS=/ read -ra path; do modname="${path[-1]%.ko}" _addedmodules["${modname//-/_}"]=2 done <"$_d_kmoduledir/modules.builtin" fi } run_post_hooks() { local args=("$@") local hook status local -i count=0 local -a paths seen IFS=: read -ra paths <<<"$_d_post" for path in "${paths[@]}"; do for hook in "$path"/*; do if [[ ! -e "$hook" ]] || in_array "${hook##*/}" "${seen[@]}"; then continue fi seen+=("${hook##*/}") [[ -x "$hook" ]] || continue (( count++ )) || msg 'Running post hooks' msg2 'Running post hook: [%s]' "${hook##*/}" KERNELVERSION="$KERNELVERSION" KERNELDESTINATION="$KERNELDESTINATION" command "$hook" "$KERNELIMAGE" "${args[@]}" status="$?" if (( status )); then error "'%s' failed with exit code %d" "$hook" "$status" return 1 fi done done (( count )) && msg 'Post processing done' return 0 } check_path() { # mode can be any combination of drw (directory/readable/writable) local mode="$1" opt="$2" path="$3" mesg mesg="Invalid option $opt" # realpath will reject file/file and dirs without the x flag path="$(realpath -qs -- "$path")" || die "%s -- '%s' is an invalid path" "$mesg" "$3" mesg+=" -- '$path'" if [[ "$mode" == *d* ]]; then [[ ! -d "$path" ]] && die '%s must be a directory' "$mesg" [[ "$mode" == *w* && ! -w "$path" ]] && die '%s must be writable' "$mesg" else [[ -d "$path" ]] && die '%s must not be a directory' "$mesg" # if the file doesn't exist, check if we can write to the parent directory [[ "$mode" == *w* && ! ( -w "$path" || ! -e "$path" && -d "${path%/*}/." && -w "${path%/*}/." ) ]] \ && die '%s must be writable' "$mesg" fi [[ "$mode" == *r* && ! -r "$path" ]] && die '%s must be readable' "$mesg" echo "$path" } # shellcheck source=functions . "$_f_functions" trap 'cleanup' EXIT _opt_short='A:c:D:g:H:hk:nLMPp:Rr:S:sd:t:U:Vvz:' _opt_long=('add:' 'addhooks:' 'config:' 'generate:' 'hookdir': 'hookhelp:' 'help' 'kernel:' 'listhooks' 'automods' 'moduleroot:' 'nocolor' 'nopost' 'allpresets' 'preset:' 'remove' 'skiphooks:' 'save' 'generatedir:' 'builddir:' 'version' 'verbose' 'compress:' 'uki:' 'uefi:' 'microcode:' 'splash:' 'kernelimage:' 'uefistub:' 'cmdline:' 'osrelease:' 'no-cmdline' 'ukiconfig:' 'no-ukify') parseopts "$_opt_short" "${_opt_long[@]}" -- "$@" || exit 1 set -- "${OPTRET[@]}" unset _opt_short _opt_long OPTRET while :; do case "$1" in # --add remains for backwards compat -A | --add | --addhooks) shift IFS=, read -r -a add <<<"$1" _optaddhooks+=("${add[@]}") unset add ;; -c | --config) _f_config="$(check_path r "$1" "$2")" || exit _optconfd=0 shift ;; --cmdline) _optcmdline="$(check_path r "$1" "$2")" || exit shift ;; --no-cmdline) _optnocmdline=1 ;; -k | --kernel) if [[ "$KERNELVERSION" == /* ]]; then KERNELVERSION="$(check_path r "$1" "$2")" || exit else KERNELVERSION="$2" fi shift ;; -s | --save) _optsavetree=1 ;; -d | --generatedir) _opttargetdir="$(check_path dw "$1" "$2")" || exit shift ;; -g | --generate) _optgenimg="$(check_path w "$1" "$2")" || exit shift ;; -h | --help) usage exit 0 ;; -V | --version) version exit 0 ;; -p | --preset) shift _optpreset+=("$1") ;; -R | --remove) _optremove=1 ;; -n | --nocolor) _optcolor=0 ;; --nopost) _optnopost=1 ;; --uefi) warning 'The --uefi option is deprecated. Use --uki instead.' ;& -U | --uki) _optuki="$(check_path w "$1" "$2")" || exit shift ;; -v | --verbose) _optquiet=0 ;; -S | --skiphooks) shift IFS=, read -r -a skip <<<"$1" _optskiphooks+=("${skip[@]}") unset skip ;; -H | --hookhelp) shift hook_help "$1" exit ;; -L | --listhooks) hook_list exit 0 ;; --splash) _optsplash="$(check_path r "$1" "$2")" || exit shift ;; --kernelimage) _optkernelimage="$(check_path r "$1" "$2")" || exit shift ;; --uefistub) _optuefistub="$(check_path r "$1" "$2")" || exit shift ;; --ukiconfig) _optukiconfig="$(check_path r "$1" "$2")" || exit shift ;; --no-ukify) _optnoukify=1 ;; -M | --automods) _optshowautomods=1 ;; --microcode) warning "The --microcode option has deprecated. Use the 'microcode' hook instead." ;& -P | --allpresets) _optpreset=("$_d_presets"/*.preset) [[ -e "${_optpreset[0]}" ]] || die 'No presets found in %s' "$_d_presets" ;; --osrelease) _optosrelease="$(check_path r "$1" "$2")" || exit shift ;; -t | --builddir) TMPDIR="$(check_path dw "$1" "$2")" || exit export TMPDIR shift ;; -z | --compress) shift _optcompress="$1" ;; -r | --moduleroot) _optmoduleroot="$(check_path dr "$1" "$2")" || exit shift ;; -D | --hookdir) tmp="$(check_path dr "$1" "$2")" || exit shift _d_flag_hooks+="$tmp/hooks:" _d_flag_install+="$tmp/install:" _d_flag_post+="$tmp/post:" unset tmp ;; --) shift break 2 ;; esac shift done OPTREST=("$@") if [[ -t 1 ]] && (( _optcolor )); then try_enable_color fi # if we get presets and remove flag, skip to preset processing if (( _optremove && ${#_optpreset[*]} )); then map process_preset "${_optpreset[@]}" exit fi if [[ -n "$_d_flag_hooks" && -n "$_d_flag_install" && -n "$_d_flag_post" ]]; then _d_hooks="${_d_flag_hooks%:}" _d_install="${_d_flag_install%:}" _d_post="${_d_flag_post%:}" fi # If we specified --uki but no -g we want to create a temporary initramfs which will be used with the efi executable. if [[ -n "$_optuki" && -z "$_optgenimg" ]]; then tmpfile="$(mktemp -t mkinitcpio.XXXXXX)" _tmpfiles+=("$tmpfile") _optgenimg="$tmpfile" declare -i initcpio_is_temporary=1 fi # insist that /proc and /dev be mounted (important for chroots) # NOTE: avoid using mountpoint for this -- look for the paths that we actually # use in mkinitcpio. Avoids issues like FS#26344. [[ -e /proc/self/mountinfo ]] || die "/proc must be mounted!" [[ -e /dev/fd ]] || die "/dev must be mounted!" # use preset $_optpreset (exits after processing) if (( ${#_optpreset[*]} )); then map process_preset "${_optpreset[@]}" exit fi KERNELIMAGE='' KERNELDESTINATION='' if [[ "$KERNELVERSION" != 'none' ]]; then # if the "version" is given as a file name, use it without modification, # if doesn't exist, resolve_kernver will fail anyway if [[ "${KERNELVERSION:0:1}" == '/' ]]; then KERNELIMAGE="$KERNELVERSION" fi KERNELVERSION="$(resolve_kernver "$KERNELVERSION")" || exit 1 _d_kmoduledir="$_optmoduleroot/lib/modules/$KERNELVERSION" [[ -d "$_d_kmoduledir" ]] || die "'$_d_kmoduledir' is not a valid kernel module directory" if [[ -z "$KERNELIMAGE" ]]; then # search well-known locations for the kernel image for img in "$_d_kmoduledir/vmlinuz" "/lib/modules/$KERNELVERSION/vmlinuz" \ "$_d_kmoduledir/vmlinux" "/lib/modules/$KERNELVERSION/vmlinux"; do if [[ -f "$img" ]]; then KERNELIMAGE="$img" if read -r pkgbase &>/dev/null <"${img%/*}/pkgbase"; then KERNELDESTINATION="/boot/${img##*/}-${pkgbase}" else KERNELDESTINATION="/boot/${img##*/}-${KERNELVERSION}" fi quiet "located kernel image: '%s'" "$KERNELIMAGE" break fi done fi if [[ -z "$KERNELIMAGE" ]]; then # check version of all kernels in /boot for img in /boot/vmlinuz-* /boot/vmlinux-*; do if [[ -f "$img" && "$(kver "$img")" == "$KERNELVERSION" ]]; then KERNELIMAGE="$img" quiet "located kernel image: '%s'" "$KERNELIMAGE" break fi done fi if [[ -f "$KERNELIMAGE" ]]; then [[ -z "$KERNELDESTINATION" ]] && KERNELDESTINATION="$KERNELIMAGE" else # this is not fatal, initramfs will still be generated but post # hooks will not know what kernel image is used warning 'Could not find kernel image for version %s' "$KERNELVERSION" fi fi MODULES_DECOMPRESS="${MODULES_DECOMPRESS:-"no"}" _d_workdir="$(initialize_buildroot "$KERNELVERSION" "$_opttargetdir")" || exit 1 BUILDROOT="${_opttargetdir:-$_d_workdir/root}" EARLYROOT="$_d_workdir/early" # Source additional configuration files, if no configuration file has been defined either with "-c" or via preset file if [[ -d "$_d_config" ]] && (( _optconfd )); then mapfile -d '' conf_files < <(LC_ALL=C.UTF-8 find "$_d_config" -maxdepth 1 -xtype f -name '*.conf' -print0 | sed -z 's/.*\///' | LC_ALL=C.UTF-8 sort -zVu) if (( ${#conf_files[@]} )); then tmpfile="$(mktemp -t mkinitcpio.XXXXXX)" _tmpfiles+=("$tmpfile") cat -- "$_f_config" > "$tmpfile" || die "Failed to read configuration '%s'" "$_f_config" for conf in "${conf_files[@]}"; do if [[ -r "$_d_config/$conf" ]]; then cat -- "$_d_config/$conf" >> "$tmpfile" msg "Using drop-in configuration file: '%s'" "$conf" fi done _f_config="$tmpfile" fi fi # shellcheck disable=SC1091 source=mkinitcpio.conf . "$_f_config" || die "Failed to read configuration '%s'" "$_f_config" arrayize_config compute_d_fwpath # after returning, hooks are populated into the array '_hooks' # HOOKS should not be referenced from here on compute_hookset if (( ${#_hooks[*]} == 0 )); then die "Invalid config: No hooks found" fi if (( _optshowautomods )); then msg "Modules autodetected" # shellcheck source=install/autodetect PATH="$_d_install" . 'autodetect' build printf '%s\n' "${!_autodetect_cache[@]}" | LC_ALL=C.UTF-8 sort exit 0 fi if [[ -n "$_optgenimg" ]]; then validate_compression msg "Starting build: '%s'" "$KERNELVERSION" elif [[ -n "$_opttargetdir" ]]; then msg "Starting build: '%s'" "$KERNELVERSION" else msg "Starting dry run: '%s'" "$KERNELVERSION" fi # Set umask to so hooks install files with 644 permissions by default umask 022 # set functrace and trap to catch errors in add_* functions declare -i _builderrors=0 set -o functrace trap '(( $? )) && [[ "$FUNCNAME" == add_* ]] && (( ++_builderrors ))' RETURN preload_builtin_modules map run_build_hook "${_hooks[@]}" || (( ++_builderrors )) # process config file parse_config "$_f_config" # switch out the error handler to catch all errors trap -- RETURN trap '(( ++_builderrors ))' ERR set -o errtrace install_modules "${!_modpaths[@]}" # unset errtrace and trap set +o functrace set +o errtrace trap -- ERR # this is simply a nice-to-have -- it doesn't matter if it fails. ldconfig -r "$BUILDROOT" &>/dev/null # remove /var/cache/ldconfig/aux-cache for reproducibility rm -f -- "$BUILDROOT/var/cache/ldconfig/aux-cache" # Set umask to create initramfs images and unified kernel images as 600 umask 077 if [[ -n "$_optgenimg" ]]; then build_image "$_optgenimg" "$_optcompress" || exit 1 _generated+=("$_optgenimg") elif [[ -n "$_opttargetdir" ]]; then msg "Build complete." else msg "Dry run complete, use -g IMAGE to generate a real image" fi if [[ -n "$_optuki" && -n "$_optgenimg" ]]; then if build_uki "$_optuki" "$_optgenimg" "$_optcmdline" "$_optosrelease" \ "$_optsplash" "$_optkernelimage" "$_optuefistub" "$_optukiconfig"; then (( ${#_generated[@]} )) && _generated+=("$_optuki") else (( ++_builderrors )) fi fi if (( ${#_generated[@]} && ! _optnopost )); then run_post_hooks "${_generated[@]}" || (( ++_builderrors )) fi exit $(( !!_builderrors )) # vim: set ft=sh ts=4 sw=4 et: