#!/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)" shopt -s nullglob umask 0022 load_files chroot ################################################################################ # 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 '^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 -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.' 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 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 "$@"