296 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			296 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
#!/usr/bin/env bash
 | 
						|
# 2018 Luke Shumaker
 | 
						|
declare -r NAME=osi-mk
 | 
						|
declare -r VERSION=20180812
 | 
						|
 | 
						|
# 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 disecting the disk-label.
 | 
						|
#
 | 
						|
# [1]: https://github.com/systemd/mkosi
 | 
						|
 | 
						|
set -euE -o pipefail
 | 
						|
source ./lib/osi.sh
 | 
						|
 | 
						|
loaded_modules=()
 | 
						|
load_module() {
 | 
						|
	local module
 | 
						|
	if ! [[ -f $1 ]]; then
 | 
						|
		error 2 'Module does not exist: %s' "$1"
 | 
						|
	fi
 | 
						|
	module="$(realpath -- "$1")"
 | 
						|
	if in_array "$module" "${loaded_modules[@]}"; then
 | 
						|
		return 0
 | 
						|
	fi
 | 
						|
	loaded_modules+=("$module")
 | 
						|
	# shellcheck disable=SC1090
 | 
						|
	source "$1"
 | 
						|
}
 | 
						|
 | 
						|
osi-mk:genfstab() {
 | 
						|
	local arg_mountpoint=$1
 | 
						|
	genfstab -t uuid "$arg_mountpoint" > "${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?
 | 
						|
		if [[ -d "${arg_mountpoint}/${inside}" ]]; then
 | 
						|
			print 'Removing existing %q:%q' "$arg_file" "$inside"
 | 
						|
			rm -rf -- "${arg_mountpoint:?}/${inside}"
 | 
						|
		fi
 | 
						|
		mkdir -p -- "$(dirname -- "${arg_mountpoint}/${inside}")"
 | 
						|
		print 'Copying %q to %q:%q' "$outside" "$arg_file" "$inside"
 | 
						|
		cp -aT -- "$outside" "${arg_mountpoint}/${inside}"
 | 
						|
	done
 | 
						|
}
 | 
						|
 | 
						|
osi-mk:grub-install() {
 | 
						|
	local arg_mountpoint=$1
 | 
						|
	cat <<-'EOT' >> "$arg_mountpoint/etc/default/grub"
 | 
						|
			GRUB_TIMEOUT=0
 | 
						|
			GRUB_DEFAULT=1 # Use the fallback initramfs, to get all drivers
 | 
						|
			EOT
 | 
						|
	# shellcheck disable=SC2016
 | 
						|
	arch-chroot -- "$arg_mountpoint" sh -c \
 | 
						|
		    'grub-install "$(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
 | 
						|
}
 | 
						|
 | 
						|
main() {
 | 
						|
	local arg_orig=("$@")
 | 
						|
	local arg_mode=outside
 | 
						|
	local arg_mountpoint=
 | 
						|
	local arg_size=
 | 
						|
	local arg_edit=false
 | 
						|
	local arg_edit_base=
 | 
						|
 | 
						|
	local arg_directories=()
 | 
						|
	local arg_modules=()
 | 
						|
	local arg_packages=()
 | 
						|
 | 
						|
	local args
 | 
						|
	if ! args="$(getopt -n "${0##*/}" -o "s:e::d:m:p:hV" -l "inside:,size:,edit::,directory:,module:,package:,help,version" -- "$@")"; then
 | 
						|
		arg_mode=error
 | 
						|
	else
 | 
						|
		eval "set -- $args"
 | 
						|
		while true; do
 | 
						|
			case "$1" in
 | 
						|
				--inside) shift; arg_mode=inside; arg_mountpoint=$1; shift;;
 | 
						|
				-s|--size) shift; arg_size=$1; shift;;
 | 
						|
				-e|--edit) shift; arg_edit=true; arg_edit_base=$1; shift;;
 | 
						|
 | 
						|
				-d|--directory) shift; arg_directories+=("$1"); shift;;
 | 
						|
				-m|--module) shift; arg_modules+=("$1"); shift;;
 | 
						|
				-p|--package) shift; arg_packages+=("$1"); shift;;
 | 
						|
 | 
						|
				-V|--version) shift; arg_mode=version;;
 | 
						|
				-h|--help) shift; arg_mode=usage;;
 | 
						|
				--) shift; break;;
 | 
						|
				*) error 1 'Internal error.  The programmer writing this tool screwed up.';;
 | 
						|
			esac
 | 
						|
		done
 | 
						|
		case "$arg_mode" in
 | 
						|
			outside|inside)
 | 
						|
				if (( $# != 1 )); then
 | 
						|
					if (( $# == 0 )); then
 | 
						|
						error 0 "Expected 1 positional argument, got none"
 | 
						|
					else
 | 
						|
						error 0 "Expected 1 positional argument, got %d: %s" "$#" "${*@Q}"
 | 
						|
					fi
 | 
						|
					arg_mode=error
 | 
						|
				else
 | 
						|
					arg_file=$1
 | 
						|
				fi
 | 
						|
				for module in "${arg_modules[@]}"; do
 | 
						|
					if ! [[ -f $module ]]; then
 | 
						|
						error 0 'Module does not exist: %s' "$module"
 | 
						|
						arg_mode=error
 | 
						|
					fi
 | 
						|
				done
 | 
						|
				for dirspec in "${arg_directories[@]}"; do
 | 
						|
					if ! [[ -d "${dirspec%:*}" ]]; then
 | 
						|
						error 0 'Directory does not exist: %s' "${dirspec%:*}"
 | 
						|
						arg_mode=error
 | 
						|
					fi
 | 
						|
				done
 | 
						|
				if [[ $arg_mode = outside ]]; then
 | 
						|
					if [[ ( $arg_edit = false || -n $arg_edit_base ) && -e $arg_file ]]; then
 | 
						|
						error 2 'Image file already exists, refusing to overwrite: %s' "$arg_file"
 | 
						|
					fi
 | 
						|
					if $arg_edit; then
 | 
						|
						if ! [[ -f ${arg_edit_base:-$arg_file} ]]; then
 | 
						|
							error 2 'Image must already exist to --edit: %s' "${arg_edit_base:-$arg_file}"
 | 
						|
						fi
 | 
						|
					else
 | 
						|
						if [[ -z $arg_size ]]; then
 | 
						|
							error 2 'Must specify --size when creating a new image'
 | 
						|
						fi
 | 
						|
					fi
 | 
						|
				fi
 | 
						|
				;;
 | 
						|
		esac
 | 
						|
	fi
 | 
						|
	case "$arg_mode" in
 | 
						|
		error) print "Try '%q --help' for more information" "${0##*/}" >&2; return 2;;
 | 
						|
		version)
 | 
						|
			print "%s (notsystemd) %s" "$NAME" "$VERSION"
 | 
						|
			return 0
 | 
						|
			;;
 | 
						|
		usage)
 | 
						|
			print 'Usage: %s [OPTIONS] FILENAME.img' "${0##*/}"
 | 
						|
			print 'Operating System Image: Make'
 | 
						|
			echo
 | 
						|
			print 'Create a mountable, bootable OS image.'
 | 
						|
			echo
 | 
						|
			print 'OPTIONS:'
 | 
						|
			# --inside is internal-only; undocumented
 | 
						|
			print '  -s SIZE, --size=SIZE             set the size of the image (default: 1G)'
 | 
						|
			print '  -e[BASE.img], --edit[=BASE.img]  edit an existing image'
 | 
						|
			echo
 | 
						|
			print '  -d OUTSIDE:INSIDE, --directory=OUTSIDE:INSIDE  include the given directory'
 | 
						|
			print '  -m MOD.sh, --module=MOD.sh                     include the given module'
 | 
						|
			print '  -p PACKAGE, --package=package                  include the given package (or group)'
 | 
						|
			echo
 | 
						|
			print '  -h, --help     display this help'
 | 
						|
			print '  -V, --version  output version information'
 | 
						|
			return 0
 | 
						|
			;;
 | 
						|
 | 
						|
		# main code starts here
 | 
						|
		outside)
 | 
						|
			# shellcheck disable=SC2059
 | 
						|
			printf -v prefix "$(gettext -- '%s [format]')" "$NAME"
 | 
						|
			{
 | 
						|
				if $arg_edit; then
 | 
						|
					if [[ -n $arg_edit_base ]]; then
 | 
						|
						cp -T -- "$arg_edit_base" "$arg_file"
 | 
						|
						btrfstune -fu "$arg_file"
 | 
						|
					fi
 | 
						|
					if [[ -n $arg_size ]]; then
 | 
						|
						# Calculate sizes in exact bytes now, to avoid any discrepancies
 | 
						|
						# between truncate(1) and btrfs-filesystem(8).
 | 
						|
						# 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.
 | 
						|
						tmpfile=$(mktemp -t -- "${0##*/}.XXXXXXXXXX")
 | 
						|
						trap "rm -- ${tmpfile@Q}" EXIT
 | 
						|
						case "$arg_size" in
 | 
						|
							'+'*|'-'*|'<'*|'>'*|'/'*|'%'*)
 | 
						|
								truncate --reference="$arg_file" --size="$arg_size" -- "$tmpfile";;
 | 
						|
							*)
 | 
						|
								truncate --size="$arg_size" -- "$tmpfile";;
 | 
						|
						esac
 | 
						|
						old_size=$(stat --format='%s' -- "$arg_file")
 | 
						|
						new_size=$(stat --format='%s' -- "$tmpfile")
 | 
						|
						rm -- "$tmpfile"
 | 
						|
						trap - EXIT
 | 
						|
 | 
						|
						# Do the resize
 | 
						|
						arg_mountpoint=$(mktemp -dt -- "${0##*/}.XXXXXXXXXX")
 | 
						|
						trap "rmdir -- ${arg_mountpoint@Q}" EXIT
 | 
						|
						if (( new_size > old_size )); then
 | 
						|
							truncate --size="$new_size" -- "$arg_file"
 | 
						|
							sudo -- ./osi-mount --root -- "$arg_file" "$arg_mountpoint" btrfs filesystem resize max "$arg_mountpoint"
 | 
						|
						elif (( new_size < old_size )); then
 | 
						|
							sudo -- ./osi-mount --root -- "$arg_file" "$arg_mountpoint" btrfs filesystem resize "$new_size" "$arg_mountpoint"
 | 
						|
							truncate --size="$new_size" -- "$arg_file"
 | 
						|
						fi
 | 
						|
						rmdir -- "$arg_mountpoint"
 | 
						|
						trap - EXIT
 | 
						|
					fi
 | 
						|
				else
 | 
						|
					truncate --size="$arg_size" -- "$arg_file"
 | 
						|
					mkfs.btrfs -- "$arg_file"
 | 
						|
				fi
 | 
						|
			} |& sed "s|^|${prefix} |"
 | 
						|
			arg_mountpoint=$(mktemp -dt -- "${0##*/}.XXXXXXXXXX")
 | 
						|
			trap "rmdir -- ${arg_mountpoint@Q}" EXIT
 | 
						|
			sudo -- ./osi-mount --root -- "$arg_file" "$arg_mountpoint" "${BASH_SOURCE[0]}" --inside="$arg_mountpoint" "${arg_orig[@]}"
 | 
						|
			;;
 | 
						|
		inside) # just keep reading...
 | 
						|
			needs_sudo
 | 
						|
 | 
						|
			### Load modules ###
 | 
						|
			packages=("${arg_packages[@]}")
 | 
						|
			cache_packages=()
 | 
						|
			post_install=(
 | 
						|
				00:osi-mk:genfstab
 | 
						|
				50:osi-mk:directories
 | 
						|
				99:osi-mk:grub-mkconfig
 | 
						|
			)
 | 
						|
			if ! $arg_edit; then
 | 
						|
				packages+=(grub btrfs-progs)
 | 
						|
				post_install+=(
 | 
						|
					98:osi-mk:grub-install
 | 
						|
				)
 | 
						|
			fi
 | 
						|
			for module in "${arg_modules[@]}"; do
 | 
						|
				load_module "$module"
 | 
						|
			done
 | 
						|
			cache_packages+=("${packages[@]}")
 | 
						|
 | 
						|
			#### install ###
 | 
						|
			# shellcheck disable=SC2059
 | 
						|
			printf -v prefix "$(gettext -- '%s [install]')" "$NAME"
 | 
						|
			{
 | 
						|
				# Pre-fill the package cache
 | 
						|
				if (( ${#cache_packages[@]} > 0 )); then
 | 
						|
					mkdir -p -- "$arg_mountpoint"/var/{cache/pacman/pkg,lib/pacman,log}
 | 
						|
					# Download anything the host doesn't already have cached
 | 
						|
					pacman -r "$arg_mountpoint" --config=/usr/share/pacman/defaults/pacman.conf.x86_64 \
 | 
						|
					       -Syw --noconfirm -- "${cache_packages[@]}"
 | 
						|
					# Copy needed packages from host cache to image cache
 | 
						|
					pacman -r "$arg_mountpoint" --config=/usr/share/pacman/defaults/pacman.conf.x86_64 \
 | 
						|
					       -Sp --print-format='%l' -- "${cache_packages[@]}" \
 | 
						|
					    | sed -n 's,^file://,,p' \
 | 
						|
					    | xargs -d $'\n' -r cp -t "$arg_mountpoint/var/cache/pacman/pkg" --
 | 
						|
				fi
 | 
						|
				if (( ${#packages[@]} > 0 )); then
 | 
						|
					# The --hookdir bit is to hack around https://bugs.archlinux.org/task/49347
 | 
						|
					pacstrap -M -C /usr/share/pacman/defaults/pacman.conf.x86_64 -- "$arg_mountpoint" \
 | 
						|
						 --hookdir="$arg_mountpoint/etc/pacman.d/hooks" \
 | 
						|
						 --needed \
 | 
						|
						 "${packages[@]}"
 | 
						|
				fi
 | 
						|
			} |& sed "s|^|${prefix} |"
 | 
						|
 | 
						|
			### post_install ###
 | 
						|
			while IFS=: read -r n fn; do
 | 
						|
				# shellcheck disable=SC2059
 | 
						|
				printf -v prefix "$(gettext -- '%s [post_install:%s:%s]')" "$NAME" "$n" "$fn"
 | 
						|
				{
 | 
						|
					print Begin
 | 
						|
					"$fn" "$arg_mountpoint"
 | 
						|
					print End
 | 
						|
				} |& sed "s|^|${prefix} |"
 | 
						|
			done < <(printf '%s\n' "${post_install[@]}" | sort)
 | 
						|
 | 
						|
			### End ###
 | 
						|
			print '%s Done' "$NAME"
 | 
						|
			;;
 | 
						|
 | 
						|
		*) error 1 'Internal error.  The programmer writing this tool screwed up.';;
 | 
						|
	esac
 | 
						|
}
 | 
						|
 | 
						|
main "$@"
 |