851 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			851 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
| #!/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
 | |
| shopt -s extglob
 | |
| 
 | |
| 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; 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 '#<device> <mountpoint> <type> <options> <dump> <fsck>'
 | |
| 			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 }'\'' </proc/mounts)"'
 | |
| }
 | |
| 
 | |
| osi-mk:grub-mkconfig() {
 | |
| 	local arg_mountpoint=$1
 | |
| 	arch-chroot -- "$arg_mountpoint" grub-mkconfig -o /boot/grub/grub.cfg
 | |
| }
 | |
| 
 | |
| osi-mk:source-date-epoch() {
 | |
| 	local arg_mountpoint=$1
 | |
| 
 | |
| 	# Backdate package install dates to 1 second before SOURCE_DATE_EPOCH.
 | |
| 	local file
 | |
| 	for file in "$arg_mountpoint"/var/lib/pacman/local/*/desc; do
 | |
| 		awk -v maxdate="$((SOURCE_DATE_EPOCH-1))" '
 | |
| 			BEGIN{
 | |
| 				x=0
 | |
| 			}
 | |
| 			$0 == "%INSTALLDATE%" {
 | |
| 				x=2
 | |
| 			}
 | |
| 			{
 | |
| 				if (x == 1 && $0 > 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
 | |
| 		if [[ "$1" != *.pkg.tar?(.!(sig|*.*)) ]]; then
 | |
| 			argparse:error 'Package filename does not look like a package: %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_conf[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="$tmpdir/pacman.conf"
 | |
| 			defer "rm -f -- ${pacman_conf@Q}"
 | |
| 			if [[ -f "/usr/share/pacman/defaults/pacman.conf.$arg_arch" ]]; then
 | |
| 				cat "/usr/share/pacman/defaults/pacman.conf.$arg_arch" >>"$pacman_conf"
 | |
| 			else
 | |
| 				cat >>"$pacman_conf" <<-EOF
 | |
| 					[options]
 | |
| 					HoldPkg = pacman glibc
 | |
| 					Architecture = $arg_arch
 | |
| 					CheckSpace
 | |
| 					SigLevel = Required DatabaseOptional
 | |
| 					LocalFileSigLevel = Optional
 | |
| 
 | |
| 					[libre]
 | |
| 					Include = /etc/pacman.d/mirrorlist
 | |
| 
 | |
| 					[core]
 | |
| 					Include = /etc/pacman.d/mirrorlist
 | |
| 
 | |
| 					[extra]
 | |
| 					Include = /etc/pacman.d/mirrorlist
 | |
| 
 | |
| 					[pcr]
 | |
| 					Include = /etc/pacman.d/mirrorlist
 | |
| 				EOF
 | |
| 			fi
 | |
| 			if (( ${#package_files[@]} > 0 )); then
 | |
| 				cat >>"$pacman_conf" <<-EOF
 | |
| 					[osi-mk]
 | |
| 					SigLevel = Optional TrustAll
 | |
| 					Server = file://${tmpdir}/repo
 | |
| 				EOF
 | |
| 				mkdir -- "${tmpdir}/repo"
 | |
| 				defer "rm -rf -- ${tmpdir@Q}/repo"
 | |
| 				for file in "${package_files[@]}"; do
 | |
| 					base="$(bsdtar xfO "$file" .PKGINFO | awk '
 | |
| 						$1 == "pkgname" { pkgname=$3 }
 | |
| 						$1 == "pkgver"  { pkgver=$3  }
 | |
| 						$1 == "arch"    { arch=$3    }
 | |
| 						END { print pkgname "-" pkgver "-" arch }')"
 | |
| 					ext=".pkg.tar${file##*.pkg.tar}"
 | |
| 					cp -Tf -- "$file" "${tmpdir}/repo/${base}${ext}"
 | |
| 					packages+=("osi-mk/${base%-*-*-*}") # trim off '-pkgver-pkgrel-arch'
 | |
| 				done
 | |
| 				pushd "$tmpdir/repo" >/dev/null
 | |
| 				repo-add osi-mk.db.tar.gz ./*.pkg.tar?(.!(sig|*.*))
 | |
| 				popd >/dev/null
 | |
| 			fi
 | |
| 
 | |
| 			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"]}"
 | |
| 						if ! osi.sh:in_array "${arg_pkgconf["$base"]}" "${packages[@]#*/}"; then
 | |
| 							packages+=("${arg_pkgconf["$base"]}")
 | |
| 						fi
 | |
| 						if ! osi.sh:in_array "${arg_pkgconf["$base"]}" "${cache_packages[@]#*/}"; then
 | |
| 							cache_packages+=("${arg_pkgconf["$base"]}")
 | |
| 						fi
 | |
| 					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 -n 's,^file://,,p' \
 | |
| 						| {
 | |
| 							while read -r file; do
 | |
| 								echo "$file"
 | |
| 								if [[ -f "$file.sig" ]]; then
 | |
| 									echo "$file.sig"
 | |
| 								fi
 | |
| 							done \
 | |
| 						} \
 | |
| 						| 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 [[ -f "$arg_mountpoint/var/log/pacman.log" ]]; then
 | |
| 					sed -i \
 | |
| 					    -e "s:${arg_mountpoint}:/mnt:g" \
 | |
| 					    -e "s:${pacman_conf}:/tmp/pacman.conf: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 "$@"
 |