Compare commits

..

13 Commits

Author SHA1 Message Date
Mike Yuan
0e14a1a13b
genfstab,common: use named reference instead of globals 2023-03-03 19:27:04 +08:00
Mike Yuan
f40fc1f2b0
doc: several cleanups (follow previous changes) 2023-03-03 19:27:04 +08:00
Mike Yuan
bc1b449f1f
doc: remove footer
The information it provides is pretty irrelevant
these days. People will use GitHub/GitLab for bug
reports :)
2023-03-03 19:27:04 +08:00
Mike Yuan
7881d7ebce
genfstab: drop ancient vfat quirk
Kernel before 3.18 is unsupported by Arch.
2023-03-03 19:27:04 +08:00
Mike Yuan
9aa95365c2
arch-chroot: several cleanups 2023-03-03 19:27:04 +08:00
Mike Yuan
2b9027a0ab
genfstab: fix bind mount handling
We could have multiple mountpoints for one src.
Loop until we find the real path for the bind mount target.
2023-03-03 19:27:04 +08:00
Mike Yuan
05d231aff9
pacstrap: respect custom pacman.conf configs
If some core settings are changed in custom pacman.conf,
they won't be prepended with new rootdir automatically.
2023-03-03 19:27:04 +08:00
Mike Yuan
c21e56e88e
common: add check_root 2023-03-03 19:27:04 +08:00
Mike Yuan
e13ce65908
common: check is_pseudofs or has_fsck more reliably
There might be filesystems we can't cover in common,
e.g. out-of-tree ones. Use findmnt / check if the
fsck command exists to make it more reliable.
2023-03-03 19:27:03 +08:00
Mike Yuan
092226862b
genfstab: re-implement write_source 2023-03-03 19:27:03 +08:00
Mike Yuan
4be53d3488
common: several cleanup for *_setup
Use errexit instead of having to add '&&'
for every single mount.
2023-03-03 19:27:03 +08:00
Mike Yuan
e3f9cb6498
arch-chroot: move resolve_link and chroot_add_resolv_link to common
chroot_add_resolve_link is cleaned up to be simpler.
(We shouldn't need the complex symlink handling logic for target)
2023-03-03 19:27:03 +08:00
Mike Yuan
33cf482649
tree-wide: fix shellcheck warnings and style consistency 2023-03-03 19:27:03 +08:00
23 changed files with 848 additions and 791 deletions

View File

@ -1,26 +0,0 @@
# EditorConfig
# https://editorconfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file, utf-8 charset, 2 space
# indentation, remove any whitespace characters preceding newline characters.
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 2
# for shfmt
switch_case_indent = true
binary_next_line = true
[*.yml]
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab

View File

@ -0,0 +1,23 @@
name: Differential ShellCheck
on:
pull_request:
branches:
- master
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Repository checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Differential ShellCheck
uses: redhat-plumbers-in-action/differential-shellcheck@latest
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,27 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
stages:
- check
.pacman_install:
before_script:
# NOTE: Install latest archlinux-keyring before upgrading system. In the
# future this should not be needed anymore when we can guarantee a valid
# keyring for longer:
# https://gitlab.archlinux.org/archlinux/archlinux-keyring/-/issues/4
- echo -e "\e[0Ksection_start:`date +%s`:pacman[collapsed=true]\r\e[0KInstalling dependencies"
- pacman -Sy --needed --noconfirm archlinux-keyring
- pacman --noconfirm -Syu --needed asciidoc make shellcheck m4
- echo -e "\e[0Ksection_end:`date +%s`:pacman\r\e[0K"
check:
stage: check
extends: .pacman_install
shellcheck:
stage: test
image: koalaman/shellcheck-alpine:latest
script:
- make shellcheck
test:
stage: check
extends: .pacman_install
script:
- make check
- shellcheck common
- shellcheck arch-chroot.in genfstab.in pacstrap.in

View File

@ -1,20 +1,18 @@
VER=26
PREFIX = /usr
PREFIX = /usr/local
BINPROGS = \
future-chroot \
arch-chroot \
genfstab \
pacstrap
MANS = \
doc/future-chroot.8 \
doc/arch-chroot.8 \
doc/genfstab.8 \
doc/pacstrap.8
BASH = bash
ZSHCOMP := $(wildcard completion/zsh/*)
BASHCOMP := $(wildcard completion/bash/*)
all: $(BINPROGS) man
man: $(MANS)
@ -25,13 +23,7 @@ _v_GEN_0 = @echo " GEN " $@;
edit = $(V_GEN) m4 -P $@.in >$@ && chmod go-w,+x $@
future-chroot: future-chroot.in common
$(edit)
genfstab: genfstab.in fstab-helpers
$(edit)
pacstrap: pacstrap.in common
%: %.in common
$(edit)
doc/%: doc/%.asciidoc doc/asciidoc.conf
@ -44,18 +36,15 @@ check: all
@for f in $(BINPROGS); do bash -O extglob -n $$f; done
@r=0; for t in test/test_*; do $(BASH) $$t || { echo $$t fail; r=1; }; done; exit $$r
shellcheck: $(BINPROGS)
shellcheck -W 99 --color $(BINPROGS)
shellcheck -W 99 --color -x test/test_*
install: all
install -d $(DESTDIR)$(PREFIX)/sbin
install -m 0755 $(BINPROGS) $(DESTDIR)$(PREFIX)/sbin
install -d $(DESTDIR)$(PREFIX)/share/zsh/site-functions
install -m 0644 $(ZSHCOMP) $(DESTDIR)$(PREFIX)/share/zsh/site-functions
install -d $(DESTDIR)$(PREFIX)/share/bash-completion/completions
install -m 0644 $(BASHCOMP) $(DESTDIR)$(PREFIX)/share/bash-completion/completions
install -d $(DESTDIR)$(PREFIX)/share/man/man8
install -m 0644 $(MANS) $(DESTDIR)$(PREFIX)/share/man/man8
install -dm755 $(DESTDIR)$(PREFIX)/bin
install -m755 $(BINPROGS) $(DESTDIR)$(PREFIX)/bin
install -Dm644 completion/_archinstallscripts.zsh $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_archinstallscripts
cd completion; for comp in *.bash; do \
install -Dm644 $$comp $(DESTDIR)$(PREFIX)/share/bash-completion/completions/$${comp%%.*}; \
done;
for manfile in $(MANS); do \
install -Dm644 $$manfile -t $(DESTDIR)$(PREFIX)/share/man/man$${manfile##*.}; \
done;
.PHONY: all man clean check shellcheck install
.PHONY: all clean install uninstall

View File

@ -1,14 +1,12 @@
# Modify Arch Install Scripts
# Future Install Scripts
# Arch Install Scripts
This is a small suite of scripts aimed at automating some menial
tasks when installing [Future Linux](https://www.futurelinux.xyz).
tasks when installing [Arch Linux](https://www.archlinux.org).
## Requirements
* GNU coreutils (>= v8.15)
* util-linux (>= 2.39)
* util-linux (>= 2.23)
* POSIX awk
* bash (>= 4.1)
* asciidoc (for generating man pages)

87
arch-chroot.in Normal file
View File

@ -0,0 +1,87 @@
#!/bin/bash
shopt -s extglob
# m4_include() is recognized as a function definition
# shellcheck source=common disable=SC1073,SC1065,SC1064,SC1072
m4_include(common)
setup=chroot_setup
unshare=0
userspec=''
chroot_args=()
usage() {
cat <<EOF
usage: ${0##*/} [options] chroot_dir [command [arguments...]]
Options:
-N Run in unshare mode as a regular user
-u <user>[:group] Specify non-root user and group (optional) to use
-h Print this help message
If 'command' is unspecified, arch-chroot will launch /bin/bash.
Note that when using arch-chroot, the target chroot directory *should* be a
mountpoint. This ensures that tools such as pacman(8) or findmnt(8) have an
accurate hierarchy of the mounted filesystems within the chroot.
If your chroot target is not a mountpoint, you can bind mount the directory on
itself to make it one, i.e. 'mount --bind chroot_dir chroot_dir'.
EOF
}
if [[ -z $1 || $1 = @(-h|--help) ]]; then
usage
exit $(( $# ? 0 : 1 ))
fi
while getopts ':Nu:' flag; do
case $flag in
N)
setup=unshare_setup
unshare=1
;;
u)
userspec="$OPTARG"
;;
:)
die "%s: option requires an argument -- '%s'" "${0##*/}" "$OPTARG"
;;
?)
die "%s: invalid option -- '%s'" "${0##*/}" "$OPTARG"
;;
esac
done
shift $(( OPTIND - 1 ))
(( $# )) || die 'No chroot directory specified'
chrootdir="$1"; shift
if ! mountpoint -q "$chrootdir"; then
warning '%s is not a mountpoint, and thus may cause undesirable side effects. (See help for more info)' "$chrootdir"
fi
command=("$@")
arch-chroot() {
check_root
[[ -d "$chrootdir" ]] || die '%s: not a directory' "$chrootdir"
$setup "$chrootdir"
chroot_add_resolv_conf "$chrootdir" || die 'Failed to setup resolv.conf in chroot'
[[ $userspec ]] && chroot_args+=(--userspec="$userspec")
SHELL=/bin/bash $pid_unshare chroot "${chroot_args[@]}" -- "$chrootdir" "${command[@]}"
}
if (( unshare )); then
$mount_unshare bash -c "$(declare_all); arch-chroot"
else
arch-chroot
fi
# vim: et ts=2 sw=2 ft=sh:

459
common
View File

@ -1,103 +1,284 @@
#!/hint/bash
# SPDX-License-Identifier: GPL-2.0-only
# shellcheck disable=SC2059 # $1 and $2 can contain the printf modifiers
# shellcheck disable=SC2155,SC2064
# generated from util-linux source: libmount/src/utils.c
declare -A pseudofs_types=([anon_inodefs]=1
[apparmorfs]=1
[autofs]=1
[bdev]=1
[binder]=1
[binfmt_misc]=1
[bpf]=1
[cgroup]=1
[cgroup2]=1
[configfs]=1
[cpuset]=1
[debugfs]=1
[devfs]=1
[devpts]=1
[devtmpfs]=1
[dlmfs]=1
[dmabuf]=1
[drm]=1
[efivarfs]=1
[fuse]=1
[fuse.archivemount]=1
[fuse.avfsd]=1
[fuse.dumpfs]=1
[fuse.encfs]=1
[fuse.gvfs-fuse-daemon]=1
[fuse.gvfsd-fuse]=1
[fuse.lxcfs]=1
[fuse.rofiles-fuse]=1
[fuse.vmware-vmblock]=1
[fuse.xwmfs]=1
[fusectl]=1
[hugetlbfs]=1
[ipathfs]=1
[mqueue]=1
[nfsd]=1
[none]=1
[nsfs]=1
[overlay]=1
[pipefs]=1
[proc]=1
[pstore]=1
[ramfs]=1
[resctrl]=1
[rootfs]=1
[rpc_pipefs]=1
[securityfs]=1
[selinuxfs]=1
[smackfs]=1
[sockfs]=1
[spufs]=1
[sysfs]=1
[tmpfs]=1
[tracefs]=1
[vboxsf]=1
[virtiofs]=1)
# generated from: pkgfile -vbr '/fsck\..+' | awk -F. '{ print $NF }' | sort
declare -A fsck_types=([btrfs]=0 # btrfs doesn't need a regular fsck utility
[cramfs]=1
[erofs]=1
[exfat]=1
[ext2]=1
[ext3]=1
[ext4]=1
[f2fs]=1
[fat]=1
[jfs]=1
[minix]=1
[msdos]=1
[reiserfs]=1
[vfat]=1
[xfs]=1)
# shellcheck disable=SC2059
out() { printf "$1 $2\n" "${@:3}"; }
error() { out "==> ERROR:" "$@"; } >&2
warning() { out "==> WARNING:" "$@"; } >&2
msg() { out "==>" "$@"; }
msg2() { out " ->" "$@"; }
warning() { out "==> WARNING:" "$@"; } >&2
error() { out "==> ERROR:" "$@"; } >&2
die() { error "$@"; exit 1; }
ignore_error() {
"$@" 2>/dev/null
return 0
"$@" 2>/dev/null || return 0
}
in_array() {
local i
local -n _arr="$1"
for i in "${_arr[@]}"; do
[[ "$i" = "$1" ]] && return 0
done
return 1
}
rev_array() {
local -n _arr="$1"
mapfile -t _arr < <(printf '%s\n' "${_arr[@]}" | tac)
}
check_root() {
(( EUID == 0 )) || die 'This script must be run with root privileges'
}
resolve_link() {
local link="$1" root="$2" target
if [[ ! $root ]]; then
target="$(realpath -eq "$link")"
else
# This is tricky to do. We read from $link in a loop
# and prepend it with $root if it's not under it.
# This can't handle e.g. $root is /mnt and $link
# is /mnt/1 pointing to /mnt/2, which should actually
# be /mnt/mnt/2. Luckily these edge cases shouldn't
# bother most of the time.
root="$(realpath -e "$root")"
target="$link"
while [[ -L "$target" ]]; do
target="$(readlink -m "$target")"
if [[ "$target" != "$root"/* ]]; then
target="$root/${target#/}"
fi
done
# Normalize the path and make sure the resolved target exists
target="$(realpath -eq "$target")"
fi
printf '%s' "$target"
}
chroot_add_mount() {
mount "$@" && CHROOT_ACTIVE_MOUNTS=("$2" "${CHROOT_ACTIVE_MOUNTS[@]}")
mount "$@" && CHROOT_ACTIVE_MOUNTS+=("$2")
}
chroot_maybe_add_mount() {
local cond=$1; shift
local cond="$1"; shift
if eval "$cond"; then
chroot_add_mount "$@"
fi
}
chroot_setup() {
CHROOT_ACTIVE_MOUNTS=()
[[ $(trap -p EXIT) ]] && die '(BUG): attempting to overwrite existing EXIT trap'
trap 'chroot_teardown' EXIT
chroot_add_mount --bind /dev "$1/dev" &&
chroot_add_mount devpts "$1/dev/pts" -t devpts -o gid=5,mode=0620 &&
chroot_add_mount proc $1/proc -t proc &&
chroot_add_mount sysfs $1/sys -t sysfs &&
chroot_add_mount tmpfs "$1/run" -t tmpfs &&
chroot_add_mount tmpfs "$1/dev/shm" -t tmpfs -o nosuid,nodev &&
ignore_error chroot_maybe_add_mount "[[ -d '$1/sys/firmware/efi/efivars' ]]" \
efivarfs "$1/sys/firmware/efi/efivars" -t efivarfs
}
chroot_teardown() {
if (( ${#CHROOT_ACTIVE_MOUNTS[@]} )); then
umount "${CHROOT_ACTIVE_MOUNTS[@]}"
fi
unset CHROOT_ACTIVE_MOUNTS
}
chroot_add_mount_lazy() {
mount "$@" && CHROOT_ACTIVE_LAZY=("$2" "${CHROOT_ACTIVE_LAZY[@]}")
mount "$@" && CHROOT_ACTIVE_LAZY+=("$2")
}
chroot_bind_device() {
touch "$2" && CHROOT_ACTIVE_FILES=("$2" "${CHROOT_ACTIVE_FILES[@]}")
touch "$2" && CHROOT_ACTIVE_FILES+=("$2")
chroot_add_mount "$1" "$2" --bind
}
chroot_add_link() {
ln -sf "$1" "$2" && CHROOT_ACTIVE_FILES=("$2" "${CHROOT_ACTIVE_FILES[@]}")
ln -sf "$1" "$2" && CHROOT_ACTIVE_FILES+=("$2")
}
chroot_add_resolv_conf() {
local src="$(resolve_link /etc/resolv.conf)" dest="$1/etc/resolv.conf"
# If we don't have a source resolv.conf file, there's nothing useful we can do.
if [[ ! -f "$src" ]]; then
warning 'Cannot find a usable resolv.conf. DNS requests might fail in chroot'
return 0
fi
# If resolv.conf in the chroot is a symlink, we make a backup of it
# and create a plain file so we can bind mount to it.
# The backup is restore during chroot_teardown().
if [[ -L "$dest" ]]; then
mv "$dest" "$dest.orig"
fi
touch -a "$dest"
chroot_add_mount "$src" "$dest" --bind
}
chroot_setup() {
CHROOT_ACTIVE_MOUNTS=()
local root="$1"
local errtrap_old="$(trap -p ERR)" shellopts_old="$SHELLOPTS"
[[ $(trap -p EXIT) ]] && die 'An EXIT trap already exists (likely a bug)'
trap "chroot_teardown $root" EXIT
trap "$errtrap_old; die 'Failed to setup chroot %s' '$root'" ERR
set -e
chroot_add_mount proc "$root"/proc -t proc -o nosuid,noexec,nodev
chroot_add_mount sys "$root"/sys -t sysfs -o nosuid,noexec,nodev,ro
ignore_error chroot_maybe_add_mount "[[ -d '$root/sys/firmware/efi/efivars' ]]" \
efivarfs "$root"/sys/firmware/efi/efivars -t efivarfs -o nosuid,noexec,nodev
chroot_add_mount udev "$root"/dev -t devtmpfs -o mode=0755,nosuid
chroot_add_mount devpts "$root"/dev/pts -t devpts -o mode=0620,gid=5,nosuid,noexec
chroot_add_mount shm "$root"/dev/shm -t tmpfs -o mode=1777,nosuid,nodev
chroot_add_mount run "$root"/run -t tmpfs -o nosuid,nodev,mode=0755
chroot_add_mount tmp "$root"/tmp -t tmpfs -o mode=1777,strictatime,nodev,nosuid
[[ "$shellopts_old" = *'errexit'* ]] || set +e
if [[ $errtrap_old ]]; then
trap "$errtrap_old" ERR
else
trap - ERR
fi
}
chroot_teardown() {
local root="$1"
if (( ${#CHROOT_ACTIVE_MOUNTS[@]} )); then
rev_array CHROOT_ACTIVE_MOUNTS
umount "${CHROOT_ACTIVE_MOUNTS[@]}"
fi
unset CHROOT_ACTIVE_MOUNTS
if [[ -L "$root"/etc/resolv.conf.orig ]]; then
mv "$root"/etc/resolv.conf.orig "$root"/etc/resolv.conf
fi
}
unshare_setup() {
CHROOT_ACTIVE_MOUNTS=()
CHROOT_ACTIVE_LAZY=()
CHROOT_ACTIVE_FILES=()
[[ $(trap -p EXIT) ]] && die '(BUG): attempting to overwrite existing EXIT trap'
trap 'unshare_teardown' EXIT
CHROOT_ACTIVE_MOUNTS=() CHROOT_ACTIVE_LAZY=() CHROOT_ACTIVE_FILES=()
local root="$1"
local errtrap_old="$(trap -p ERR)" shellopts_old="$SHELLOPTS"
chroot_add_mount_lazy "$1" "$1" --bind &&
chroot_add_mount proc "$1/proc" -t proc &&
chroot_add_mount sysfs "$1/sys" -t sysfs &&
chroot_add_link /proc/self/fd "$1/dev/fd" &&
chroot_add_link /proc/self/fd/0 "$1/dev/stdin" &&
chroot_add_link /proc/self/fd/1 "$1/dev/stdout" &&
chroot_add_link /proc/self/fd/2 "$1/dev/stderr" &&
chroot_bind_device /dev/full "$1/dev/full" &&
chroot_bind_device /dev/null "$1/dev/null" &&
chroot_bind_device /dev/random "$1/dev/random" &&
chroot_bind_device /dev/tty "$1/dev/tty" &&
chroot_bind_device /dev/urandom "$1/dev/urandom" &&
chroot_bind_device /dev/zero "$1/dev/zero" &&
chroot_add_mount tmpfs "$1/run" -t tmpfs
[[ $(trap -p EXIT) ]] && die 'An EXIT trap already exists (likely a bug)'
trap "unshare_teardown $root" EXIT
trap "$errtrap_old; die 'Failed to setup chroot %s' '$root'" ERR
set -e
chroot_add_mount_lazy "$root" "$root" --bind
chroot_add_mount proc "$root"/proc -t proc -o nosuid,noexec,nodev
chroot_add_mount_lazy /sys "$root"/sys --rbind
chroot_add_link "$root"/proc/self/fd "$root"/dev/fd
chroot_add_link "$root"/proc/self/fd/0 "$root"/dev/stdin
chroot_add_link "$root"/proc/self/fd/1 "$root"/dev/stdout
chroot_add_link "$root"/proc/self/fd/2 "$root"/dev/stderr
chroot_bind_device /dev/full "$root"/dev/full
chroot_bind_device /dev/null "$root"/dev/null
chroot_bind_device /dev/zero "$root"/dev/zero
chroot_bind_device /dev/random "$root"/dev/random
chroot_bind_device /dev/urandom "$root"/dev/urandom
chroot_bind_device /dev/tty "$root"/dev/tty
chroot_add_mount run "$root"/run -t tmpfs -o nosuid,nodev,mode=0755
chroot_add_mount tmp "$root"/tmp -t tmpfs -o mode=1777,strictatime,nodev,nosuid
[[ "$shellopts_old" = *'errexit'* ]] || set +e
if [[ $errtrap_old ]]; then
trap "$errtrap_old" ERR
else
trap - ERR
fi
}
unshare_teardown() {
chroot_teardown
chroot_teardown "$1"
if (( ${#CHROOT_ACTIVE_LAZY[@]} )); then
rev_array CHROOT_ACTIVE_LAZY
umount --lazy "${CHROOT_ACTIVE_LAZY[@]}"
fi
unset CHROOT_ACTIVE_LAZY
if (( ${#CHROOT_ACTIVE_FILES[@]} )); then
rev_array CHROOT_ACTIVE_FILES
rm "${CHROOT_ACTIVE_FILES[@]}"
fi
unset CHROOT_ACTIVE_FILES
}
pid_unshare="unshare --fork --pid"
# shellcheck disable=SC2034
mount_unshare="$pid_unshare --mount --map-auto --map-root-user --setuid 0 --setgid 0"
# This outputs code for declaring all variables to stdout. For example, if
@ -116,3 +297,173 @@ declare_all() {
# Then declare functions
declare -pf
}
try_cast() (
_=$(( ${1#"$2"} ))
) 2>/dev/null
valid_number_of_base() {
local base="$1" len="${#2}" i
for (( i = 0; i < len; i++ )); do
try_cast "$base" "${2:i:1}" || return 1
done
return 0
}
mangle() {
local i chr out
local {a..f}='' {A..F}=''
for (( i = 0; i < ${#1}; i++ )); do
chr="${1:i:1}"
case $chr in
[[:space:]\\])
printf -v chr '%03o' "'$chr"
out+=\\
;;
esac
out+="$chr"
done
printf '%s' "$out"
}
unmangle() {
local i chr out len="$(( ${#1} - 4 ))"
local {a..f}='' {A..F}=''
for (( i = 0; i < len; i++ )); do
chr="${1:i:1}"
case $chr in
\\)
if valid_number_of_base 8 "${1:i+1:3}" ||
valid_number_of_base 16 "${1:i+1:3}"; then
printf -v chr '%b' "${1:i:4}"
(( i += 3 ))
fi
;;
esac
out+="$chr"
done
printf '%s' "$out${1:i}"
}
optstring_match_one_option() {
local options=() target="$2"
local -n _optstring_match="$1"
IFS=, read -ra options <<<"$_optstring_match"
if [[ "$target" != *'='* ]]; then
options=("${options[@]%%=*}")
fi
in_array options "$target"
}
optstring_get_options() {
local i options=()
local -n _ret="$1" _optstring_get="$2"
local -i got=0
IFS=, read -ra options <<<"$_optstring_get"
shift 2
for i in "${!options[@]}"; do
if optstring_match_one_option 'options[i]' "$1"; then
_ret+=(["${options[i]%%=*}"]="${options[i]//*=}")
got=1
fi
shift || break
done
(( got ))
}
optstring_from_array() {
local optstring
local -n _optarray="$1"
optstring="$(printf ',%s' "${_optarray[@]}")"
optstring="${optstring:1}" # Remove the leading comma
printf '%s' "$optstring"
}
optstring_normalize() {
local i options=()
local -n _optstring_norm="$1"
IFS=, read -ra options <<<"$_optstring_norm"
# remove empty fields
for i in "${!options[@]}"; do
[[ ${options[i]} ]] || unset 'options[i]'
done
# avoid empty strings, reset to "defaults"
if (( ! ${#options[@]} )); then
_optstring_norm="defaults"
else
_optstring_norm="$(optstring_from_array options)"
fi
}
optstring_remove_options() {
local i options=() target="$2"
local -n _optstring_remove="$1"
IFS=, read -ra options <<<"$_optstring_remove"
for i in "${!options[@]}"; do
optstring_match_one_option 'options[i]' "$target" && unset 'options[i]'
done
_optstring_remove="$(optstring_from_array options)"
}
optstring_append_one_option() {
local option="$2"
local -n _optstring_append="$1"
if ! optstring_match_one_option _optstring_append "$option"; then
_optstring_append+=",$option"
fi
optstring_normalize _optstring_append
}
optstring_prepend_one_option() {
local option="$2"
local -n _optstring_prepend="$1"
if ! optstring_match_one_option _optstring_prepend "$option"; then
_optstring_prepend="$option,$_optstring_prepend"
fi
optstring_normalize _optstring_prepend
}
dm_name_for_devnode() {
read -r dm_name <"/sys/class/block/${1#/dev/}/dm/name"
if [[ $dm_name ]]; then
printf '/dev/mapper/%s' "$dm_name"
else
# don't leave the caller hanging, just print the original name
# along with the failure.
error 'Failed to resolve device mapper name for: %s' "$1"
fi
}
fstype_is_pseudofs() {
(( pseudofs_types["$1"] )) || findmnt --pseudo "$1" &>/dev/null
}
fstype_has_fsck() {
(( fsck_types["$1"] == 0 )) && return 1
(( fsck_types["$1"] )) || command -v "fsck.$1" &>/dev/null
}
# vim: et ts=2 sw=2 ft=sh:

View File

@ -1,20 +1,12 @@
#compdef pacstrap
#compdef pacstrap genfstab arch-chroot
_pacstrap_args=(
'-h[display help]'
)
_pacstrap_args_nonh=(
'-C[Use an alternate config file for pacman]:config file:_files -/'
'-c[Use the package cache on the host, rather than the target]'
'-D[Skip pacman dependency checks]'
'-G[Avoid copying the host pacman keyring to the target]'
'-i[Prompt for package confirmation when needed (run interactively)]'
'-K[Initialize an empty pacman keyring in the target (implies -G)]'
'-M[Avoid copying the host mirrorlist to the target]'
'-N[Run in unshare mode as a regular user]'
'-P[Copy the host pacman config to the target]'
'-U[Use pacman -U to install packages]'
'(-h --help)-c[Use the package cache on the host, rather than the target]'
'(--help -h)-i[Avoid auto-confirmation of package selections]'
)
@ -59,6 +51,17 @@ _pacstrap_none(){
"$_longopts[@]" \
}
_genfstab_args=(
'-h[display help]'
)
_genfstab_args_nonh=(
'(--help -h)-p[Avoid printing pseudofs mounts]'
'(-U --help -h)-L[Use labels for source identifiers]'
'(-L --help -h)-U[Use UUIDs for source identifiers]'
)
_arch_chroot_args=( '-h[display help]' )
_longopts=( '--help[display help]' )
_pacstrap(){
@ -91,10 +94,69 @@ _pacstrap(){
fi
}
_genfstab(){
if [[ -z ${(M)words:#--help} && -z ${(M)words:#-*h} ]]; then
case $words[CURRENT] in
-p*|-L*|-U*)
_arguments -s : \
"$_genfstab_args_nonh[@]"
;;
-*)
_arguments -s : \
"$_genfstab_args[@]" \
"$_genfstab_args_nonh[@]" \
"$_longopts[@]"
;;
--*)
_arguments -s : \
"$_longopts[@]"
;;
*)
_arguments \
"$_genfstab_args[@]" \
"$_genfstab_args_nonh[@]" \
"$_longopts[@]" \
":*:_path_files -/"
;;
esac
else
return 1
fi
}
_arch_chroot(){
if [[ -z ${(M)words:#--help} && -z ${(M)words:#-*h} ]]; then
case $words[CURRENT] in
-*)
_arguments -s : \
"$_arch_chroot_args[@]" \
"$_longopts[@]" \
;;
--*)
_arguments -s : \
"$_longopts[@]"
;;
*)
_arguments \
':*:_path_files -/'
;;
esac
else
return 1
fi
}
_install_scripts(){
case "$service" in
pacstrap)
_pacstrap "$@";;
_pacstrap "$@"
;;
genfstab)
_genfstab "$@";;
arch-chroot)
_arch_chroot "$@";;
*)
_message "Error";;
esac
}

View File

@ -1,8 +1,8 @@
_future_chroot() {
_arch_chroot() {
compopt +o dirnames
local cur prev opts i
_init_completion -n : || return
opts="-N -u -r -h"
opts="-N -u -h"
for i in "${COMP_WORDS[@]:1:COMP_CWORD-1}"; do
if [[ -d ${i} ]]; then
@ -22,4 +22,4 @@ _future_chroot() {
compopt -o dirnames
}
complete -F _future_chroot future-chroot
complete -F _arch_chroot arch-chroot

View File

@ -3,7 +3,7 @@ _genfstab() {
local cur prev words cword
_init_completion || return
local opts="-f -L -p -P -t -U -h"
local opts="-f -L -P -p -t -U -h"
case ${prev} in
-f)

View File

@ -1,31 +0,0 @@
#compdef future-chroot
# NOTE: nearly everything here is borrowed from the chroot completion
local -a args=(
'(-h --help)'{-h,--help}'[display help]'
'-N[Run in unshare mode as a regular user]'
'-u[The non-root user and optional group to use]: :->userspecs'
'-r[Do not change the resolv.conf within the chroot]'
'1:new root directory:_directories'
'*:::command:_normal'
)
local ret=1
_arguments $args && ret=0
# @todo user:group specs are probably used often enough to justify making a type
# function for this (see also `chown`, `cpio`, `rsync`, ...)
[[ $state == userspecs ]] &&
if compset -P '*:*:'; then
ret=1
elif compset -P '*:'; then
_groups && ret=0
elif compset -S ':*'; then
_users && ret=0
else
_users -qS : && ret=0
fi
return ret

View File

@ -1,13 +0,0 @@
#compdef genfstab
local -a args=(
'(-h --help)'{-h,--help}'[display help]'
'-p[Avoid printing pseudofs mounts]'
'-f[Restrict output to mountpoints matching the prefix FILTER]'
'(-U -L)-t[Use TAG for source identifiers]:tag:(LABEL UUID PARTLABEL PARTUUID)'
'(-U -t)-L[Use labels for source identifiers]'
'(-L -t)-U[Use UUIDs for source identifiers]'
':*:_path_files -/'
)
_arguments $args

View File

@ -0,0 +1,47 @@
arch-chroot(8)
==============
Name
----
arch-chroot - enhanced chroot command
Synopsis
--------
arch-chroot [options] chroot_dir [command [arguments...]]
Description
-----------
arch-chroot wraps the linkman:chroot[1] command while ensuring that important
functionality is available, e.g. mounting '/dev', '/proc' and other API
filesystems, or exposing linkman:resolv.conf[5] to the chroot.
If 'command' is unspecified, arch-chroot will launch */bin/bash*.
[NOTE]
======
The target chroot directory *should* be a mountpoint. This ensures that tools
such as linkman:pacman[8] or linkman:findmnt[8] have an accurate hierarchy of
the mounted filesystems within the chroot. If your chroot target is not a
mountpoint, you can bind mount the directory on itself to make it a one, i.e.
'mount --bind chroot_dir chroot_dir'
======
Options
-------
*-N*::
Run in unshare mode. This will use linkman:unshare[1] to create a new
mount and user namespace, allowing regular users to create new system
installations.
*-u <user>[:group]*::
Specify non-root user and group (optional) to use.
*-h*::
Output syntax and command line options.
See Also
--------
linkman:chroot[1], linkman:proc[5], linkman:sysfs[5]

View File

@ -1,53 +0,0 @@
future-chroot(8)
==============
Name
----
future-chroot - enhanced chroot command
Synopsis
--------
future-chroot [options] chroot-dir [command] [arguments...]
Description
-----------
future-chroot wraps the linkman:chroot[1] command while ensuring that important
functionality is available, e.g. mounting '/dev/', '/proc' and other API
filesystems, or exposing linkman:resolv.conf[5] to the chroot.
If 'command' is unspecified, future-chroot will launch */bin/bash*.
[NOTE]
======
The target chroot-dir *should* be a mountpoint. This ensures that tools such as
linkman:pacman[8] or linkman:findmnt[8] have an accurate hierarchy of the
mounted filesystems within the chroot. If your chroot target is not a
mountpoint, you can bind mount the directory on itself to make it a mountpoint,
i.e.:
'mount --bind /your/chroot /your/chroot'
======
Options
-------
*-N*::
Run in unshare mode. This will use linkman:unshare[1] to create a new
mount and user namespace, allowing regular users to create new system
installations.
*-u <user>[:group]*::
Specify non-root user and optional group to use.
*-r*::
Do not change the resolv.conf within the chroot. This means that the resolver
might not work in the chroot, which could be the required state.
*-h*::
Output syntax and command line options.
See Also
--------
linkman:pacman[8]

View File

@ -26,12 +26,12 @@ Options
*-L*::
Use labels for source identifiers (shortcut for '-t LABEL').
*-p*::
Exclude pseudofs mounts (default behavior).
*-P*::
Include pseudofs mounts.
*-p*::
Exclude pseudofs mounts (default behavior).
*-t* <tag>::
Use 'tag' for source identifiers (should be one of: 'LABEL', 'UUID',
'PARTLABEL', 'PARTUUID').
@ -45,4 +45,4 @@ Options
See Also
--------
linkman:pacman[8]
linkman:fstab[5], linkman:file-hierarchy[7], linkman:filesystems[5]

View File

@ -1,236 +0,0 @@
#!/hint/bash
# SPDX-License-Identifier: GPL-2.0-only
# generated from util-linux source: libmount/src/utils.c
declare -A pseudofs_types=([anon_inodefs]=1
[apparmorfs]=1
[autofs]=1
[bdev]=1
[binder]=1
[binfmt_misc]=1
[bpf]=1
[cgroup]=1
[cgroup2]=1
[configfs]=1
[cpuset]=1
[debugfs]=1
[devfs]=1
[devpts]=1
[devtmpfs]=1
[dlmfs]=1
[dmabuf]=1
[drm]=1
[efivarfs]=1
[fuse]=1
[fuse.archivemount]=1
[fuse.avfsd]=1
[fuse.dumpfs]=1
[fuse.encfs]=1
[fuse.gvfs-fuse-daemon]=1
[fuse.gvfsd-fuse]=1
[fuse.lxcfs]=1
[fuse.rofiles-fuse]=1
[fuse.vmware-vmblock]=1
[fuse.xwmfs]=1
[fusectl]=1
[hugetlbfs]=1
[ipathfs]=1
[mqueue]=1
[nfsd]=1
[none]=1
[nsfs]=1
[overlay]=1
[pipefs]=1
[proc]=1
[pstore]=1
[ramfs]=1
[resctrl]=1
[rootfs]=1
[rpc_pipefs]=1
[securityfs]=1
[selinuxfs]=1
[smackfs]=1
[sockfs]=1
[spufs]=1
[sysfs]=1
[tmpfs]=1
[tracefs]=1
[vboxsf]=1
[virtiofs]=1)
# generated from: pkgfile -vbr '/fsck\..+' | awk -F. '{ print $NF }' | sort
declare -A fsck_types=([btrfs]=0 # btrfs doesn't need a regular fsck utility
[cramfs]=1
[erofs]=1
[exfat]=1
[ext2]=1
[ext3]=1
[ext4]=1
[f2fs]=1
[fat]=1
[jfs]=1
[minix]=1
[msdos]=1
[reiserfs]=1
[vfat]=1
[xfs]=1)
# shellcheck disable=SC2059 # $1 and $2 can contain the printf modifiers
out() { printf "$1 $2\n" "${@:3}"; }
error() { out "==> ERROR:" "$@"; } >&2
die() { error "$@"; exit 1; }
try_cast() (
_=$(( $1#$2 ))
) 2>/dev/null
valid_number_of_base() {
local base=$1 len=${#2} i=
for (( i = 0; i < len; i++ )); do
try_cast "$base" "${2:i:1}" || return 1
done
return 0
}
mangle() {
local i= chr= out=
local {a..f}= {A..F}=
for (( i = 0; i < ${#1}; i++ )); do
chr=${1:i:1}
case $chr in
[[:space:]\\])
printf -v chr '%03o' "'$chr"
out+=\\
;;
esac
out+=$chr
done
printf '%s' "$out"
}
unmangle() {
local i= chr= out= len=$(( ${#1} - 4 ))
local {a..f}= {A..F}=
for (( i = 0; i < len; i++ )); do
chr=${1:i:1}
case $chr in
\\)
if valid_number_of_base 8 "${1:i+1:3}" ||
valid_number_of_base 16 "${1:i+1:3}"; then
printf -v chr '%b' "${1:i:4}"
(( i += 3 ))
fi
;;
esac
out+=$chr
done
printf '%s' "$out${1:i}"
}
optstring_match_option() {
local candidate pat patterns
IFS=, read -ra patterns <<<"$1"
for pat in "${patterns[@]}"; do
if [[ $pat = *=* ]]; then
# "key=val" will only ever match "key=val"
candidate=$2
else
# "key" will match "key", but also "key=anyval"
candidate=${2%%=*}
fi
[[ $pat = "$candidate" ]] && return 0
done
return 1
}
optstring_remove_option() {
local o options_ remove=$2 IFS=,
read -ra options_ <<<"${!1}"
for o in "${!options_[@]}"; do
optstring_match_option "$remove" "${options_[o]}" && unset 'options_[o]'
done
declare -g "$1=${options_[*]}"
}
optstring_normalize() {
local o options_ norm IFS=,
read -ra options_ <<<"${!1}"
# remove empty fields
for o in "${options_[@]}"; do
[[ $o ]] && norm+=("$o")
done
# avoid empty strings, reset to "defaults"
declare -g "$1=${norm[*]:-defaults}"
}
optstring_append_option() {
if ! optstring_has_option "$1" "$2"; then
declare -g "$1=${!1},$2"
fi
optstring_normalize "$1"
}
optstring_prepend_option() {
local options_=$1
if ! optstring_has_option "$1" "$2"; then
declare -g "$1=$2,${!1}"
fi
optstring_normalize "$1"
}
optstring_get_option() {
local _opts o
IFS=, read -ra _opts <<<"${!1}"
for o in "${_opts[@]}"; do
if optstring_match_option "$2" "$o"; then
declare -g "$o"
return 0
fi
done
return 1
}
optstring_has_option() {
local "${2%%=*}"
optstring_get_option "$1" "$2"
}
dm_name_for_devnode() {
read dm_name <"/sys/class/block/${1#/dev/}/dm/name"
if [[ $dm_name ]]; then
printf '/dev/mapper/%s' "$dm_name"
else
# don't leave the caller hanging, just print the original name
# along with the failure.
error 'Failed to resolve device mapper name for: %s' "$1"
fi
}
fstype_is_pseudofs() {
(( pseudofs_types["$1"] ))
}
fstype_has_fsck() {
(( fsck_types["$1"] ))
}

View File

@ -1,124 +0,0 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-only
shopt -s extglob
unshare=0
keepresolvconf=0
m4_include(common)
usage() {
cat <<EOF
usage: ${0##*/} chroot-dir [command] [arguments...]
-h Print this help message
-N Run in unshare mode as a regular user
-u <user>[:group] Specify non-root user and optional group to use
-r Do not change the resolv.conf within the chroot
If 'command' is unspecified, ${0##*/} will launch /bin/bash.
Note that when using future-chroot, the target chroot directory *should* be a
mountpoint. This ensures that tools such as pacman(8) or findmnt(8) have an
accurate hierarchy of the mounted filesystems within the chroot.
If your chroot target is not a mountpoint, you can bind mount the directory on
itself to make it a mountpoint, i.e. 'mount --bind /your/chroot /your/chroot'.
EOF
}
resolve_link() {
local target=$1
local root=$2
# If a root was given, make sure it ends in a slash.
[[ -n $root && $root != */ ]] && root=$root/
while [[ -L $target ]]; do
target=$(readlink -m "$target")
# If a root was given, make sure the target is under it.
# Make sure to strip any leading slash from target first.
[[ -n $root && $target != $root* ]] && target=$root${target#/}
done
printf %s "$target"
}
chroot_add_resolv_conf() {
local chrootdir=$1
local src
local dest="$chrootdir/etc/resolv.conf"
src=$(resolve_link /etc/resolv.conf)
# If we don't have a source resolv.conf file, there's nothing useful we can do.
[[ -e $src ]] || return 0
if [[ ! -e "$dest" && ! -h "$dest" ]]; then
# There may be no resolv.conf in the chroot. In this case, we'll just exit.
# The chroot environment must not be concerned with DNS resolution.
return 0
fi
chroot_add_mount "$src" "$dest" -c --bind
}
future-chroot() {
(( EUID == 0 )) || die 'This script must be run with root privileges'
[[ -d $chrootdir ]] || die "Can't create chroot on non-directory %s" "$chrootdir"
$setup "$chrootdir" || die "failed to setup chroot %s" "$chrootdir"
if (( ! keepresolvconf )); then
chroot_add_resolv_conf "$chrootdir" || die "failed to setup resolv.conf"
fi
if ! mountpoint -q "$chrootdir"; then
warning "$chrootdir is not a mountpoint. This may have undesirable side effects."
fi
chroot_args=()
[[ $userspec ]] && chroot_args+=(--userspec "$userspec")
SHELL=/bin/bash $pid_unshare chroot "${chroot_args[@]}" -- "$chrootdir" "${args[@]}"
}
while getopts ':hNu:r' flag; do
case $flag in
h)
usage
exit 0
;;
N)
unshare=1
;;
u)
userspec=$OPTARG
;;
r)
keepresolvconf=1
;;
:)
die '%s: option requires an argument -- '\''%s'\' "${0##*/}" "$OPTARG"
;;
?)
die '%s: invalid option -- '\''%s'\' "${0##*/}" "$OPTARG"
;;
esac
done
shift $(( OPTIND - 1 ))
(( $# )) || die 'No chroot directory specified'
chrootdir=$1
shift
args=("$@")
if (( unshare )); then
setup=unshare_setup
$mount_unshare bash -c "$(declare_all); future-chroot"
else
setup=chroot_setup
future-chroot
fi

View File

@ -1,61 +1,60 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-only
shopt -s extglob
m4_include(fstab-helpers)
# m4_include() is recognized as a function definition
# shellcheck source=common disable=SC1073,SC1065,SC1064,SC1072
m4_include(common)
bytag=src
pseudofs=0
write_source() {
local src=$1 spec= label= uuid= comment=()
local src spec comments=()
label=$(lsblk -rno LABEL "$1" 2>/dev/null)
uuid=$(lsblk -rno UUID "$1" 2>/dev/null)
src="$(mangle "$1")"
local -A sources=([LABEL]="$(mangle "$(lsblk -rno LABEL "$1" 2>/dev/null)")"
[UUID]="$(lsblk -rno UUID "$1" 2>/dev/null)"
[PARTLABEL]="$(mangle "$(lsblk -rno PARTLABEL "$1" 2>/dev/null)")"
[PARTUUID]="$(lsblk -rno PARTUUID "$1" 2>/dev/null)"
)
# bind mounts do not have a UUID!
case $bytag in
'')
[[ $uuid ]] && comment=("UUID=$uuid")
[[ $label ]] && comment+=("LABEL=$(mangle "$label")")
;;
LABEL)
spec=$label
[[ $uuid ]] && comment=("$src" "UUID=$uuid")
;;
UUID)
spec=$uuid
comment=("$src")
[[ $label ]] && comment+=("LABEL=$(mangle "$label")")
;;
*)
[[ $uuid ]] && comment=("$1" "UUID=$uuid")
[[ $label ]] && comment+=("LABEL=$(mangle "$label")")
[[ $bytag ]] && spec=$(lsblk -rno "$bytag" "$1" 2>/dev/null)
;;
esac
[[ $comment ]] && printf '# %s\n' "${comment[*]}"
if [[ $spec ]]; then
printf '%-20s' "$bytag=$(mangle "$spec")"
if [[ ${sources["$bytag"]} ]]; then
spec="$bytag"
comments+=("$src")
else
printf '%-20s' "$(mangle "$src")"
spec=src
fi
for tag in "${!sources[@]}"; do
if [[ ${sources["$tag"]} && "$tag" != "$spec" ]]; then
comments+=("$tag=${sources["$tag"]}")
fi
done
(( ${#comments[@]} )) && printf '# %s\n' "${comments[*]}"
if [[ "$spec" != "src" ]]; then
printf '%-20s' "$spec=${sources["$spec"]}"
else
printf '%-20s' "$src"
fi
}
optstring_apply_quirks() {
local varname=$1 fstype=$2
local fstype="$2"
local -n _optstring="$1"
# SELinux displays a 'seclabel' option in /proc/self/mountinfo. We can't know
# if the system we're generating the fstab for has any support for SELinux (as
# one might install Arch from a Fedora environment), so let's remove it.
optstring_remove_option "$varname" seclabel
optstring_remove_options _optstring seclabel
# Prune 'relatime' option for any pseudofs. This seems to be a rampant
# default which the kernel often exports even if the underlying filesystem
# doesn't support it. Example: https://bugs.archlinux.org/task/54554.
if awk -v fstype="$fstype" '$1 == fstype { exit 1 }' /proc/filesystems; then
optstring_remove_option "$varname" relatime
if fstype_is_pseudofs "$fstype"; then
optstring_remove_options _optstring relatime
fi
case $fstype in
@ -63,25 +62,15 @@ optstring_apply_quirks() {
# Having only one of subvol= and subvolid= is enough for mounting a btrfs subvolume
# And having subvolid= set prevents things like 'snapper rollback' to work, as it
# updates the subvolume in-place, leaving subvol= unchanged with a different subvolid.
if optstring_has_option "$varname" subvol; then
optstring_remove_option "$varname" subvolid
if optstring_match_one_option _optstring subvol; then
optstring_remove_options _optstring subvolid
fi
;;
f2fs)
# These are build-time or runtime-unchangeable options for f2fs.
# The former means that kernels supporting the options will only
# provide the negative versions of these (e.g. noacl), and vice versa
# These are Kconfig options for f2fs. Kernels supporting the options will
# only provide the negative versions of these (e.g. noacl), and vice versa
# for kernels without support.
# The latter means that the options can only be specified/changed
# during the initial mount but not remount.
optstring_remove_option "$varname" noacl,acl,nouser_xattr,user_xattr,atgc
;;
vfat)
# Before Linux v3.8, "cp" is prepended to the value of the codepage.
if optstring_get_option "$varname" codepage && [[ $codepage = cp* ]]; then
optstring_remove_option "$varname" codepage
optstring_append_option "$varname" "codepage=${codepage#cp}"
fi
optstring_remove_options _optstring noacl,acl,nouser_xattr,user_xattr
;;
esac
}
@ -96,13 +85,13 @@ usage: ${0##*/} [options] root
-p Exclude pseudofs mounts (default behavior)
-P Include pseudofs mounts
-t <tag> Use TAG for source identifiers (TAG should be one of: LABEL,
UUID, PARTLABEL, PARTUUID)
UUID, PARTLABEL, PARTUUID)
-U Use UUIDs for source identifiers (shortcut for -t UUID)
-h Print this help message
genfstab generates output suitable for addition to an fstab file based on the
devices mounted under the mountpoint specified by the given root.
genfstab generates output suitable for addition to an fstab file based on
the devices mounted under the mountpoint specified by the given root.
EOF
}
@ -114,15 +103,12 @@ fi
while getopts ':f:LPpt:U' flag; do
case $flag in
f)
prefixfilter="$OPTARG"
;;
L)
bytag=LABEL
;;
U)
bytag=UUID
;;
f)
prefixfilter=$OPTARG
;;
P)
pseudofs=1
;;
@ -130,41 +116,46 @@ while getopts ':f:LPpt:U' flag; do
pseudofs=0
;;
t)
bytag=${OPTARG^^}
bytag="${OPTARG^^}"
;;
U)
bytag=UUID
;;
:)
die '%s: option requires an argument -- '\''%s'\' "${0##*/}" "$OPTARG"
die "%s: option requires an argument -- '%s'" "${0##*/}" "$OPTARG"
;;
?)
die '%s: invalid option -- '\''%s'\' "${0##*/}" "$OPTARG"
die "%s: invalid option -- '%s'" "${0##*/}" "$OPTARG"
;;
esac
done
shift $(( OPTIND - 1 ))
(( $# )) || die "No root directory specified"
root=$(realpath -mL "$1"); shift
(( $# )) || die 'No root directory specified'
root="$(realpath -mL "$1")"; shift
if ! mountpoint -q "$root"; then
die "$root is not a mountpoint"
die '%s: not a mountpoint' "$root"
fi
# handle block devices
while read -r src target fstype opts fsroot; do
if (( !pseudofs )) && fstype_is_pseudofs "$fstype"; then
findmnt -Recvruno SOURCE,TARGET,FSTYPE,OPTIONS,FSROOT "$root" |
while read -r src target fstype opts fsroot; do
if (( ! pseudofs )) && fstype_is_pseudofs "$fstype"; then
continue
fi
[[ $target = "$prefixfilter"* ]] || continue
[[ "$target" = "$prefixfilter"* ]] || continue
# default 5th and 6th columns
dump=0 pass=2
src=$(unmangle "$src")
target=$(unmangle "$target")
target=${target#$root}
src="$(unmangle "$src")"
target="$(unmangle "$target")"
target="${target#"$root/"}"
[[ "$target" != '/'* ]] && target="/$target"
if (( !foundroot )) && findmnt "$src" "$root" >/dev/null; then
if (( ! foundroot )) && findmnt "$src" "$root" >/dev/null; then
# this is root. we can't possibly have more than one...
pass=1 foundroot=1
fi
@ -174,19 +165,29 @@ while read -r src target fstype opts fsroot; do
pass=0
fi
if [[ $fsroot != / && $fstype != btrfs ]]; then
if [[ "$fsroot" != "/" && "$fstype" != "btrfs" ]]; then
# it's a bind mount
src=$(findmnt -funcevo TARGET "$src")$fsroot
src="/${src#$root/}"
if [[ $src -ef $target ]]; then
mapfile -t bind_srcs < <(findmnt -uncevo TARGET "$src")
for bind_src in "${bind_srcs[@]}"; do
if [[ -d "$bind_src$fsroot" ]]; then
src="$bind_src$fsroot"
break
fi
done
src="${src#"$root/"}"
[[ "$src" != '/'* ]] && src="/$src"
if [[ "$src" = "$target" ]]; then
# hrmm, this is weird. we're probably looking at a file or directory
# that was bound into a chroot from the host machine. Ignore it,
# because this won't actually be a valid mount. Worst case, the user
# just re-adds it.
continue
fi
fstype=none
opts+=,bind
opts+=',bind'
pass=0
fi
@ -195,44 +196,41 @@ while read -r src target fstype opts fsroot; do
fuseblk)
# well-behaved FUSE filesystems will report themselves as fuse.$fstype.
# this is probably NTFS-3g, but let's just make sure.
if ! newtype=$(lsblk -no FSTYPE "$src") || [[ -z $newtype ]]; then
if ! newtype="$(lsblk -no FSTYPE "$src")" || [[ -z $newtype ]]; then
# avoid blanking out fstype, leading to an invalid fstab
error 'Failed to derive real filesystem type for FUSE device on %s' "$target"
else
fstype=$newtype
fstype="$newtype"
fi
;;
esac
optstring_apply_quirks "opts" "$fstype"
optstring_apply_quirks opts "$fstype"
# write one line
write_source "$src"
printf '\t%-10s' "/$(mangle "${target#/}")" "$fstype" "$opts"
printf '\t%-10s' "$(mangle "$target")" "$fstype" "$opts"
printf '\t%s %s' "$dump" "$pass"
printf '\n\n'
done < <(findmnt -Recvruno SOURCE,TARGET,FSTYPE,OPTIONS,FSROOT "$root")
done
# handle swaps devices
{
# ignore header
read
read -r
while read -r device type _ _ prio; do
options=defaults
if (( prio >= 0 )); then
options+=,pri=$prio
options+=",pri=$prio"
fi
# skip files marked deleted by the kernel
[[ $device = *'\040(deleted)' ]] && continue
[[ "$device" = *'\040(deleted)' ]] && continue
# skip devices not part of the prefix
[[ $device = "$prefixfilter"* ]] || continue
if [[ $type = file ]]; then
printf '%-20s' "${device#${root%/}}"
elif [[ $device = /dev/dm-+([0-9]) ]]; then
if [[ "$type" = "file" ]]; then
printf '%-20s' "${device#"${root%/}"}"
elif [[ "$device" = "/dev/dm-"+([0-9]) ]]; then
# device mapper doesn't allow characters we need to worry
# about being mangled, and it does the escaping of dashes
# for us in sysfs.
@ -241,6 +239,8 @@ done < <(findmnt -Recvruno SOURCE,TARGET,FSTYPE,OPTIONS,FSROOT "$root")
write_source "$(unmangle "$device")"
fi
printf '\t%-10s\t%-10s\t%-10s\t0 0\n\n' 'none' 'swap' "$options"
printf '\t%-10s\t%-10s\t%-10s\t0 0\n\n' none swap "$options"
done
} </proc/swaps
# vim: et ts=2 sw=2 ft=sh:

View File

@ -1,5 +1,4 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-only
#
# Assumptions:
@ -11,17 +10,20 @@
shopt -s extglob
# m4_include() is recognized as a function definition
# shellcheck source=common disable=SC1073,SC1065,SC1064,SC1072
m4_include(common)
hostcache=0
copykeyring=1
initkeyring=0
copymirrorlist=1
pacman_args=()
pacmode=-Sy
unshare=0
copyconf=0
copyconfig=0
pacman_config=/etc/pacman.conf
m4_include(common)
pacman_args=()
pacmode='-Sy'
setup=chroot_setup
unshare=0
usage() {
cat <<EOF
@ -37,55 +39,16 @@ usage: ${0##*/} [options] root [packages...]
-M Avoid copying the host's mirrorlist to the target
-N Run in unshare mode as a regular user
-P Copy the host's pacman config to the target
-U Use pacman -U to install packages
-U Use 'pacman -U' to install packages
-h Print this help message
pacstrap installs packages to the specified new root directory. If no packages
are given, pacstrap defaults to the "base" group.
pacstrap installs packages to the specified new root directory.
If no packages are given, pacstrap defaults to the 'base' metapackage.
EOF
}
pacstrap() {
(( EUID == 0 )) || die 'This script must be run with root privileges'
# create obligatory directories
msg 'Creating install root at %s' "$newroot"
# shellcheck disable=SC2174 # permissions are perfectly fine here
mkdir -m 0755 -p "$newroot"/var/{cache/pacman/pkg,lib/pacman,log} "$newroot"/{dev,run,etc/pacman.d}
# shellcheck disable=SC2174 # permissions are perfectly fine here
mkdir -m 1777 -p "$newroot"/tmp
# shellcheck disable=SC2174 # permissions are perfectly fine here
mkdir -m 0555 -p "$newroot"/{sys,proc}
# mount API filesystems
$setup "$newroot" || die "failed to setup chroot %s" "$newroot"
if [[ ! -d $newroot/etc/pacman.d/gnupg ]]; then
if (( initkeyring )); then
pacman-key --gpgdir "$newroot"/etc/pacman.d/gnupg --init
elif (( copykeyring )) && [[ -d /etc/pacman.d/gnupg ]]; then
# if there's a keyring on the host, copy it into the new root
cp -a --no-preserve=ownership /etc/pacman.d/gnupg "$newroot/etc/pacman.d/"
fi
fi
msg 'Installing packages to %s' "$newroot"
if ! $pid_unshare pacman -r "$newroot" "${pacman_args[@]}"; then
die 'Failed to install packages to new root'
fi
if (( copymirrorlist )); then
# install the host's mirrorlist onto the new root
cp -a /etc/pacman.d/mirrorlist "$newroot/etc/pacman.d/"
fi
if (( copyconf )); then
cp -a "$pacman_config" "$newroot/etc/pacman.conf"
fi
}
if [[ -z $1 || $1 = @(-h|--help) ]]; then
usage
exit $(( $# ? 0 : 1 ))
@ -94,20 +57,20 @@ fi
while getopts ':C:cDGiKMNPU' flag; do
case $flag in
C)
pacman_config=$OPTARG
;;
D)
pacman_args+=(-dd)
pacman_config="$OPTARG"
;;
c)
hostcache=1
;;
i)
interactive=1
D)
pacman_args+=(-dd)
;;
G)
copykeyring=0
;;
i)
interactive=1
;;
K)
initkeyring=1
;;
@ -115,53 +78,96 @@ while getopts ':C:cDGiKMNPU' flag; do
copymirrorlist=0
;;
N)
setup=unshare_setup
unshare=1
;;
P)
copyconf=1
copyconfig=1
;;
U)
pacmode=-U
pacmode='-U'
;;
:)
die '%s: option requires an argument -- '\''%s'\' "${0##*/}" "$OPTARG"
die "%s: option requires an argument -- '%s'" "${0##*/}" "$OPTARG"
;;
?)
die '%s: invalid option -- '\''%s'\' "${0##*/}" "$OPTARG"
die "%s: invalid option -- '%s'" "${0##*/}" "$OPTARG"
;;
esac
done
shift $(( OPTIND - 1 ))
(( $# )) || die "No root directory specified"
newroot=$1
shift
(( $# )) || die 'No root directory specified'
newroot="$1"; shift
[[ -d "$newroot" ]] || die '%s: not a directory' "$newroot"
[[ -d $newroot ]] || die "%s is not a directory" "$newroot"
tmpfile="$(mktemp -t pacman.conf.XXXX)"
cp "$pacman_config" "$tmpfile"
sed -i 's/^DownloadUser/#&/' "$tmpfile"
pacman_config="$tmpfile"
pacman_args+=("$pacmode" "${@:-base}" --config="$pacman_config" --disable-sandbox)
if (( ! hostcache )); then
pacman_args+=(--cachedir="$newroot/var/cache/pacman/pkg")
fi
pacman_args+=("$pacmode" "${@:-base}" --config="$pacman_config")
if (( ! interactive )); then
pacman_args+=(--noconfirm)
fi
set -e
gpgdir="$(pacman-conf --config="$pacman_config" GPGDir)"
cachedir="$(pacman-conf --config="$pacman_config" CacheDir)"
if (( copyconfig )); then
dbpath="$newroot$(pacman-conf --config="$pacman_config" DBPath)"
logfile="$newroot$(pacman-conf --config="$pacman_config" LogFile)"
cachedir_target="$newroot$cachedir"
gpgdir_target="$newroot$gpgdir"
pacman_args+=(--dbpath="$dbpath" --logfile="$logfile")
else
dbpath="$newroot"/var/lib/pacman
logfile="$newroot"/var/log/pacman.log
cachedir_target="$newroot"/var/cache/pacman/pkg
gpgdir_target="$newroot"/etc/pacman.d/gnupg
fi
if (( ! hostcache )); then
cachedir="$cachedir_target"
fi
pacman_args+=(--cachedir="$cachedir")
set +e
pacstrap() {
check_root
# create obligatory directories
msg 'Creating install root at %s' "$newroot"
mkdir -m 0755 -p "$dbpath" "$cachedir_target" "$(dirname "$logfile")" "$newroot"/{dev,run,etc/pacman.d}
mkdir -m 1777 -p "$newroot"/tmp
mkdir -m 0555 -p "$newroot"/{sys,proc}
# mount API filesystems
$setup "$newroot"
if [[ ! -d "$gpgdir_target" ]]; then
if (( initkeyring )); then
pacman-key --gpgdir "$gpgdir_target" --init
elif (( copykeyring )) && [[ -d "$gpgdir" ]]; then
# if there's a keyring on the host, copy it into the new root
cp -aT --no-preserve=ownership "$gpgdir" "$gpgdir_target"
fi
fi
msg 'Installing packages to %s' "$newroot"
$pid_unshare pacman --root "$newroot" "${pacman_args[@]}" || die 'Failed to install packages to new root'
if (( copymirrorlist )); then
# install the host's mirrorlist onto the new root
cp -a /etc/pacman.d/mirrorlist "$newroot"/etc/pacman.d/ || warning "Failed to copy the host's mirrorlist to new root"
fi
if (( copyconfig )); then
cp -a "$pacman_config" "$newroot"/etc/pacman.conf
fi
}
if (( unshare )); then
setup=unshare_setup
$mount_unshare bash -c "$(declare_all); pacstrap"
else
setup=chroot_setup
pacstrap
fi
# TODO: There is a trap check on exit. Need to rework the trap handling with
# hook-ins/callbacks to remove aux files
rm "$tmpfile"
# vim: et ts=2 sw=2 ft=sh:

View File

@ -1,6 +1,6 @@
#!/bin/bash
. ./fstab-helpers
. "${1:-./common}"
. ./test/common
ASSERT_streq ' deleted' "$(unmangle "$(mangle ' deleted')")"

View File

@ -1,6 +1,6 @@
#!/bin/bash
. ./fstab-helpers
. "${1:-./common}"
. ./test/common
optstring=rw,relatime,fd=29,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
@ -26,10 +26,8 @@ EXPECT_success optstring_has_option optstring maxproto
EXPECT_failure optstring_get_option optstring proto
EXPECT_success optstring_get_option optstring maxproto
# shellcheck disable=SC2154 # set via the optstring helper above
ASSERT_streq "$maxproto" "5"
EXPECT_success optstring_get_option optstring timeout
# shellcheck disable=SC2154 # set via the optstring helper above
ASSERT_streq "$timeout" "300"
optstring_remove_option optstring pgrp

View File

@ -1,6 +1,6 @@
#!/bin/bash
. ./fstab-helpers
. "${1:-./common}"
. ./test/common
EXPECT_success valid_number_of_base 16 feedfacebeef