#!/usr/bin/env bash set -euE # librechroot # Copyright (C) 2010 Nicolás Reynolds # Copyright (C) 2011 Joshua Haase # Copyright (C) 2012-2014 Luke Shumaker # # 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 3 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) load_files chroot . libremessages shopt -s nullglob umask 0022 readonly _arch_nspawn=$(librelib chroot/arch-nspawn) readonly _mkarchroot=$(librelib chroot/mkarchroot) readonly _makechrootpkg=$(librelib chroot/makechrootpkg.sh) # Because the makechrootpkg.sh library functions don't work with -euE _makechrootpkg() ( set +euE . "$_makechrootpkg" "$@" ) # 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 } 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 '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 "-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 ) # 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[@]}" } arch_nspawn_flags=() sysd_nspawn_flags=() arch-nspawn() { local copydir=$1; shift # XXX: SYSTEMD-STDOUT HACK if [[ -t 1 ]]; then cmd=("$@") else # This perl script is similar to `sed 's|\n|\r\n|g'`, (or, more # correctly, `sed 's|$|\r|'`) but it does't line-buffer. local perlcmd=' my $size; my $buffer; while(1) { $size=sysread(STDIN, $buffer, 40); last if ($size < 1); $buffer =~ s/\n/\r\n/g; syswrite(STDOUT, $buffer); }' cmd=(bash --noprofile --norc -c "set -o pipefail; $(printf '%q ' "$@") |& perl -e $(printf '%q' "$perlcmd")") fi set +u # if an array is empty, it counts as unbound "$_arch_nspawn" "${arch_nspawn_flags[@]}" "$copydir" "${sysd_nspawn_flags[@]}" -- "${cmd[@]}" set -u } # Globals: $CHROOTDIR, $CHROOT, $COPY, $rootdir and $copydir main() { COPY=$LIBREUSER [[ $COPY != root ]] || COPY=copy local mode=enter while getopts 'n:l:NC:M: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");; 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/420" plain "https://bugs.freedesktop.org/show_bug.cgi?id=70290" prose "Due to a bug in systemd-nspawn, redirecting stdin is not supported. We have been able to mitigate the problems with redirecting stdout, but until the bug is fixed, redirecting stdin will only end in pain." >&2 return 1 fi # Keep this lock 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 ! 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" set +u # if an array is empty, it counts as unbound "$_mkarchroot" "${arch_nspawn_flags[@]}" "$rootdir" base-devel set -u 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 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 if [[ $mode != delete ]]; then # "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 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" "$COPY" 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 '$copydir'/bin/chcleanup '$copydir'/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 "$@"