diff options
Diffstat (limited to 'src/chroot-tools/librechroot')
-rwxr-xr-x | src/chroot-tools/librechroot | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/src/chroot-tools/librechroot b/src/chroot-tools/librechroot new file mode 100755 index 0000000..cf564ed --- /dev/null +++ b/src/chroot-tools/librechroot @@ -0,0 +1,509 @@ +#!/usr/bin/env bash +set -euE +# librechroot + +# Copyright (C) 2010-2012 Nicolás Reynolds <fauno@parabola.nu> +# Copyright (C) 2011-2012 Joshua Ismael Haase Hernández (xihh) <hahj87@gmail.com> +# Copyright (C) 2012 Michał Masłowski <mtjm@mtjm.eu> +# Copyright (C) 2012-2016 Luke Shumaker <lukeshu@sbcglobal.net> +# +# License: GNU GPLv2+ +# +# This file is part of Parabola. +# +# Parabola is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# Parabola is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Parabola. If not, see <http://www.gnu.org/licenses/>. + +# HACKING: if a command is added or removed, it must be changed in 4 places: +# - the usage() text +# - the commands=() array +# - the case statement in main() that checks the number of arguments +# - the case statement in main() that runs them + +. "$(librelib conf)" +load_files chroot + +. libremessages + +shopt -s nullglob +umask 0022 + +################################################################################ +# Wrappers for files in ${pkglibexecdir}/chroot/ # +################################################################################ + +readonly _arch_nspawn="$(librelib chroot/arch-nspawn)" +readonly _mkarchroot="$(librelib chroot/mkarchroot)" +readonly _makechrootpkg="$(librelib chroot/makechrootpkg.sh)" + +arch_nspawn_flags=() +sysd_nspawn_flags=() + +hack_arch_nspawn_flags() { + local copydir="$1" + + local makepkg_conf="$copydir/etc/makepkg.conf" + + OPTIND=1 + set -- ${arch_nspawn_flags+"${arch_nspawn_flags[@]}"} + while getopts 'hC:M:c:f:s' arg; do + case "$arg" in + M) makepkg_conf="$OPTARG" ;; + *) :;; + esac + done + + # Detect the architecture of the chroot + local CARCH + if [[ -f "$makepkg_conf" ]]; then + eval $(grep '^CARCH=' "$makepkg_conf") + else + CARCH="$(uname -m)" + fi + + if [[ "$CARCH" == armv7h ]] && ! setarch armv7l 2>/dev/null; then + # We're running an ARM chroot on a non-ARM processor + + # Make sure that qemu-static is set up with binfmt_misc + if [[ $(grep -xF \ + -e 'enabled'\ + -e 'interpreter /usr/bin/qemu-arm-static' \ + /proc/sys/fs/binfmt_misc/arm 2>/dev/null |wc -l) -lt 2 ]]; then + error 'Cannot cross-compile for ARM on x86' + plain 'This requires a binfmt_misc entry for qemu-arm-static,' + plain 'which is provided by the %s package.' binfmt-qemu-static + plain 'If you have this, you may need to restart %s.' systemd-binfmt.service + return 1 + fi + + # Let qemu/binfmt_misc do its thing + arch_nspawn_flags+=(-f /usr/bin/qemu-arm-static -s) + + # The -any packages are built separately for ARM from + # x86, so if we use the same CacheDir as the x86 host, + # then there will be PGP errors. + mkdir -p /var/cache/pacman/pkg-arm + arch_nspawn_flags+=(-c /var/cache/pacman/pkg-arm) + fi +} + +# Usage: arch-nspawn $copydir $cmd... +arch-nspawn() { + local copydir=$1; shift + local cmd=("$@") + + local arch_nspawn_flags=(${arch_nspawn_flags+"${arch_nspawn_flags[@]}"}) + hack_arch_nspawn_flags "$copydir" + + "$_arch_nspawn" \ + ${arch_nspawn_flags+"${arch_nspawn_flags[@]}"} \ + "$copydir" \ + ${sysd_nspawn_flags+"${sysd_nspawn_flags[@]}"} \ + -- \ + "${cmd[@]}" +} + +# Usage: mkarchroot $copydir $pkgs... +mkarchroot() { + local copydir=$1; shift + local pkgs=("$@") + + local arch_nspawn_flags=(${arch_nspawn_flags+"${arch_nspawn_flags[@]}"}) + hack_arch_nspawn_flags "$copydir" + + unshare -m "$_mkarchroot" \ + ${arch_nspawn_flags+"${arch_nspawn_flags[@]}"} \ + "$copydir" \ + "${pkgs[@]}" +} + +# Usage: _makechrootpkg $function $arguments... +# Don't load $_makechrootpkg directly because it doesn't work with -euE +_makechrootpkg() ( + set +euE + . "$_makechrootpkg" + "$@" +) + +################################################################################ +# Utility functions # +################################################################################ + +# Usage: make_empty_repo $copydir +make_empty_repo() { + local copydir=$1 + mkdir -p "${copydir}/repo" + bsdtar -czf "${copydir}/repo/repo.db.tar.gz" -T /dev/null + ln -s "repo.db.tar.gz" "${copydir}/repo/repo.db" +} + +# Usage: chroot_add_to_local_repo $copydir $pkgfiles... +chroot_add_to_local_repo() { + local copydir=$1; shift + mkdir -p "$copydir/repo" + local pkgfile + for pkgfile in "$@"; do + cp "$pkgfile" "$copydir/repo" + pushd "$copydir/repo" >/dev/null + repo-add repo.db.tar.gz "${pkgfile##*/}" + popd >/dev/null + done +} + +# Print code to set $rootdir and $copydir; blank them on error +calculate_directories() { + # Don't assume that CHROOTDIR or CHROOT are set, + # but assume that COPY is set. + local rootdir copydir + + if [[ -n ${CHROOTDIR:-} ]] && [[ -n ${CHROOT:-} ]]; then + rootdir="${CHROOTDIR}/${CHROOT}/root" + else + rootdir='' + fi + + if [[ ${COPY:0:1} = / ]]; then + copydir=$COPY + elif [[ -n ${CHROOTDIR:-} ]] && [[ -n ${CHROOT:-} ]]; then + copydir="${CHROOTDIR}/${CHROOT}/${COPY}" + else + copydir='' + fi + + declare -p rootdir + declare -p copydir +} + +check_mountpoint() { + local file=$1 + local mountpoint="$(df -P "$file"|sed '1d;s/.*\s//')" + local mountopts=($(LC_ALL=C mount|awk "{ if (\$3==\"$mountpoint\") { gsub(/[(,)]/, \" \", \$6); print \$6 } }")) + ! in_array nosuid "${mountopts[@]}" && ! in_array noexec "${mountopts[@]}" +} + +################################################################################ +# Main program # +################################################################################ + +usage() { + eval "$(calculate_directories)" + print "Usage: %s [OPTIONS] COMMAND [ARGS...]" "${0##*/}" + print 'Interacts with an archroot (arch chroot).' + echo + prose 'This is configured with `chroot.conf`, either in + `/etc/libretools.d/`, or `$XDG_CONFIG_HOME/libretools/`. + The variables you may set are $CHROOTDIR, $CHROOT, and + $CHROOTEXTRAPKG.' + echo + prose 'There may be multiple chroots; they are stored in $CHROOTDIR.' + echo + prose 'Each chroot is named; the default is configured with $CHROOT.' + echo + prose 'Each named chroot has a master clean copy (named `root`), and any + number of other named copies; the copy used by default is the + current username (or $SUDO_USER, or `copy` if root).' + echo + prose 'The full path to the chroot copy is "$CHROOTDIR/$CHROOT/$COPY", + unless the copy name is manually specified as an absolute path, + in which case, that path is used.' + echo + prose 'The current settings for the above variables are:' + printf ' CHROOTDIR : %s\n' "${CHROOTDIR:-$(_ 'ERROR: NO SETTING')}" + printf ' CHROOT : %s\n' "${CHROOT:-$(_ 'ERROR: NO SETTING')}" + printf ' COPY : %s\n' "$COPY" + printf ' rootdir : %s\n' "${rootdir:-$(_ 'ERROR')}" + printf ' copydir : %s\n' "${copydir:-$(_ 'ERROR')}" + echo + prose 'If the chroot or copy does not exist, it will be created + automatically. A chroot by default contains the packages in the + group "base-devel" and any packages named in $CHROOTEXTRAPKG. + Unless the `-C` or `-M` flags are used, the configuration files + that this program installs are the stock versions supplied in the + packages, not the versions from your host system. Other tools + (such as libremakepkg) may alter the configuration.' + echo + prose 'This command will make the following configuration changes in the + chroot:' + bullet 'overwrite `/etc/libretools.d/chroot.conf`' + bullet 'overwrite `/etc/pacman.d/mirrorlist`' + bullet 'set `CacheDir` in `/etc/pacman.conf`' + prose 'If a new `pacman.conf` is inserted with the `-C` flag, the change + is made after the file is copied in; the `-C` flag doesn'"'"'t + stop the change from being effective.' + echo + prose 'The processor architecture of the chroot is determined + by the by `CARCH` variable in the `/etc/makepkg.conf` + file inside of the chroot.' + echo + prose 'The `-A CARCH` flag is *almost* simply an alias for' + printf ' %s\n' \ + '-C "/usr/share/pacman/defaults/pacman.conf.$CARCH" \' \ + '-M "/usr/share/pacman/defaults/makepkg.conf.$CARCH"' + prose 'However, before doing that, it actually makes a temporary copy of + `pacman.conf`, and sets the `Architecture` line to match the + `CARCH` line in `makepkg.conf`.' + echo + prose 'Creating a copy, deleting a copy, or syncing a copy can be fairly + slow; but are very fast if $CHROOTDIR is on a btrfs partition.' + echo + print 'Options:' + flag "-n <$(_ CHROOT)>" 'Name of the chroot to use' + flag "-l <$(_ COPY)>" 'Name of, or absolute path to, the copy to use' + flag '-N' 'Disable networking in the chroot' + flag "-C <$(_ FILE)>" 'Copy this file to `$copydir/etc/pacman.conf`' + flag "-M <$(_ FILE)>" 'Copy this file to `$copydir/etc/makepkg.conf`' + flag "-A <$(_ CARCH)>" 'Set the architecture of the copy; simply an + alias for the `-C` and `-M` flags, see above.' + flag "-w <$(_ 'PATH[:PATH]')>" 'Bind mount a file or directory, read/write' + flag "-r <$(_ 'PATH[:PATH]')>" 'Bind mount a file or directory, read-only' + echo + print 'Commands:' + print ' Create/copy/delete:' + flag 'noop|make' 'Do not do anything, but still creates the chroot + copy if it does not exist' + flag 'sync' 'Sync the copy with the clean (`root`) copy' + flag 'delete' 'Delete the chroot copy' + print ' Dealing with packages:' + flag "install-file $(_ FILES...)" 'Like `pacman -U FILES...`' + flag "install-name $(_ NAMES...)" 'Like `pacman -S NAMES...`' + flag 'update' 'Like `pacman -Syu`' + flag 'clean-pkgs' 'Remove all packages from the chroot copy that + are not in base-devel, $CHROOTEXTRAPKG, or named + as a dependency in the file `/startdir/PKGBUILD` + in the chroot copy' + print ' Other:' + flag "run $(_ CMD...)" 'Run CMD in the chroot copy' + flag 'enter' 'Enter an interactive shell in the chroot copy' + flag 'clean-repo' 'Clean /repo in the chroot copy' + flag 'help' 'Show this message' +} +readonly commands=( + noop make sync delete + install-file install-name update clean-pkgs + run enter clean-repo help +) + +# Globals: $CHROOTDIR, $CHROOT, $COPY, $rootdir and $copydir +main() { + COPY=$LIBREUSER + [[ $COPY != root ]] || COPY=copy + + local mode=enter + while getopts 'n:l:NC:M:A:w:r:' opt; do + case $opt in + n) CHROOT=$OPTARG;; + l) COPY=$OPTARG;; + N) sysd_nspawn_flags+=(--private-network);; + C|M) arch_nspawn_flags+=(-$opt "$OPTARG");; + A) + if ! [[ -f "/usr/share/pacman/defaults/pacman.conf.$OPTARG" && -f "/usr/share/pacman/defaults/makepkg.conf.$OPTARG" ]]; then + error 'Unsupported architecture: %s' "$OPTARG" + plain 'See the files in %q for valid architectures.' /usr/share/pacman/defaults/ + return 1; + fi + trap 'rm -f -- "$tmppacmanconf"' EXIT + tmppacmanconf="$(mktemp --tmpdir librechroot-pacman.conf.XXXXXXXXXX)" + < "/usr/share/pacman/defaults/pacman.conf.$OPTARG" sed -r "s|^#?\\s*Architecture.+|Architecture = ${OPTARG}|g" > "$tmppacmanconf" + arch_nspawn_flags+=( + -C "$tmppacmanconf" + -M "/usr/share/pacman/defaults/makepkg.conf.$OPTARG" + );; + w) sysd_nspawn_flags+=("--bind=$OPTARG");; + r) sysd_nspawn_flags+=("--bind-ro=$OPTARG");; + *) usage >&2; return 1;; + esac + done + shift $(($OPTIND - 1)) + if [[ $# -lt 1 ]]; then + error "Must specify a command" + usage >&2 + return 1 + fi + mode=$1 + if ! in_array "$mode" "${commands[@]}"; then + error "Unrecognized command: %s" "$mode" + usage >&2 + return 1 + fi + shift + case "$mode" in + noop|make|sync|delete|update|enter|clean-pkgs|clean-repo) + if [[ $# -gt 0 ]]; then + error 'Command `%s` does not take any arguments: %s' "$mode" "$*" + usage >&2 + return 1 + fi + :;; + install-file) + if [[ $# -lt 1 ]]; then + error 'Command `%s` requires at least one file' "$mode" + usage >&2 + return 1 + else + local missing=() + local file + for file in "$@"; do + if ! [[ -f $file ]]; then + missing+=("$file") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + error "%s: file(s) not found: %s" "$mode" "${missing[*]}" + return 1 + fi + fi + :;; + install-name) + if [[ $# -lt 1 ]]; then + error 'Command `%s` requires at least one package name' "$mode" + usage >&2 + return 1 + fi + :;; + run) + if [[ $# -lt 1 ]]; then + error 'Command `%s` requires at least one argument' "$mode" + usage >&2 + return 1 + fi + :;; + esac + + + if [[ $mode == help ]]; then + usage + return 0 + fi + + check_vars chroot CHROOTDIR CHROOT + eval "$(calculate_directories)" + + readonly LIBREUSER LIBREHOME + readonly CHROOTDIR CHROOT COPY + readonly rootdir copydir + readonly mode + + ######################################################################## + + if (( EUID )); then + error "This program must be run as root." + return 1 + fi + + umask 0022 + + # XXX: SYSTEMD-STDIN HACK + if ! [[ -t 0 ]]; then + error "Input is not a TTY" + plain "https://labs.parabola.nu/issues/431" + plain "https://bugs.freedesktop.org/show_bug.cgi?id=70290" + prose "Due to a bug in systemd-nspawn, redirecting stdin is not + supported." >&2 + return 1 + fi + + # Keep this lock for as long as we are running + # Note that '9' is the same FD number as in mkarchroot et al. + lock 9 "$copydir.lock" \ + "Waiting for existing lock on chroot copy to be released: [%s]" "$COPY" + + if [[ $mode != delete ]]; then + if ! check_mountpoint "$copydir.lock"; then + error "Chroot copy is mounted with nosuid or noexec options: [%s]" "$COPY" + return 1 + fi + + if [[ ! -d $rootdir ]]; then + msg "Creating 'root' copy for chroot [%s]" "$CHROOT" + mkarchroot "$rootdir" base-devel + make_empty_repo "$rootdir" + fi + + if [[ ! -d $copydir ]] || [[ $mode == sync ]]; then + msg "Syncing copy [%s] with root copy" "$COPY" + _makechrootpkg sync_chroot "$CHROOTDIR/$CHROOT" "$COPY" + fi + + # Note: the in-chroot pkgconfdir is non-configurable, this is + # intentionally hard-coded. + mkdir -p "$copydir/etc/libretools.d" + { + if [[ ${#CHROOTEXTRAPKG[*]} -eq 0 ]]; then + echo 'CHROOTEXTRAPKG=()' + else + printf 'CHROOTEXTRAPKG=(' + printf '%q ' "${CHROOTEXTRAPKG[@]}" + printf ')\n' + fi + } > "$copydir"/etc/libretools.d/chroot.conf + + # "touch" the chroot first + # this will + # - overwrite '/etc/pacman.d/mirrorlist'" + # - set 'CacheDir' in \`/etc/pacman.conf'" + # - apply -C or -M flags + arch-nspawn "$copydir" true + trap EXIT # clear the trap to remove the tmp pacman.conf from -A + arch_nspawn_flags=() # XXX dirty hack, don't apply -C or -M again + fi + + ######################################################################## + + case "$mode" in + # Creat/copy/delete + noop|make|sync) :;; + delete) + if [[ -d $copydir ]]; then + _makechrootpkg delete_chroot "$copydir" + fi + ;; + + # Dealing with packages + install-file) + _makechrootpkg install_packages "$copydir" "$@" + chroot_add_to_local_repo "$copydir" "$@" + ;; + install-name) + arch-nspawn "$copydir" pacman -Sy -- "$@" + ;; + update) + arch-nspawn "$copydir" pacman -Syu --noconfirm + ;; + clean-pkgs) + trap "rm -f -- $(printf '%q ' "$copydir"/{bin/chcleanup,chrootexec})" EXIT + install -m755 "$(librelib chroot/chcleanup)" "$copydir/bin/chcleanup" + printf '%s\n' \ + '#!/bin/bash' \ + 'mkdir -p /startdir' \ + 'cd /startdir' \ + '/bin/chcleanup' \ + > "$copydir/chrootexec" + chmod 755 "$copydir/chrootexec" + arch-nspawn "$copydir" /chrootexec + ;; + + # Other + run) + arch-nspawn "$copydir" "$@" + ;; + enter) + arch-nspawn "$copydir" bash + ;; + clean-repo) + rm -rf "${copydir}"/repo/* + make_empty_repo "$copydir" + ;; + esac +} + +main "$@" |