#!/usr/bin/env bash set -euE # librechroot # Copyright 2010 Nicolás Reynolds # Copyright 2011 Joshua Haase # Copyright 2012-2013 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 3 places: # - the usage() text # - the commands=() array # - the case statement in main() . $(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 ) # 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 } 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 equivalent to `sed -s 's|\n|\r\n|g', but # it doesn'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 >/dev/stderr; return 1;; esac done shift $(($OPTIND - 1)) if [[ $# -lt 1 ]]; then error "Must specify a command" usage >/dev/stderr return 1 fi mode=$1 if ! in_array "$mode" "${commands[@]}"; then error "Unrecognized command: %s" "$mode" usage >/dev/stderr return 1 fi shift 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 if ! [[ -t 0 ]]; then warning "Input is not a TTY--signals will not be handled correctly." 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 [[ ! -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 [[ -n ${CHROOTEXTRAPKG[*]:-} ]]; then declare -p CHROOTEXTRAPKG | sed -r 's/declare( -.)* //' else printf 'CHROOTEXTRAPKG=()\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 "$@"