Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions arch-chroot.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ shopt -s extglob

m4_include(common)

setup=chroot_setup
unshare="$root_unshare"

usage() {
cat <<EOF
usage: ${0##*/} chroot-dir [command]

-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.
Expand Down Expand Up @@ -51,12 +55,16 @@ chroot_add_resolv_conf() {
chroot_add_mount /etc/resolv.conf "$resolv_conf" --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
;;
Expand All @@ -70,21 +78,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"
71 changes: 71 additions & 0 deletions common
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,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
Expand Down
2 changes: 1 addition & 1 deletion completion/arch-chroot.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion completion/pacstrap.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions doc/arch-chroot.8.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions doc/pacstrap.8.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
57 changes: 34 additions & 23 deletions pacstrap.in
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ hostcache=0
copykeyring=1
copymirrorlist=1
pacmode=-Sy
setup=chroot_setup
unshare="$root_unshare"

usage() {
cat <<EOF
Expand All @@ -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
Expand All @@ -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
Expand All @@ -64,6 +65,10 @@ while getopts ':C:cdGiMU' flag; do
M)
copymirrorlist=0
;;
N)
setup=unshare_setup
unshare="$user_unshare"
;;
U)
pacmode=-U
;;
Expand Down Expand Up @@ -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: