0387b253c8
Quote all variables. Terminate option processing using '--' for commands that support it. Do not hardcode file descriptor. Compare integers with arithmetic comparison instead of string comparison. Replace echo with printf. Use heredoc for usage text. Don't print INFO messages when quiet is set. Export SOURCE_DATE_EPOCH.
458 lines
14 KiB
Bash
Executable File
458 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
set -e -u
|
|
|
|
# Control the environment
|
|
umask 0022
|
|
export LANG="C"
|
|
export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-"$(date +%s)"}"
|
|
|
|
app_name="${0##*/}"
|
|
arch="$(uname -m)"
|
|
pkg_list=()
|
|
run_cmd=""
|
|
quiet="y"
|
|
pacman_conf="/etc/pacman.conf"
|
|
iso_label="ARCH_$(date +%Y%m)"
|
|
iso_publisher="Arch Linux <http://www.archlinux.org>"
|
|
iso_application="Arch Linux Live/Rescue CD"
|
|
install_dir="arch"
|
|
work_dir="work"
|
|
out_dir="out"
|
|
sfs_mode="sfs"
|
|
sfs_comp="xz"
|
|
gpg_key=""
|
|
|
|
# Show an INFO message
|
|
# $1: message string
|
|
_msg_info() {
|
|
local _msg="${1}"
|
|
[[ "${quiet}" == "y" ]] || printf '[%s] INFO: %s\n' "${app_name}" "${_msg}"
|
|
|
|
}
|
|
|
|
# Show an ERROR message then exit with status
|
|
# $1: message string
|
|
# $2: exit code number (with 0 does not exit)
|
|
_msg_error() {
|
|
local _msg="${1}"
|
|
local _error=${2}
|
|
printf '\n[%s] ERROR: %s\n\n' "${app_name}" "${_msg}" >&2
|
|
if (( _error > 0 )); then
|
|
exit "${_error}"
|
|
fi
|
|
}
|
|
|
|
_chroot_init() {
|
|
mkdir -p -- "${work_dir}/airootfs"
|
|
_pacman base syslinux
|
|
}
|
|
|
|
_chroot_run() {
|
|
eval -- arch-chroot "${work_dir}/airootfs" "${run_cmd}"
|
|
}
|
|
|
|
_mount_airootfs() {
|
|
trap "_umount_airootfs" EXIT HUP INT TERM
|
|
mkdir -p -- "${work_dir}/mnt/airootfs"
|
|
_msg_info "Mounting '${work_dir}/airootfs.img' on '${work_dir}/mnt/airootfs'"
|
|
mount -- "${work_dir}/airootfs.img" "${work_dir}/mnt/airootfs"
|
|
_msg_info "Done!"
|
|
}
|
|
|
|
_umount_airootfs() {
|
|
_msg_info "Unmounting '${work_dir}/mnt/airootfs'"
|
|
umount -d -- "${work_dir}/mnt/airootfs"
|
|
_msg_info "Done!"
|
|
rmdir -- "${work_dir}/mnt/airootfs"
|
|
trap - EXIT HUP INT TERM
|
|
}
|
|
|
|
# Show help usage, with an exit status.
|
|
# $1: exit status number.
|
|
_usage () {
|
|
IFS='' read -r -d '' usagetext <<ENDUSAGETEXT || true
|
|
usage ${app_name} [options] command <command options>
|
|
general options:
|
|
-p PACKAGE(S) Package(s) to install, can be used multiple times
|
|
-r <command> Run <command> inside airootfs
|
|
-C <file> Config file for pacman.
|
|
Default: '${pacman_conf}'
|
|
-L <label> Set a label for the disk
|
|
Default: '${iso_label}'
|
|
-P <publisher> Set a publisher for the disk
|
|
Default: '${iso_publisher}'
|
|
-A <application> Set an application name for the disk
|
|
Default: '${iso_application}'
|
|
-D <install_dir> Set an install_dir. All files will by located here.
|
|
Default: '${install_dir}'
|
|
NOTE: Max 8 characters, use only [a-z0-9]
|
|
-w <work_dir> Set the working directory
|
|
Default: '${work_dir}'
|
|
-o <out_dir> Set the output directory
|
|
Default: '${out_dir}'
|
|
-s <sfs_mode> Set SquashFS image mode (img or sfs)
|
|
img: prepare airootfs.sfs for dm-snapshot usage
|
|
sfs: prepare airootfs.sfs for overlayfs usage
|
|
Default: '${sfs_mode}'
|
|
-c <comp_type> Set SquashFS compression type (gzip, lzma, lzo, xz, zstd)
|
|
Default: '${sfs_comp}'
|
|
-v Enable verbose output
|
|
-h This message
|
|
commands:
|
|
init
|
|
Make base layout and install base group
|
|
install
|
|
Install all specified packages (-p)
|
|
run
|
|
run command specified by -r
|
|
prepare
|
|
build all images
|
|
pkglist
|
|
make a pkglist.txt of packages installed on airootfs
|
|
iso <image name>
|
|
build an iso image from the working dir
|
|
ENDUSAGETEXT
|
|
printf '%s\n' "${usagetext}"
|
|
exit "${1}"
|
|
}
|
|
|
|
# Shows configuration according to command mode.
|
|
# $1: init | install | run | prepare | iso
|
|
_show_config () {
|
|
local _mode="$1"
|
|
printf '\n'
|
|
_msg_info "Configuration settings"
|
|
_msg_info " Command: ${command_name}"
|
|
_msg_info " Architecture: ${arch}"
|
|
_msg_info " Working directory: ${work_dir}"
|
|
_msg_info " Installation directory: ${install_dir}"
|
|
case "${_mode}" in
|
|
init)
|
|
_msg_info " Pacman config file: ${pacman_conf}"
|
|
;;
|
|
install)
|
|
_msg_info " Pacman config file: ${pacman_conf}"
|
|
_msg_info " Packages: ${pkg_list[*]}"
|
|
;;
|
|
run)
|
|
_msg_info " Run command: ${run_cmd}"
|
|
;;
|
|
prepare)
|
|
;;
|
|
pkglist)
|
|
;;
|
|
iso)
|
|
_msg_info " Image name: ${img_name}"
|
|
_msg_info " Disk label: ${iso_label}"
|
|
_msg_info " Disk publisher: ${iso_publisher}"
|
|
_msg_info " Disk application: ${iso_application}"
|
|
;;
|
|
esac
|
|
printf '\n'
|
|
}
|
|
|
|
# Install desired packages to airootfs
|
|
_pacman () {
|
|
_msg_info "Installing packages to '${work_dir}/airootfs/'..."
|
|
|
|
if [[ "${quiet}" = "y" ]]; then
|
|
pacstrap -C "${pacman_conf}" -c -G -M -- "${work_dir}/airootfs" "$@" &> /dev/null
|
|
else
|
|
pacstrap -C "${pacman_conf}" -c -G -M -- "${work_dir}/airootfs" "$@"
|
|
fi
|
|
|
|
_msg_info "Packages installed successfully!"
|
|
}
|
|
|
|
# Cleanup airootfs
|
|
_cleanup () {
|
|
_msg_info "Cleaning up what we can on airootfs..."
|
|
|
|
# Delete initcpio image(s)
|
|
if [[ -d "${work_dir}/airootfs/boot" ]]; then
|
|
find "${work_dir}/airootfs/boot" -type f -name '*.img' -delete
|
|
fi
|
|
# Delete kernel(s)
|
|
if [[ -d "${work_dir}/airootfs/boot" ]]; then
|
|
find "${work_dir}/airootfs/boot" -type f -name 'vmlinuz*' -delete
|
|
fi
|
|
# Delete pacman database sync cache files (*.tar.gz)
|
|
if [[ -d "${work_dir}/airootfs/var/lib/pacman" ]]; then
|
|
find "${work_dir}/airootfs/var/lib/pacman" -maxdepth 1 -type f -delete
|
|
fi
|
|
# Delete pacman database sync cache
|
|
if [[ -d "${work_dir}/airootfs/var/lib/pacman/sync" ]]; then
|
|
find "${work_dir}/airootfs/var/lib/pacman/sync" -delete
|
|
fi
|
|
# Delete pacman package cache
|
|
if [[ -d "${work_dir}/airootfs/var/cache/pacman/pkg" ]]; then
|
|
find "${work_dir}/airootfs/var/cache/pacman/pkg" -type f -delete
|
|
fi
|
|
# Delete all log files, keeps empty dirs.
|
|
if [[ -d "${work_dir}/airootfs/var/log" ]]; then
|
|
find "${work_dir}/airootfs/var/log" -type f -delete
|
|
fi
|
|
# Delete all temporary files and dirs
|
|
if [[ -d "${work_dir}/airootfs/var/tmp" ]]; then
|
|
find "${work_dir}/airootfs/var/tmp" -mindepth 1 -delete
|
|
fi
|
|
# Delete package pacman related files.
|
|
find "${work_dir}" \( -name '*.pacnew' -o -name '*.pacsave' -o -name '*.pacorig' \) -delete
|
|
_msg_info "Done!"
|
|
}
|
|
|
|
# Makes a ext4 filesystem inside a SquashFS from a source directory.
|
|
_mkairootfs_img () {
|
|
if [[ ! -e "${work_dir}/airootfs" ]]; then
|
|
_msg_error "The path '${work_dir}/airootfs' does not exist" 1
|
|
fi
|
|
|
|
_msg_info "Creating ext4 image of 32GiB..."
|
|
truncate -s 32G -- "${work_dir}/airootfs.img"
|
|
if [[ "${quiet}" == "y" ]]; then
|
|
mkfs.ext4 -q -O '^has_journal,^resize_inode' -E 'lazy_itable_init=0' -m 0 -F -- "${work_dir}/airootfs.img"
|
|
else
|
|
mkfs.ext4 -O '^has_journal,^resize_inode' -E 'lazy_itable_init=0' -m 0 -F -- "${work_dir}/airootfs.img"
|
|
fi
|
|
tune2fs -c 0 -i 0 -- "${work_dir}/airootfs.img" &> /dev/null
|
|
_msg_info "Done!"
|
|
_mount_airootfs
|
|
_msg_info "Copying '${work_dir}/airootfs/' to '${work_dir}/mnt/airootfs/'..."
|
|
cp -aT -- "${work_dir}/airootfs/" "${work_dir}/mnt/airootfs/"
|
|
chown root:root -- "${work_dir}/mnt/airootfs/"
|
|
_msg_info "Done!"
|
|
_umount_airootfs
|
|
mkdir -p -- "${work_dir}/iso/${install_dir}/${arch}"
|
|
_msg_info "Creating SquashFS image, this may take some time..."
|
|
if [[ "${quiet}" = "y" ]]; then
|
|
mksquashfs "${work_dir}/airootfs.img" "${work_dir}/iso/${install_dir}/${arch}/airootfs.sfs" -noappend \
|
|
-comp "${sfs_comp}" -no-progress &> /dev/null
|
|
else
|
|
mksquashfs "${work_dir}/airootfs.img" "${work_dir}/iso/${install_dir}/${arch}/airootfs.sfs" -noappend \
|
|
-comp "${sfs_comp}"
|
|
fi
|
|
_msg_info "Done!"
|
|
rm -- "${work_dir}/airootfs.img"
|
|
}
|
|
|
|
# Makes a SquashFS filesystem from a source directory.
|
|
_mkairootfs_sfs () {
|
|
if [[ ! -e "${work_dir}/airootfs" ]]; then
|
|
_msg_error "The path '${work_dir}/airootfs' does not exist" 1
|
|
fi
|
|
|
|
mkdir -p -- "${work_dir}/iso/${install_dir}/${arch}"
|
|
_msg_info "Creating SquashFS image, this may take some time..."
|
|
if [[ "${quiet}" = "y" ]]; then
|
|
mksquashfs "${work_dir}/airootfs" "${work_dir}/iso/${install_dir}/${arch}/airootfs.sfs" -noappend \
|
|
-comp "${sfs_comp}" -no-progress &> /dev/null
|
|
else
|
|
mksquashfs "${work_dir}/airootfs" "${work_dir}/iso/${install_dir}/${arch}/airootfs.sfs" -noappend \
|
|
-comp "${sfs_comp}"
|
|
fi
|
|
_msg_info "Done!"
|
|
}
|
|
|
|
_mkchecksum () {
|
|
_msg_info "Creating checksum file for self-test..."
|
|
cd -- "${work_dir}/iso/${install_dir}/${arch}"
|
|
sha512sum airootfs.sfs > airootfs.sha512
|
|
cd -- "${OLDPWD}"
|
|
_msg_info "Done!"
|
|
}
|
|
|
|
_mksignature () {
|
|
_msg_info "Creating signature file..."
|
|
cd -- "${work_dir}/iso/${install_dir}/${arch}"
|
|
gpg --detach-sign --default-key "${gpg_key}" airootfs.sfs
|
|
cd -- "${OLDPWD}"
|
|
_msg_info "Done!"
|
|
}
|
|
|
|
command_pkglist () {
|
|
_show_config pkglist
|
|
|
|
_msg_info "Creating a list of installed packages on live-enviroment..."
|
|
pacman -Q --sysroot "${work_dir}/airootfs" > "${work_dir}/iso/${install_dir}/pkglist.${arch}.txt"
|
|
_msg_info "Done!"
|
|
|
|
}
|
|
|
|
# Create an ISO9660 filesystem from "iso" directory.
|
|
command_iso () {
|
|
local _iso_efi_boot_args=()
|
|
|
|
if [[ ! -f "${work_dir}/iso/isolinux/isolinux.bin" ]]; then
|
|
_msg_error "The file '${work_dir}/iso/isolinux/isolinux.bin' does not exist." 1
|
|
fi
|
|
if [[ ! -f "${work_dir}/iso/isolinux/isohdpfx.bin" ]]; then
|
|
_msg_error "The file '${work_dir}/iso/isolinux/isohdpfx.bin' does not exist." 1
|
|
fi
|
|
|
|
# If exists, add an EFI "El Torito" boot image (FAT filesystem) to ISO-9660 image.
|
|
if [[ -f "${work_dir}/iso/EFI/archiso/efiboot.img" ]]; then
|
|
_iso_efi_boot_args+=(
|
|
'-eltorito-alt-boot'
|
|
'-e' 'EFI/archiso/efiboot.img'
|
|
'-no-emul-boot'
|
|
'-isohybrid-gpt-basdat'
|
|
)
|
|
fi
|
|
|
|
_show_config iso
|
|
|
|
mkdir -p -- "${out_dir}"
|
|
_msg_info "Creating ISO image..."
|
|
local _qflag=""
|
|
if [[ "${quiet}" == "y" ]]; then
|
|
xorriso -as mkisofs -quiet \
|
|
-iso-level 3 \
|
|
-full-iso9660-filenames \
|
|
-rational-rock \
|
|
-volid "${iso_label}" \
|
|
-appid "${iso_application}" \
|
|
-publisher "${iso_publisher}" \
|
|
-preparer "prepared by ${app_name}" \
|
|
-eltorito-boot isolinux/isolinux.bin \
|
|
-eltorito-catalog isolinux/boot.cat \
|
|
-no-emul-boot -boot-load-size 4 -boot-info-table \
|
|
-isohybrid-mbr "${work_dir}/iso/isolinux/isohdpfx.bin" \
|
|
"${_iso_efi_boot_args[@]}" \
|
|
-output "${out_dir}/${img_name}" \
|
|
"${work_dir}/iso/"
|
|
else
|
|
xorriso -as mkisofs \
|
|
-iso-level 3 \
|
|
-full-iso9660-filenames \
|
|
-rational-rock \
|
|
-volid "${iso_label}" \
|
|
-appid "${iso_application}" \
|
|
-publisher "${iso_publisher}" \
|
|
-preparer "prepared by ${app_name}" \
|
|
-eltorito-boot isolinux/isolinux.bin \
|
|
-eltorito-catalog isolinux/boot.cat \
|
|
-no-emul-boot -boot-load-size 4 -boot-info-table \
|
|
-isohybrid-mbr "${work_dir}/iso/isolinux/isohdpfx.bin" \
|
|
"${_iso_efi_boot_args[@]}" \
|
|
-output "${out_dir}/${img_name}" \
|
|
"${work_dir}/iso/"
|
|
fi
|
|
_msg_info "Done! | $(ls -sh -- "${out_dir}/${img_name}")"
|
|
}
|
|
|
|
# create airootfs.sfs filesystem, and push it in "iso" directory.
|
|
command_prepare () {
|
|
_show_config prepare
|
|
|
|
_cleanup
|
|
if [[ "${sfs_mode}" == "sfs" ]]; then
|
|
_mkairootfs_sfs
|
|
else
|
|
_mkairootfs_img
|
|
fi
|
|
_mkchecksum
|
|
if [[ "${gpg_key}" ]]; then
|
|
_mksignature
|
|
fi
|
|
}
|
|
|
|
# Install packages on airootfs.
|
|
# A basic check to avoid double execution/reinstallation is done via hashing package names.
|
|
command_install () {
|
|
if [[ ! -f "${pacman_conf}" ]]; then
|
|
_msg_error "Pacman config file '${pacman_conf}' does not exist" 1
|
|
fi
|
|
|
|
if (( ${#pkg_list[@]} == 0 )); then
|
|
_msg_error "Packages must be specified" 0
|
|
_usage 1
|
|
fi
|
|
|
|
_show_config install
|
|
|
|
_pacman "${pkg_list[@]}"
|
|
}
|
|
|
|
command_init() {
|
|
_show_config init
|
|
_chroot_init
|
|
}
|
|
|
|
command_run() {
|
|
_show_config run
|
|
_chroot_run
|
|
}
|
|
|
|
while getopts 'p:r:C:L:P:A:D:w:o:s:c:g:vh' arg; do
|
|
case "${arg}" in
|
|
p)
|
|
read -r -a opt_pkg_list <<< "${OPTARG}"
|
|
pkg_list+=("${opt_pkg_list[@]}")
|
|
;;
|
|
r) run_cmd="${OPTARG}" ;;
|
|
C) pacman_conf="$(realpath -- "${OPTARG}")" ;;
|
|
L) iso_label="${OPTARG}" ;;
|
|
P) iso_publisher="${OPTARG}" ;;
|
|
A) iso_application="${OPTARG}" ;;
|
|
D) install_dir="${OPTARG}" ;;
|
|
w) work_dir="$(realpath -- "${OPTARG}")" ;;
|
|
o) out_dir="$(realpath -- "${OPTARG}")" ;;
|
|
s) sfs_mode="${OPTARG}" ;;
|
|
c) sfs_comp="${OPTARG}" ;;
|
|
g) gpg_key="${OPTARG}" ;;
|
|
v) quiet="n" ;;
|
|
h|?) _usage 0 ;;
|
|
*)
|
|
_msg_error "Invalid argument '${arg}'" 0
|
|
_usage 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if (( EUID != 0 )); then
|
|
_msg_error "${app_name} must be run as root." 1
|
|
fi
|
|
|
|
shift $((OPTIND - 1))
|
|
|
|
if (( $# < 1 )); then
|
|
_msg_error "No command specified" 0
|
|
_usage 1
|
|
fi
|
|
command_name="${1}"
|
|
|
|
case "${command_name}" in
|
|
init)
|
|
command_init
|
|
;;
|
|
install)
|
|
command_install
|
|
;;
|
|
run)
|
|
command_run
|
|
;;
|
|
prepare)
|
|
command_prepare
|
|
;;
|
|
pkglist)
|
|
command_pkglist
|
|
;;
|
|
iso)
|
|
if (( $# < 2 )); then
|
|
_msg_error "No image specified" 0
|
|
_usage 1
|
|
fi
|
|
img_name="${2}"
|
|
command_iso
|
|
;;
|
|
*)
|
|
_msg_error "Invalid command name '${command_name}'" 0
|
|
_usage 1
|
|
;;
|
|
esac
|
|
|
|
# vim:ts=4:sw=4:et:
|