diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | arch-chroot.in | 103 | ||||
| -rw-r--r-- | common | 75 | ||||
| -rw-r--r-- | completion/arch-chroot.bash | 2 | ||||
| -rw-r--r-- | completion/pacstrap.bash | 2 | ||||
| -rw-r--r-- | doc/arch-chroot.8.asciidoc | 7 | ||||
| -rw-r--r-- | doc/pacstrap.8.asciidoc | 5 | ||||
| -rw-r--r-- | pacstrap.in | 57 |
8 files changed, 189 insertions, 63 deletions
@@ -9,6 +9,7 @@ tasks when installing [Arch Linux](https://www.archlinux.org). * util-linux (>= 2.23) * POSIX awk * bash (>= 4.1) +* asciidoc (for generating man pages) ## License diff --git a/arch-chroot.in b/arch-chroot.in index fd6140e..bcb38df 100644 --- a/arch-chroot.in +++ b/arch-chroot.in @@ -4,11 +4,15 @@ shopt -s extglob m4_include(common) +setup=chroot_setup +unshare="$root_unshare" + usage() { cat <<EOF -usage: ${0##*/} chroot-dir [command] +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 If 'command' is unspecified, ${0##*/} will launch /bin/bash. @@ -23,40 +27,63 @@ 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 resolv_conf=$1/etc/resolv.conf - - [[ -e /etc/resolv.conf ]] || return 0 - - # Handle resolv.conf as a symlink to somewhere else. - if [[ -L $chrootdir/etc/resolv.conf ]]; then - # readlink(1) should always give us *something* since we know at this point - # it's a symlink. For simplicity, ignore the case of nested symlinks. - resolv_conf=$(readlink "$chrootdir/etc/resolv.conf") - if [[ $resolv_conf = /* ]]; then - resolv_conf=$chrootdir$resolv_conf - else - resolv_conf=$chrootdir/etc/$resolv_conf - fi - - # ensure file exists to bind mount over - if [[ ! -f $resolv_conf ]]; then - install -Dm644 /dev/null "$resolv_conf" || return 1 - fi - elif [[ ! -e $chrootdir/etc/resolv.conf ]]; then - # The chroot might not have a resolv.conf. - return 0 + local chrootdir=$1 + local src=$(resolve_link /etc/resolv.conf) + local dest=$(resolve_link "$chrootdir/etc/resolv.conf" "$chrootdir") + + # If we don't have a source resolv.conf file, there's nothing useful we can do. + [[ -e $src ]] || return 0 + + if [[ ! -e $dest ]]; then + # There are two reasons the destination might not exist: + # + # 1. There may be no resolv.conf in the chroot. In this case, $dest won't exist, + # and it will be equal to $1/etc/resolv.conf. In this case, we'll just exit. + # The chroot environment must not be concerned with DNS resolution. + # + # 2. $1/etc/resolv.conf is (or resolves to) a broken link. The environment + # clearly intends to handle DNS resolution, but something's wrong. Maybe it + # normally creates the target at boot time. We'll (try to) take care of it by + # creating a dummy file at the target, so that we have something to bind to. + + # Case 1. + [[ $dest = $chrootdir/etc/resolv.conf ]] && return 0 + + # Case 2. + install -Dm644 /dev/null "$dest" || return 1 fi - chroot_add_mount /etc/resolv.conf "$resolv_conf" --bind + chroot_add_mount "$src" "$dest" --bind } -while getopts ':hu:' flag; do +while getopts ':hNu:' flag; do case $flag in h) usage exit 0 ;; + N) + setup=unshare_setup + unshare="$user_unshare" + ;; u) userspec=$OPTARG ;; @@ -70,21 +97,27 @@ while getopts ':hu:' flag; do done shift $(( OPTIND - 1 )) -(( EUID == 0 )) || die 'This script must be run with root privileges' (( $# )) || die 'No chroot directory specified' chrootdir=$1 shift -[[ -d $chrootdir ]] || die "Can't create chroot on non-directory %s" "$chrootdir" +arch-chroot() { + (( EUID == 0 )) || die 'This script must be run with root privileges' + + [[ -d $chrootdir ]] || die "Can't create chroot on non-directory %s" "$chrootdir" -if ! mountpoint -q "$chrootdir"; then - warning "$chrootdir is not a mountpoint. This may have undesirable side effects." -fi + $setup "$chrootdir" || die "failed to setup chroot %s" "$chrootdir" + chroot_add_resolv_conf "$chrootdir" || die "failed to setup resolv.conf" -chroot_setup "$chrootdir" || die "failed to setup chroot %s" "$chrootdir" -chroot_add_resolv_conf "$chrootdir" || die "failed to setup resolv.conf" + 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") + chroot_args=() + [[ $userspec ]] && chroot_args+=(--userspec "$userspec") + + SHELL=/bin/bash chroot "${chroot_args[@]}" -- "$chrootdir" "${args[@]}" +} -SHELL=/bin/bash unshare --fork --pid chroot "${chroot_args[@]}" -- "$chrootdir" "$@" +args=("$@") +$unshare bash -c "$(declare_all); arch-chroot" @@ -39,6 +39,7 @@ declare -A fsck_types=([cramfs]=1 [ext3]=1 [ext4]=1 [ext4dev]=1 + [f2fs]=1 [jfs]=1 [minix]=1 [msdos]=1 @@ -89,7 +90,7 @@ chroot_setup() { chroot_add_mount udev "$1/dev" -t devtmpfs -o mode=0755,nosuid && chroot_add_mount devpts "$1/dev/pts" -t devpts -o mode=0620,gid=5,nosuid,noexec && chroot_add_mount shm "$1/dev/shm" -t tmpfs -o mode=1777,nosuid,nodev && - chroot_add_mount /run "$1/run" --bind && + chroot_add_mount run "$1/run" -t tmpfs -o nosuid,nodev,mode=0755 && chroot_add_mount tmp "$1/tmp" -t tmpfs -o mode=1777,strictatime,nodev,nosuid } @@ -100,6 +101,77 @@ chroot_teardown() { unset CHROOT_ACTIVE_MOUNTS } +chroot_add_mount_lazy() { + mount "$@" && CHROOT_ACTIVE_LAZY=("$2" "${CHROOT_ACTIVE_LAZY[@]}") +} + +chroot_bind_device() { + touch "$2" && CHROOT_ACTIVE_FILES=("$2" "${CHROOT_ACTIVE_FILES[@]}") + chroot_add_mount $1 "$2" --bind +} + +chroot_add_link() { + ln -sf "$1" "$2" && CHROOT_ACTIVE_FILES=("$2" "${CHROOT_ACTIVE_FILES[@]}") +} + +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_add_mount_lazy "$1" "$1" --bind && + chroot_add_mount proc "$1/proc" -t proc -o nosuid,noexec,nodev && + chroot_add_mount_lazy /sys "$1/sys" --rbind && + chroot_add_link "$1/proc/self/fd" "$1/dev/fd" && + chroot_add_link "$1/proc/self/fd/0" "$1/dev/stdin" && + chroot_add_link "$1/proc/self/fd/1" "$1/dev/stdout" && + chroot_add_link "$1/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 run "$1/run" -t tmpfs -o nosuid,nodev,mode=0755 && + chroot_add_mount tmp "$1/tmp" -t tmpfs -o mode=1777,strictatime,nodev,nosuid +} + +unshare_teardown() { + chroot_teardown + + if (( ${#CHROOT_ACTIVE_LAZY[@]} )); then + umount --lazy "${CHROOT_ACTIVE_LAZY[@]}" + fi + unset CHROOT_ACTIVE_LAZY + + if (( ${#CHROOT_ACTIVE_FILES[@]} )); then + rm "${CHROOT_ACTIVE_FILES[@]}" + fi + unset CHROOT_ACTIVE_FILES +} + +root_unshare="unshare --fork --pid" +user_unshare="$root_unshare --mount --map-auto --map-root-user --setuid 0 --setgid 0" + +# This outputs code for declaring all variables to stdout. For example, if +# FOO=BAR, then running +# declare -p FOO +# will result in the output +# declare -- FOO="bar" +# This function may be used to re-declare all currently used variables and +# functions in a new shell. +declare_all() { + # Remove read-only variables to avoid warnings. Unfortunately, declare +r -p + # doesn't work like it looks like it should (declaring only read-write + # variables). However, declare -rp will print out read-only variables, which + # we can then use to remove those definitions. + declare -p | grep -Fvf <(declare -rp) + # Then declare functions + declare -pf +} + try_cast() ( _=$(( $1#$2 )) ) 2>/dev/null @@ -243,7 +315,6 @@ dm_name_for_devnode() { else # don't leave the caller hanging, just print the original name # along with the failure. - print '%s' "$1" error 'Failed to resolve device mapper name for: %s' "$1" fi } diff --git a/completion/arch-chroot.bash b/completion/arch-chroot.bash index 707208a..583bd8f 100644 --- a/completion/arch-chroot.bash +++ b/completion/arch-chroot.bash @@ -2,7 +2,7 @@ _arch_chroot() { compopt +o dirnames local cur prev opts i _init_completion -n : || return - opts="-u -h" + opts="-N -u -h" for i in "${COMP_WORDS[@]:1:COMP_CWORD-1}"; do if [[ -d ${i} ]]; then diff --git a/completion/pacstrap.bash b/completion/pacstrap.bash index fb948f0..a77cb04 100644 --- a/completion/pacstrap.bash +++ b/completion/pacstrap.bash @@ -8,7 +8,7 @@ _pacstrap() { COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts="-C -c -G -i -M -h" + opts="-C -c -G -i -M -N -h" for i in "${COMP_WORDS[@]:1:COMP_CWORD-1}"; do if [[ -d ${i} ]]; then diff --git a/doc/arch-chroot.8.asciidoc b/doc/arch-chroot.8.asciidoc index 5586a46..a361043 100644 --- a/doc/arch-chroot.8.asciidoc +++ b/doc/arch-chroot.8.asciidoc @@ -7,7 +7,7 @@ arch-chroot - enhanced chroot command Synopsis -------- -arch-chroot [options] chroot-dir [command] +arch-chroot [options] chroot-dir [command] [arguments...] Description ----------- @@ -32,6 +32,11 @@ i.e.: 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. diff --git a/doc/pacstrap.8.asciidoc b/doc/pacstrap.8.asciidoc index d3d517c..6eed23e 100644 --- a/doc/pacstrap.8.asciidoc +++ b/doc/pacstrap.8.asciidoc @@ -37,6 +37,11 @@ Options *-M*:: Avoid copying the host's mirrorlist to the target. +*-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*:: Use pacman -U to install packages. Useful for obtaining fine-grained control over the installed packages. diff --git a/pacstrap.in b/pacstrap.in index 703df11..9ffe17c 100644 --- a/pacstrap.in +++ b/pacstrap.in @@ -16,6 +16,8 @@ hostcache=0 copykeyring=1 copymirrorlist=1 pacmode=-Sy +setup=chroot_setup +unshare="$root_unshare" usage() { cat <<EOF @@ -27,6 +29,7 @@ usage: ${0##*/} [options] root [packages...] -G Avoid copying the host's pacman keyring to the target -i Prompt for package confirmation when needed (run interactively) -M Avoid copying the host's mirrorlist to the target + -N Run in unshare mode as a regular user -U Use pacman -U to install packages -h Print this help message @@ -42,9 +45,7 @@ if [[ -z $1 || $1 = @(-h|--help) ]]; then exit $(( $# ? 0 : 1 )) fi -(( EUID == 0 )) || die 'This script must be run with root privileges' - -while getopts ':C:cdGiMU' flag; do +while getopts ':C:cdGiMNU' flag; do case $flag in C) pacman_config=$OPTARG @@ -64,6 +65,10 @@ while getopts ':C:cdGiMU' flag; do M) copymirrorlist=0 ;; + N) + setup=unshare_setup + unshare="$user_unshare" + ;; U) pacmode=-U ;; @@ -95,30 +100,36 @@ fi [[ -d $newroot ]] || die "%s is not a directory" "$newroot" -# create obligatory directories -msg 'Creating install root at %s' "$newroot" -mkdir -m 0755 -p "$newroot"/var/{cache/pacman/pkg,lib/pacman,log} "$newroot"/{dev,run,etc/pacman.d} -mkdir -m 1777 -p "$newroot"/tmp -mkdir -m 0555 -p "$newroot"/{sys,proc} +pacstrap() { + (( EUID == 0 )) || die 'This script must be run with root privileges' + + # create obligatory directories + msg 'Creating install root at %s' "$newroot" + mkdir -m 0755 -p "$newroot"/var/{cache/pacman/pkg,lib/pacman,log} "$newroot"/{dev,run,etc/pacman.d} + mkdir -m 1777 -p "$newroot"/tmp + mkdir -m 0555 -p "$newroot"/{sys,proc} -# mount API filesystems -chroot_setup "$newroot" || die "failed to setup chroot %s" "$newroot" + # mount API filesystems + $setup "$newroot" || die "failed to setup chroot %s" "$newroot" -if (( copykeyring )); then - # if there's a keyring on the host, copy it into the new root, unless it exists already - if [[ -d /etc/pacman.d/gnupg && ! -d $newroot/etc/pacman.d/gnupg ]]; then - cp -a /etc/pacman.d/gnupg "$newroot/etc/pacman.d/" + if (( copykeyring )); then + # if there's a keyring on the host, copy it into the new root, unless it exists already + if [[ -d /etc/pacman.d/gnupg && ! -d $newroot/etc/pacman.d/gnupg ]]; then + cp -a --no-preserve=ownership /etc/pacman.d/gnupg "$newroot/etc/pacman.d/" + fi fi -fi -msg 'Installing packages to %s' "$newroot" -if ! unshare --fork --pid pacman -r "$newroot" $pacmode "${pacman_args[@]}"; then - die 'Failed to install packages to new root' -fi + msg 'Installing packages to %s' "$newroot" + if ! pacman -r "$newroot" $pacmode "${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 (( copymirrorlist )); then + # install the host's mirrorlist onto the new root + cp -a /etc/pacman.d/mirrorlist "$newroot/etc/pacman.d/" + fi +} + +$unshare bash -c "$(declare_all); pacstrap" # vim: et ts=2 sw=2 ft=sh: |
