793 lines
23 KiB
Bash
Executable File
793 lines
23 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
|
|
|
|
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=' </dev/null >&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 '#<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
|
|
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 "$@"
|