#!/usr/bin/env bash # Copyright (C) 2018 Luke Shumaker # Copyright (C) 2023-2024 Umorpha Systems # SPDX-License-Identifier: AGPL-3.0-or-later declare -r NAME=osi-mk declare -r VERSION=20231023 # Why is this different than mkosi[1]? # # - mkosi claims to be "legacy-free"--but they call everything that's # not systemd "legacy"; that clearly won't do for creating OpenRC # images. # # - mkosi claims to be "legacy-free", only supporting GPT # disk-labels--but btrfs can be booted directly, without a separate # disk-label, or a separate ESP partition. To a btrfs disk, GPT/ESP # is legacy. # # - Using a raw btrfs disk means that it can easily be mounted without # first dissecting the disk-label. # # [1]: https://github.com/systemd/mkosi set -euE -o pipefail shopt -s expand_aliases install_prefix="$(realpath --logical --canonicalize-missing -- "${BASH_SOURCE[0]}/../..")" readonly install_prefix source "${install_prefix}/lib/osi.sh" source "${install_prefix}/lib/argparse.sh" # Module-facing functions ###################################################### loaded_modules=() load_module() { local module if ! [[ -f $1 ]]; then argparse:error 'Module does not exist: %s' "$1" return fi module="$(realpath -- "$1")" if osi.sh:in_array "$module" "${loaded_modules[@]}"; then return 0 fi loaded_modules+=("$module") # shellcheck source=/dev/null source "$1" } # Internal functions ########################################################### beg_indent() { exec 8> >( osi.sh:printf -v prefix -- "$@" # shellcheck disable=SC2154 # false-positive, doesn't understand `osi.sh:printf -v` "${install_prefix}/lib/indent" "$NAME [$prefix] " ) _indent_pid=$! } alias end_indent=' &8 2>&1 8<&-; exec 8<&-; wait $_indent_pid' normalize_size() { local size ref_file size=$1 ref_file=$2 if [[ $size =~ ^[0-9]+$ ]]; then echo "$size" return fi # Use truncate(1) to do this. It's a little gross, but # because TMPDIR is likely on tmpfs, it's time-cheap, and # because of sparse files, it's space-cheap. local tmpfile tmpfile=$(mktemp -t -- "${0##*/}.size.XXXXXXXXXX") trap "rm -- ${tmpfile@Q}" EXIT case "$size" in '+'*|'-'*|'<'*|'>'*|'/'*|'%'*) truncate --reference="$ref_file" --size="$size" -- "$tmpfile";; *) truncate --size="$size" -- "$tmpfile";; esac stat --format='%s' -- "$tmpfile" } # Built-in hooks ############################################################### osi-mk:genfstab() { local arg_mountpoint=$1 { # This header mimics the stock fstat from 'filesystem'. echo '# Static information about the filesystems.' echo '# See fstab(5) for details.' echo { # This doesn't quite mimic the header from 'filesystem' # # - I swapped "file system" for "device", for clarity, # and so that it's one word for `column -t`. # - I swapped "pass" for "fsck", for clarity. # - I removed the space after the "#", so it works with # `column -t`. echo '# ' if type format_genfstab:"$arg_fmt" &>/dev/null; then format_genfstab:"$arg_fmt" "$arg_mountpoint" else genfstab -t uuid "$arg_mountpoint" | grep '^[#]' | awk '$3 != "swap"' fi } | column -t } >"${arg_mountpoint}/etc/fstab" } osi-mk:directories() { local arg_mountpoint=$1 local spec outside inside for spec in "${arg_directories[@]}"; do outside="${spec%:*}" inside="${spec#"${outside}:"}" # TODO: maybe rsync would be better? mkdir -p -- "$(dirname -- "${arg_mountpoint}/${inside}")" osi.sh:print 'Copying %q to %q:%q' "$outside" "$arg_file" "$inside" cp -aT --no-preserve=ownership -- "$outside" "${arg_mountpoint}/${inside}" done } osi-mk:hostname() { local arg_mountpoint=$1 echo "${arg_conf[hostname]}" >"$arg_mountpoint/etc/hostname" } osi-mk:machine-id() { local arg_mountpoint=$1 case "${arg_conf[machine-id]}" in auto) :;; # leave it alone none) rm -f -- "$arg_mountpoint/etc/machine-id";; *) echo "${arg_conf[machine-id]}" >"$arg_mountpoint/etc/machine-id";; esac } osi-mk:mkinitcpio() { local arg_mountpoint=$1 osi.sh:print 'Configuring mkinitcpio to include all drivers' mkdir -p -- "$arg_mountpoint/etc/mkinitcpio.conf.d" cat >"$arg_mountpoint/etc/mkinitcpio.conf.d/osi-mk.conf" <<'EOF' #!/hint/bash # Copyright (C) 2023 Umorpha Systems # SPDX-License-Identifier: AGPL-3.0-or-later # Remove 'autodetect' from HOOKS; include all drivers. for ((i=0; i<${#HOOKS[@]}; i++)); do if [[ ${HOOKS[i]} == autodetect ]]; then HOOKS=("${HOOKS[@]:0:i}" "${HOOKS[@]:i+1}") fi done EOF } osi-mk:grub-install() { local arg_mountpoint=$1 local target=${arg_conf[bootloader]#grub-} if [[ $target == arm-uboot-* ]]; then target=arm-uboot fi # shellcheck disable=SC2016 arch-chroot -- "$arg_mountpoint" sh -c \ "grub-install --target=${target} "'"$(awk '\''$2 == "/" { print $1 }'\'' maxdate) { print maxdate } else { print } x-- } ' <"$file" >"$file.tmp" mv -T -- "$file.tmp" "$file" touch --date="@$((SOURCE_DATE_EPOCH-1))" -- "$file" done # Backdate pacman.log to 1 second before SOURCE_DATE_EPOCH. if [[ -f "$arg_mountpoint/var/log/pacman.log" ]]; then local old new old='^\[....-..-..T..:..:..[-+]....]' new="[$(TZ=UTC date --date="@$((SOURCE_DATE_EPOCH-1))" --iso-8601=seconds | sed 's/:00$/00/')]" sed -i "s/$old/$new/" -- "$arg_mountpoint/var/log/pacman.log" touch --date="@$((SOURCE_DATE_EPOCH-1))" -- "$arg_mountpoint/var/log/pacman.log" fi # Backdate file timestamps to SOURCE_DATE_EPOCH. if "${format_is_block["$arg_fmt"]}" || ! "${format_after_clamps_mtime["$arg_fmt"]}"; then # Ugg, it's lame that `find` can't find files newer than a # timestamp without using a temporary file. touch --date="@$SOURCE_DATE_EPOCH" -- "$arg_mountpoint/.source_date_epoch" find "$arg_mountpoint" -xdev -depth \ -newer "$arg_mountpoint/.source_date_epoch" \ -exec touch --no-dereference --date="@$SOURCE_DATE_EPOCH" -- {} + rm -f -- "$arg_mountpoint/.source_date_epoch" touch --date="@$SOURCE_DATE_EPOCH" -- "$arg_mountpoint/" fi } # Main ######################################################################### declare -a formats=() declare -A format_options=() declare -A format_is_block=() declare -A format_wants_tmpfs=() declare -A format_after_clamps_mtime=() source "${install_prefix}/lib/mk/format-dir.sh" source "${install_prefix}/lib/mk/format-tar.sh" source "${install_prefix}/lib/mk/format-btrfs.sh" source "${install_prefix}/lib/mk/format-erofs.sh" main() { # argparse specifications ############################################## # # There got to be enough args with enough validation logic # that the declare/parse/set/validate locations were all far # enough apart that it was hard to maintain. # # So I factored out an argparse.sh library, so that I could # keep the logic for a given argument close to itself. # # Please don't hate me for failing to KISS or remember that # YAGNI; please trust that I did actually "NI". opt_specs+=(': --inside : MOUNTPOINT : internal-use only') local arg_mountpoint= opt_visit:inside() { argparse:setmode inside; arg_mountpoint=$1; } opt_specs+=('-e : --edit : [BASE.img] : edit an existing image') local arg_edit=false local arg_edit_base='' opt_visit:edit() { arg_edit=true; arg_edit_base=$1; } opt_final:edit() { if $arg_edit; then # We set arg_file below in opt_positional(). if ! [[ -e ${arg_edit_base:-$arg_file} ]]; then argparse:error 'Image must already exist to --edit: %s' "${arg_edit_base:-$arg_file}" fi fi } opt_specs+=(': --disorderfs : : use disorderfs') local arg_disorderfs=false opt_visit:disorderfs() { arg_disorderfs=true; } opt_specs+=('') opt_specs+=('-d : --directory : OUTSIDE\:INSIDE : include the given directory') local arg_directories=() opt_visit:directory() { if ! [[ -d "${1%:*}" ]]; then argparse:error 'Directory does not exist: %s' "${1%:*}" return fi arg_directories+=("$1") } opt_specs+=('-m : --module : MOD.sh : include the given module') local arg_modules=() opt_visit:module() { if ! [[ -f $1 ]]; then argparse:error 'Module does not exist: %s' "$1" return fi arg_modules+=("$1") } opt_specs+=('-p : --package : PKGNAME : include the given package (or group)') local arg_packages=() opt_visit:package() { arg_packages+=("$1"); } opt_specs+=('-P : --package-file : PKG.pkg.tar.xz : include the given package file') local arg_package_files=() opt_visit:package-file() { if ! [[ -f "$1" ]]; then argparse:error 'Package file does not exist: %s' "$1" return fi arg_package_files+=("$1") } opt_specs+=('-A : --arch : ARCH : set the CPU architecture') local arg_arch= arg_arch="$(uname -m)" opt_visit_early:arch() { if ! [[ -f "/usr/share/pacman/defaults/pacman.conf.$1" ]]; then argparse:error 'Invalid --conf=arch= value: %q' "$1" return fi arg_arch=$1 } opt_visit:arch() { :; } opt_specs+=(': --pkgconf : KEY=VAL : set which provider of a virtual package to use') declare -A arg_pkgconf=( [initramfs]=mkinitcpio [dbus-units]=dbus-broker-units ) opt_visit:pkgconf() { local key val key="${1%%=*}" val="${1#*=}" arg_pkgconf["$key"]="$val" } arg_fmt=btrfs opt_specs+=(": --fmt : FORMAT : set which disk format (${formats[*]}) to use (default: ${arg_fmt})") opt_visit_early:fmt() { if ! osi.sh:in_array "$1" "${formats[@]}"; then argparse:error 'Unrecognized --fmt= value: %q' "$1" return fi arg_fmt=$1 } opt_visit:fmt() { :; } opt_specs+=(': --fmtconf : KEY=VAL : set a format-specifc config option') declare -A arg_fmtconf=() opt_visit:fmtconf() { local key val key="${1%%=*}" val="${1#*=}" local valid_format_options IFS=' ' read -r -a valid_format_options <<<"${format_options["$arg_fmt"]}" if ! osi.sh:in_array "$key" "${valid_format_options[@]}"; then argparse:error '--fmt=%s does not accept --fmtconf=%s=' "$arg_fmt" "$key" return fi arg_fmtconf["$key"]="$val" } opt_final:fmtconf() { if "${format_is_block["$arg_fmt"]}" && ! $arg_edit && [[ -z ${arg_fmtconf[size]:-} ]]; then argparse:error '--fmt=%s requires specifying --fmtconf=size= when creating a new image' "$arg_fmt" fi if type format_checkconf:"$arg_fmt" &>/dev/null; then format_checkconf:"$arg_fmt" fi } opt_specs+=(': --conf : KEY=VAL : set an osi-mk config option') declare -A arg_conf=( [bootloader]='' # default is set in opt_final:conf() [genfstab]='true' [machine-id]='auto' [hostname]='' [SOURCE_DATE_EPOCH]='' ) if [[ "${SOURCE_DATE_EPOCH:-}" =~ ^([0-9]+|auto)$ ]]; then arg_conf[SOURCE_DATE_EPOCH]="$SOURCE_DATE_EPOCH" arg_orig=(--conf=SOURCE_DATE_EPOCH="${arg_conf[SOURCE_DATE_EPOCH]}" "${arg_orig[@]}") fi opt_visit:conf() { local key val key="${1%%=*}" val="${1#*=}" case "$key" in bootloader) case "$val" in # This is the list as of GRUB 2.12-1.parabola1 grub-i386-pc) :;; grub-i386-efi) :;; grub-i386-emu) :;; grub-i386-xen) :;; grub-i386-qemu) :;; grub-i386-ieee1275) :;; grub-i386-coreboot) :;; grub-i386-multiboot) :;; grub-x86_64-efi) :;; grub-x86_64-emu) :;; grub-x86_64-xen) :;; grub-arm-emu) :;; grub-arm-uboot) :;; grub-arm-uboot-am335x_bone) :;; grub-arm-uboot-omap3_beagle) :;; grub-arm-uboot-omap3_beagle_xm) :;; grub-arm-uboot-omap3_beagle_xm_ab) :;; grub-arm-uboot-udoo) :;; none) :;; *) argparse:error 'Unrecognized --conf=bootloader= value: %q' "$val"; return;; esac case "$arg_arch" in i686) case "$val" in grub-i386-*) :;; grub-*) argparse:error 'Invalid GRUB platform for CPU architecture: %q %q' "$arg_arch" "$val"; return;; esac ;; x86_64) case "$val" in grub-i386-*) :;; grub-x86_64-*) :;; grub-*) argparse:error 'Invalid GRUB platform for CPU architecture: %q %q' "$arg_arch" "$val"; return;; esac ;; armv7h) case "$val" in grub-arm-*) :;; grub-*) argparse:error 'Invalid GRUB platform for CPU architecture: %q %q' "$arg_arch" "$val"; return;; esac ;; esac ;; genfstab) case "${val,,}" in 1|t|true|y|yes) val=true;; 0|f|false|n|no) val=false;; *) argparse:error 'Unrecognized --conf=genfstab= value: %q' "$val"; return;; esac ;; machine-id) if ! [[ "$val" =~ ^(auto|none|[0-9a-f]{32})$ ]]; then argparse:error 'Invalid --conf=machine-id= value: %q' "$val" return fi ;; hostname) : ;; SOURCE_DATE_EPOCH) if ! [[ "$val" =~ ^([0-9]+|auto|)$ ]]; then argparse:error 'Invalid --conf=SOURCE_DATE_EPOCH= value: %q' "$val" return fi ;; *) argparse:error 'Unrecognized flag: --conf=%s' "$key" return ;; esac arg_conf["$key"]="$val"; } opt_final:conf() { if [[ -z "${arg_conf[bootloader]:-}" ]]; then case "$arg_arch" in x86_64|i686) arg_conf[bootloader]=grub-i386-pc;; armv7h) arg_conf[bootloader]=grub-arm-uboot;; esac fi } local arg_file opt_positional() { if (( $# != 1 )); then argparse:error "Expected 1 positional argument, got %d: %s" "$#" "${*@Q}" return fi arg_file=$1 if [[ "$opt_mode" != inside ]]; then if [[ ( $arg_edit = false || -n $arg_edit_base ) && -e $arg_file ]]; then argparse:error 'Image file already exists, refusing to overwrite: %s' "$arg_file" fi fi } # actually parse the args ############################################## local arg_orig=("$@") argparse outside "$@" # main ################################################################ case "$opt_mode" in error) osi.sh:print "Try '%q --help' for more information" "${0##*/}" >&2 return $EXIT_INVALIDARGUMENT ;; version) osi.sh:print "%s (osi-tools) %s" "$NAME" "$VERSION" return $EXIT_SUCCESS ;; help) osi.sh:print 'Usage: %s [OPTIONS] FILENAME.img' "${0##*/}" osi.sh:print 'Operating System Image: Make' echo osi.sh:print 'Create a mountable, bootable OS image.' echo osi.sh:print 'OPTIONS:' echo "${opt_flaghelp}" | grep -v -e --inside return $EXIT_SUCCESS ;; # main code starts here outside) # the part that runs as a normal user without the image mounted mount_dev="$arg_file" mount_opt='' beg_indent 'format:before' { if "${format_is_block["$arg_fmt"]}"; then if $arg_edit; then if [[ -n $arg_edit_base ]]; then cp -T --reflink -- "$arg_edit_base" "$arg_file" fi old_size=$(stat --format='%s' -- "$arg_file") new_size=$old_size if [[ -n ${arg_fmtconf[size]:-} ]]; then new_size=$(normalize_size "${arg_fmtconf[size]}" "$arg_file") fi if (( new_size > old_size )); then truncate --size="$new_size" -- "$arg_file" fi format_editfs:"$arg_fmt" if (( new_size < old_size )); then truncate --size="$new_size" -- "$arg_file" fi else truncate --size="${arg_fmtconf[size]}" -- "$arg_file" format_mkfs:"$arg_fmt" fi else if "${format_wants_tmpfs["$arg_fmt"]}"; then mount_dev=$(mktemp -dt -- "${0##*/}.dev.XXXXXXXXXX") # Make our own tmpfs because $TMPDIR might be mounted # with 'noexec' or something. Also, so we can clean it # up without a `sudo rm -rf`. osi.sh:sudo mount -t tmpfs tmpfs:osi-mk "$mount_dev" trap "osi.sh:sudo umount ${mount_dev@Q}; rmdir -- ${mount_dev@Q}" EXIT mount_opt=bind fi format_before:"$arg_fmt" fi } end_indent arg_mountpoint=$(mktemp -dt -- "${0##*/}.mnt.XXXXXXXXXX") r=0 osi.sh:sudo -- "${install_prefix}/bin/osi-mount" --root \ --rwdir="${TMPDIR:-/tmp}" \ --rwdir=/var/cache/pacman/pkg \ --rwdir=/etc/pacman.d/gnupg \ --options="$mount_opt" \ -- \ "$mount_dev" "$arg_mountpoint" \ "${BASH_SOURCE[0]}" --inside="$arg_mountpoint" \ "${arg_orig[@]}" || r=$? rmdir -- "$arg_mountpoint" if (( r != 0 )); then return $r fi if ! "${format_is_block["$arg_fmt"]}"; then beg_indent 'format:after' { if "${format_after_clamps_mtime["$arg_fmt"]}"; then if [[ "${arg_conf[SOURCE_DATE_EPOCH]:-}" == auto ]]; then local file for file in "$mount_dev"/var/lib/pacman/local/*/desc; do arg_conf[SOURCE_DATE_EPOCH]="$(sed -n '/^%INSTALLDATE%$/{n;p;}' -- "$file")" break done fi if [[ -n "${arg_conf[SOURCE_DATE_EPOCH]:-}" ]]; then export SOURCE_DATE_EPOCH="${arg_conf[SOURCE_DATE_EPOCH]}" fi fi format_after:"$arg_fmt" } end_indent fi osi.sh:print '%s Done' "$NAME" ;; inside) # the part that runs as root with the image mounted osi.sh:needs_sudo _deferred=() _do_deferred() { for (( i=${#_deferred[@]}-1; i >= 0; i-- )); do eval "${_deferred[$i]}" done } trap _do_deferred EXIT defer() { _deferred+=("$1") } ### Load modules ### packages=("${arg_packages[@]}") package_files=("${arg_package_files[@]}") pre_install=() post_install=() for module in "${arg_modules[@]}"; do load_module "$module" done ### Builtin "modules" ### post_install+=( 10:osi-mk:machine-id 50:osi-mk:directories ) if "${arg_conf[genfstab]}"; then if ! $arg_edit || [[ -n $arg_edit_base ]] || [[ -n "${arg_fmtconf[fsuuid]:-}" ]]; then post_install+=(10:osi-mk:genfstab) fi fi case "${arg_conf[bootloader]}" in grub-*) if [[ "${arg_conf[bootloader]}" == grub-arm-uboot-* ]]; then packages+=("grub-${arg_conf[bootloader]#grub-arm-uboot-}") else packages+=(grub) fi post_install+=(88:osi-mk:grub-mkconfig) # before 89:osi-mk:source-date-epoch if ! $arg_edit; then post_install+=(87:osi-mk:grub-install) # before '88:osi-mk:grub-mkconfig' fi ;; esac case "${arg_pkgconf[initramfs]}" in mkinitcpio) pre_install+=(10:osi-mk:mkinitcpio) ;; esac if [[ -n "${arg_conf[hostname]:-}" ]]; then post_install+=(10:osi-mk:hostname) fi if [[ -n "${arg_conf[SOURCE_DATE_EPOCH]:-}" ]]; then post_install+=(89:osi-mk:source-date-epoch) fi ### Finalize config ### local tmpdir tmpdir="$(mktemp -dt "${0##*/}.XXXXXXXXXX")" defer "rmdir -- ${tmpdir@Q}" pacman_conf="/usr/share/pacman/defaults/pacman.conf.$arg_arch" local pkgfile pkgdeps for pkgfile in "${package_files[@]}"; do mapfile -t pkgdeps < <(bsdtar xfO "$pkgfile" .PKGINFO | sed -n 's/^depend = //p') cache_packages+=("${pkgdeps[@]}") done cache_packages+=("${packages[@]}") ### Disorderfs ### if $arg_disorderfs; then orig_arg_mountpoint=$arg_mountpoint arg_mountpoint="$tmpdir/dis" mkdir -- "$arg_mountpoint" disorderfs --multi-user=yes --shuffle-dirents=yes "$orig_arg_mountpoint" "$arg_mountpoint" defer "fusermount -u ${arg_mountpoint@Q}; rmdir ${arg_mountpoint@Q}" fi ### Download ### beg_indent 'download:repos' { # Download syncdbs to the image mkdir -p -- "$arg_mountpoint"/var/{lib/pacman,log} pacman -r "$arg_mountpoint" --config="$pacman_conf" \ -Sy --noconfirm } end_indent beg_indent 'download:check-config' { # Validate that the user specified an answer to # every question that pacman asks. opt_fail=false while read -r -a options; do name="${options[0]%:}" options=("${options[@]:1}") base=${name%%[<>=]*} if osi.sh:in_array "${arg_pkgconf["$base"]:-}" "${options[@]}" "${options[@]#*/}"; then osi.sh:print 'inserting package %s=%s' "$name" "${arg_pkgconf["$base"]}" packages+=("${arg_pkgconf["$base"]}") cache_packages+=("${arg_pkgconf["$base"]}") else osi.sh:error 0 "must set option '%q' to one of [%s]" "$base" "${options[*]}" opt_fail=true fi done < <("${install_prefix}/lib/pacman-choices" \ -r "$arg_mountpoint" --config="$pacman_conf" \ --oneline -- "${cache_packages[@]}") if [[ $opt_fail == true ]]; then exit $EXIT_NOTCONFIGURED fi } end_indent beg_indent 'download:packages' { if (( ${#cache_packages[@]} > 0 )); then # this check is important for --edit # Download needed packages to the host cache pacman -r "$arg_mountpoint" --config="$pacman_conf" \ -Syw --noconfirm -- "${cache_packages[@]}" # Copy needed packages from host cache to image cache mkdir -p -- "$arg_mountpoint"/var/cache/pacman/pkg pacman -r "$arg_mountpoint" --config="$pacman_conf" \ -Sp --print-format='%l' -- "${cache_packages[@]}" \ | sed -En 's,^file://(.*),\1\n\1.sig,p' \ | xargs -d $'\n' -r cp -t "$arg_mountpoint/var/cache/pacman/pkg" -- fi } end_indent if [[ "${arg_conf[SOURCE_DATE_EPOCH]:-}" =~ ^[0-9]+$ ]]; then osi.sh:print '%s SOURCE_DATE_EPOCH=%s' "$NAME" "${arg_conf[SOURCE_DATE_EPOCH]}" export SOURCE_DATE_EPOCH="${arg_conf[SOURCE_DATE_EPOCH]}" fi ### pre_install ### while IFS=: read -r n fn; do beg_indent 'pre_install:%s:%s' "$n" "$fn" ( osi.sh:print Begin "$fn" "$arg_mountpoint" osi.sh:print End ) end_indent done < <([[ "${#pre_install[@]}" == 0 ]] || printf '%s\n' "${pre_install[@]}" | sort) rm -f -- "$arg_mountpoint"/var/log/pacman.log ### Install ### beg_indent 'install' { local pacstrap_flags=( -G # don't copy the host's pacman keyring -M # don't copy the host's mirrorlist -C "$pacman_conf" -- "$arg_mountpoint" --hookdir="$arg_mountpoint/etc/pacman.d/hooks" # hack around https://bugs.archlinux.org/task/49347 --needed # for --edit ) if (( ${#packages[@]} > 0 )); then # this check is important for --edit TMPDIR='' pacstrap "${pacstrap_flags[@]}" "${packages[@]}" fi if (( ${#package_files[@]} > 0 )); then TMPDIR='' pacstrap -U "${pacstrap_flags[@]}" "${package_files[@]}" fi if [[ -f "$arg_mountpoint/var/log/pacman.log" ]]; then sed -i "s:${arg_mountpoint}:/mnt:g" -- "$arg_mountpoint/var/log/pacman.log" fi } end_indent if [[ "${arg_conf[SOURCE_DATE_EPOCH]:-}" == auto ]]; then # set it to 1 second after the last timestamp from any # package (that extra second is so that post-install # things can be timestamped after the package files). arg_conf[SOURCE_DATE_EPOCH]=$(($({ zcat "$arg_mountpoint"/var/lib/pacman/local/*/mtree | grep -o 'time=[0-9]*' | cut -d= -f2 cat /var/lib/pacman/local/*/desc | sed -n '/^%BUILDDATE%$/{n;p;}' } | sort -n | tail -n1) + 1)) fi if [[ -n "${arg_conf[SOURCE_DATE_EPOCH]:-}" ]]; then osi.sh:print '%s SOURCE_DATE_EPOCH=%s' "$NAME" "${arg_conf[SOURCE_DATE_EPOCH]}" export SOURCE_DATE_EPOCH="${arg_conf[SOURCE_DATE_EPOCH]}" fi ### post_install ### while IFS=: read -r n fn; do beg_indent 'post_install:%s:%s' "$n" "$fn" ( osi.sh:print Begin "$fn" "$arg_mountpoint" osi.sh:print End ) end_indent done < <([[ "${#post_install[@]}" == 0 ]] || printf '%s\n' "${post_install[@]}" | sort) ;; *) osi.sh:bug 'unknown opt_mode: %s' "$opt_mode";; esac } main "$@"