#!/usr/bin/env bash set -euE # librechroot # Copyright (C) 2010-2012 Nicolás Reynolds # Copyright (C) 2011-2012 Joshua Ismael Haase Hernández (xihh) # Copyright (C) 2012 Michał Masłowski # Copyright (C) 2012-2017 Luke Shumaker # # 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 . # 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)" . "$(librelib messages)" . "$(librelib chroot/makechrootpkg)" shopt -s nullglob umask 0022 ################################################################################ # Wrappers for files in ${pkglibexecdir}/chroot/ # ################################################################################ readonly _arch_nspawn="$(librelib chroot/arch-nspawn)" readonly _mkarchroot="$(librelib chroot/mkarchroot)" 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 -a '^CARCH=' "$makepkg_conf")" else CARCH="$(uname -m)" fi if [[ "$CARCH" == armv7h ]] && ! setarch armv7l /bin/true 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 -c -xF \ -e 'enabled' \ -e 'interpreter /usr/bin/qemu-arm-static' \ /proc/sys/fs/binfmt_misc/arm 2>/dev/null) -lt 2 ]]; then error 'Cannot cross-compile for ARM on x86' plain 'This requires a binfmt_misc entry for qemu-arm-static.' prose 'Such a binfmt_misc entry is provided by the %s package. If you have it installed, but still see this message, you may need to restart %s.' \ binfmt-qemu-static systemd-binfmt.service return $EXIT_NOTINSTALLED 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[@]}" } ################################################################################ # 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 mountopts mountpoint="$(df -P "$file"|sed '1d;s/.*\s//')" 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 $EXIT_INVALIDARGUMENT; 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 $EXIT_INVALIDARGUMENT;; esac done shift $((OPTIND - 1)) if [[ $# -lt 1 ]]; then error "Must specify a command" usage >&2 return $EXIT_INVALIDARGUMENT fi mode=$1 if ! in_array "$mode" "${commands[@]}"; then error "Unrecognized command: %s" "$mode" usage >&2 return $EXIT_INVALIDARGUMENT 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 $EXIT_INVALIDARGUMENT fi :;; install-file) if [[ $# -lt 1 ]]; then error 'Command `%s` requires at least one file' "$mode" usage >&2 return $EXIT_INVALIDARGUMENT 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 $EXIT_INVALIDARGUMENT fi fi :;; install-name) if [[ $# -lt 1 ]]; then error 'Command `%s` requires at least one package name' "$mode" usage >&2 return $EXIT_INVALIDARGUMENT fi :;; run) if [[ $# -lt 1 ]]; then error 'Command `%s` requires at least one argument' "$mode" usage >&2 return $EXIT_INVALIDARGUMENT fi :;; esac if [[ $mode == help ]]; then usage return $EXIT_SUCCESS fi load_conf chroot.conf 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 $EXIT_NOPERMISSION 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 $EXIT_FAILURE 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 $EXIT_FAILURE 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" sync_chroot "$CHROOTDIR/$CHROOT/root" "$copydir" "$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 delete_chroot "$copydir" fi ;; # Dealing with packages install-file) 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 "$@"