#!/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-2018 Luke Shumaker # Copyright (C) 2018 Andreas Grapentin # Copyright (C) 2020 bill-auger # # 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, take special notice of: # - the usage() text # - the COMMANDS=() array # - the the "validate CLI options" switch block which validates them # - the "handle commands" switch block which handles them source "$(librelib conf)" source "$(librelib messages)" source "$(librelib chroot/makechrootpkg)" shopt -s nullglob umask 0022 ################################################################################ # Utility functions # ################################################################################ # Usage: detect_chroot_arch $makepkg_conf detect_chroot_arch() { local makepkg_conf="$1" local chroot_arch chroot_arch=$(grep -a '^CARCH=' "$makepkg_conf" 2> /dev/null | cut -d '=' -f 2 | tr -d '"') [[ ${chroot_arch} =~ x86_64|i686|armv7h ]] || chroot_arch=$(uname -m) echo ${chroot_arch} } # 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 local pkgfile mkdir -p "$copydir/repo" 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 # TODO: review the implementation and USAGE for `-l` # this trick only works with absolute path to chroot # eg: why does `-l` accept 'Name of, or absolute path ...' # when -n can be given for 'Name of the chroot to use'? if [[ ${COPY:0:1} = / ]]; then CHROOTDIR=$(dirname $(dirname ${COPY})) CHROOT=$( basename $(dirname ${COPY})) fi 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 CHROOTDIR declare -p CHROOT 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[@]}" } ################################################################################ # Wrappers for files in ${pkglibexecdir}/chroot/ # ################################################################################ readonly _chroot_run="$(librelib chroot/chroot-run)" readonly _mkarchroot="$(librelib chroot/mkarchroot)" chroot_run_flags=() mkarchroot_flags=() hack_mkarchroot_flags() { local copydir="$1" local makepkg_conf="$copydir/etc/makepkg.conf" local CARCH=$(detect_chroot_arch "$makepkg_conf") local setarch interpreter OPTIND=1 set -- "${mkarchroot_flags[@]}" while getopts 'hC:M:c:f:s' arg; do case "$arg" in M) makepkg_conf="$OPTARG" ;; *) :;; esac done case $CARCH in armv7h|armv7l) setarch=armv7l; interpreter=/usr/bin/qemu-arm-static ;; *) setarch=$CARCH; interpreter=/usr/bin/qemu-$CARCH-static ;; esac if ! setarch $setarch /bin/true 2>/dev/null; then # We're running a cross-arch chroot # Make sure that qemu-static is set up with binfmt_misc if [[ -z $(grep -l -xF \ -e "interpreter $interpreter" \ -r -- /proc/sys/fs/binfmt_misc 2>/dev/null \ | xargs -r grep -xF 'enabled') ]] then error 'Cannot cross-compile for %s on %s' "$CARCH" "$(uname -m)" plain 'This requires a binfmt_misc entry for %s.' "$interpreter" 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.' \ qemu-user-static-binfmt binfmt return $EXIT_NOTINSTALLED fi # Let qemu/binfmt_misc do its thing mkarchroot_flags+=(-f "$interpreter" -s) fi } # Usage: chroot-run $copydir $cmd... chroot-run() { local copydir=$1; shift local cmd=("$@") local mkarchroot_flags=("${mkarchroot_flags[@]}") hack_mkarchroot_flags "$copydir" "$_chroot_run" \ "${mkarchroot_flags[@]}" \ "${chroot_run_flags[@]}" \ -- \ "$copydir" \ "${cmd[@]}" } # Usage: mkarchroot $copydir $pkgs... mkarchroot() { local copydir=$1; shift local pkgs=("$@") local mkarchroot_flags=("${mkarchroot_flags[@]}") hack_mkarchroot_flags "$copydir" local cmd=( unshare -m "$_mkarchroot" # mkarchroot flags: "${mkarchroot_flags[@]}" # chroot directory: -- "$copydir" # pacman flags: # * hack around https://bugs.archlinux.org/task/49347 --hookdir="$copydir/etc/pacman.d/hooks" # packages: # * and maybe more flags... pacstrap injects flags at the end, # so we can't disable flag parsing with '--') "${pkgs[@]}" ) "${cmd[@]}" } ################################################################################ # Main program # ################################################################################ readonly DEF_PACMANCONF_DIR=/usr/share/pacman/defaults readonly COMMANDS=( make sync delete install-file install-name upgrade clean-pkgs run enter clean-repo help ) usage() { # NOTE: assumes that 'calculate_directories' has executed local mount_msg="$(_ 'PATH[:INSIDE_PATH[:OPTIONS]]')" print "Usage: %s [OPTIONS] COMMAND [ARGS...]" "${0##*/}" echo print 'Interacts with a libre "archroot" (similarly to `arch-chroot`).' echo prose 'This is configured with `chroot.conf`, either in `/etc/libretools.d/`, or `$XDG_CONFIG_HOME/libretools/`. The configurable variables are:' bullet '$CHROOTDIR' bullet '$CHROOT' bullet '$CHROOTEXTRAPKG' echo prose 'Each chroot is named; and there may be multiple chroots; all of which, are stored in $CHROOTDIR, with the default chroot name being: $CHROOT, as set in chroot.conf. Each named chroot has a pristine, read-only master system (`root`), which is used as the seed for any number of named working copies, which may be derived from it. If a working copy is not specified, the default is: $LIBREUSER.' prose '$LIBREUSER is determined at runtime, per /usr/lib/libretools/conf.sh. $LIBREUSER is normally the login of the invoking user, per $SUDO_USER; but may be over-ridden by setting $LIBREUSER in the `configuration` section of conf.sh.' prose 'If the working copy is not explicitly specified as an absolute path, the path to the working copy ($copydir) will determined at runtime:' print ' $CHROOTDIR/$CHROOT/$COPY' echo prose 'The current settings for the above variables are:' bullet "\$CHROOTDIR : %s" "${CHROOTDIR:-$(_ 'ERROR: NO SETTING')}" bullet "\$CHROOT : %s" "${CHROOT:-$( _ 'ERROR: NO SETTING')}" bullet "\$COPY : %s" "${COPY}" bullet "\$rootdir : %s" "${rootdir:-$( _ 'ERROR' )}" bullet "\$copydir : %s" "${copydir:-$( _ 'ERROR' )}" echo prose 'If the chroot-set or working copy does not exist, it will be created automatically. A chroot by default contains the packages in the `base-devel` group and any packages named in $CHROOTEXTRAPKG. However, for technical reasons, `fakeroot-tcp` replaces `fakeroot` in armv7h chroots (per BR #2775). The packages installed into the chroot, will be those found in the standard Parabola repos: (libre, core, extra, community, pcr), regardless of the pacman/makepkg configuration of the host system. The configuration files in the chroot will also be the standard ones installed by the `pacman` package. These defaults may be over-ridden with the `-C`, `-M`, and `-A` flags; and other tools (such as `libremakepkg`) may over-ride them later.' echo prose 'This command will make the following configuration changes in the chroot:' bullet 'overwrite `/etc/libretools.d/chroot.conf`' # libretools/librechroot bullet 'overwrite `/etc/pacman.d/mirrorlist`' # devtools/chroot-run bullet 'set `CacheDir` in `/etc/pacman.conf`' # devtools/chroot-run prose 'If an over-ride `pacman.conf` is specified with the `-C` flag, the changes above are made after the file is copied in. The `-C` flag will not prevent these modifications.' echo prose 'The processor architecture of the chroot is determined by the `$CARCH` variable, in the `/etc/makepkg.conf` file, inside of the chroot. The `-M` flag allows specifying an over-ride of the standard file.' echo print 'The `-A ` flag sets the -C and -M flags as:' echo " -C \"${DEF_PACMANCONF_DIR}/pacman.conf.\$CARCH\" \\" echo " -M \"${DEF_PACMANCONF_DIR}/makepkg.conf.\$CARCH\"" prose 'However, `pacman.conf` will be modified to:' bullet 'set `Architecture` to match the `CARCH=` line in `makepkg.conf`' bullet 'comment-out any `Include = /etc/pacman.d/*.conf` lines' bullet 'add special-case build support repos (commented-out)' prose 'The `-A` option is recommended for creating a new `root` seed, e.g.:' echo ' sudo librechroot -n i686 -A i686 make' echo prose 'Creating/deleting/synchronizing a copy, can be relatively slow; but can be very fast, if $CHROOTDIR is on a btrfs partition.' echo flag 'Options:' \ "-n <$(_ NAME)>" 'Name of the chroot to use' \ "-l <$(_ COPY)>" 'Name of, or absolute path to, the copy to use' \ "-C <$(_ FILE)>" 'Copy this file to chroot as: `/etc/pacman.conf` This option is mutually exclusive with -A.' \ "-M <$(_ FILE)>" 'Copy this file to chroot as: `/etc/makepkg.conf` This option is mutually exclusive with -A.' \ "-A <$(_ CARCH)>" 'Specify the architecture of a new `root` seed. This option is mutually exclusive with -C and -M.' \ "-N" 'Disable networking in the chroot' \ "-w <${mount_msg}>" 'Bind mount a file or directory, read/write' \ "-r <${mount_msg}>" 'Bind mount a file or directory, read-only' echo flag 'Commands (create/copy/delete):' \ 'make' 'Create a `root` seed and working copy' \ 'sync' 'Sync the copy with the clean `root` seed' \ 'delete' 'Delete the working copy' flag 'Commands (packages):' \ "install-file $(_ FILES...)" 'Like `pacman -U FILES...`' \ "install-name $(_ NAMES...)" 'Like `pacman -S NAMES...`' \ 'upgrade' 'Like `pacman -Syu`' \ 'clean-pkgs' 'Remove all packages from the working copy that are not in `base-devel`, $CHROOTEXTRAPKG, or in the depends array of the working copy /startdir/PKGBUILD; and install all packages that are. `libremakepkg` does this implicitly.' flag 'Commands (maintenance):' \ "run $(_ CMD...)" 'Run CMD in the working copy' \ 'enter' 'Enter an interactive shell in the working copy' \ 'clean-repo' 'Clean /repo in the working copy' flag 'Commands (misc):' \ 'help' 'Show this message' } # Usage: ExitInvalidArgument error_fmt [ error_arg1 [ error_arg2 [ ... ] ] ] ExitInvalidArgument() { (( $# )) && error "$@" print "See: \`%s help\` for usage" "${0##*/}" exit $EXIT_INVALIDARGUMENT } # Globals: $CHROOTDIR, $CHROOT, $COPY, $rootdir and $copydir main() { COPY=$( [[ $LIBREUSER == root ]] && echo copy || echo "$LIBREUSER" ) declare -i was_conf_error=0 load_conf chroot.conf CHROOTDIR CHROOT || was_conf_error=$? local mode=enter opt declare -Ai used_opts local target_arch=$(uname -m) local def_pacmanconf="${DEF_PACMANCONF_DIR}"/pacman.conf.${target_arch} local def_makepkgconf="${DEF_PACMANCONF_DIR}"/makepkg.conf.${target_arch} local tmp_pacmanconf="$(mktemp --tmpdir librechroot-pacman.conf.XXXXXXXXXX)" local seed_pacmanconf="${def_pacmanconf}" local use_tmp_pacmanconf=0 mkarchroot_flags=( -C "$def_pacmanconf" -M "$def_makepkgconf" ) trap "[[ ! -f \"$tmp_pacmanconf\" ]] || rm -f -- \"$tmp_pacmanconf\"" EXIT ## parse CLI options ## while getopts 'n:l:C:M:A:Nw:r:' opt; do case $opt in n) CHROOT=$OPTARG ;; l) COPY=$OPTARG ;; C) mkarchroot_flags=( -C "$OPTARG" -M "$def_makepkgconf" ) ;; M) mkarchroot_flags=( -M "$OPTARG" -C "$def_pacmanconf" ) target_arch=$(detect_chroot_arch "$OPTARG") ;; A) target_arch=$OPTARG use_tmp_pacmanconf=1 def_pacmanconf="${DEF_PACMANCONF_DIR}"/pacman.conf.${target_arch} def_makepkgconf="${DEF_PACMANCONF_DIR}"/makepkg.conf.${target_arch} seed_pacmanconf="${tmp_pacmanconf}" mkarchroot_flags+=( -C "${seed_pacmanconf}" -M "${def_makepkgconf}" ) ;; N ) chroot_run_flags+=( -N ) ;; w ) chroot_run_flags+=(-b "-B:$OPTARG") ;; r ) chroot_run_flags+=(-b "-Br:$OPTARG") ;; * ) ExitInvalidArgument ;; esac used_opts[$opt]+=1 done mode=${!OPTIND} shift $OPTIND ## validate state ## if (( $was_conf_error )); then error "Could not load chroot.conf configuration" exit $was_conf_error fi eval "$(calculate_directories)" readonly LIBREUSER LIBREHOME readonly CHROOTDIR CHROOT COPY readonly rootdir copydir readonly mode ## validate CLI options ## for opt in n l C M A; do if (( ${used_opts[$opt]:-0} > 1 )); then ExitInvalidArgument "$(_ 'Option -%s may only be given once')" "$opt" fi done if (( ${used_opts[A]:-0} && ( ${used_opts[C]:-0} || ${used_opts[M]:-0} ) )); then ExitInvalidArgument "$(_ 'Option -A may not be used together with -C or -M')" fi if [[ -z "${mode}" ]]; then ExitInvalidArgument "$(_ 'Must specify a command')" fi if ! in_array "$mode" "${COMMANDS[@]}"; then ExitInvalidArgument "$(_ 'Unrecognized command: %s')" "$mode" fi case "$mode" in help) usage return $EXIT_SUCCESS :;; make|sync|delete|upgrade|enter|clean-pkgs|clean-repo) if [[ $# -gt 0 ]]; then ExitInvalidArgument "$(_ 'Command `%s` does not take any arguments: %s')" "$mode" "$*" fi :;; install-file) if [[ $# -lt 1 ]]; then ExitInvalidArgument "$(_ 'Command `%s` requires at least one file')" "$mode" else local missing=() local file for file in "$@"; do if ! [[ -f $file ]]; then missing+=("$file") fi done if [[ ${#missing[@]} -gt 0 ]]; then ExitInvalidArgument "$(_ '%s: file(s) not found: %s')" "$mode" "${missing[*]}" fi fi :;; install-name) if [[ $# -lt 1 ]]; then ExitInvalidArgument "$(_ 'Command `%s` requires at least one package name')" "$mode" fi :;; run) if [[ $# -lt 1 ]]; then ExitInvalidArgument "$(_ 'Command `%s` requires at least one argument')" "$mode" fi :;; # obsolete commands noop) # `noop` was inprecise and redundant ExitInvalidArgument "$(_ 'Command `noop` is obsolete. Use `make` instead.')" :;; update) # `update` was semantically incorrect ExitInvalidArgument "$(_ 'Command `update` is obsolete. Use `upgrade` instead.')" :;; esac if (( use_tmp_pacmanconf )) && \ ! [[ -f "${def_pacmanconf}" && -f "${def_makepkgconf}" ]]; then ExitInvalidArgument "$(_ 'Unsupported architecture: %s. See the files in %q for valid architectures.')" \ "${target_arch}" "${DEF_PACMANCONF_DIR}/" fi ## process CLI options ## if (( use_tmp_pacmanconf )); then readonly REPOS_HEADER='###########################\n# Parabola standard repos #' readonly VOLATILE_REPOS='\ ##############################\ # Special-case build-support #\ ##############################\ \ # Enable the volatile arm [arm-aur] repo only as needed\ # [arm-aur]\ # Server = https://mirror.archlinuxarm.org/$arch/$repo/\ \ # Enable the volatile i686 [build-support] repo only as needed\ # [build-support]\ # Server = http://mirror.archlinux32.org/$arch/$repo/\ \ # Enable the volatile i686 [staging] repo only as needed\ # [staging]\ # Server = http://mirror.archlinux32.org/$arch/$repo/\ \ # Enable the volatile x86_64 [staging] repo only as needed\ # [staging]\ # Server = http://mirror.archlinux.org/$arch/$repo/' # generate custom pacman.conf sed -r \ -e "s|^#?\\s*Architecture.+|Architecture = ${target_arch}|g" \ -e "s|^.*Include\s*=\s*/etc/pacman.d/.*\.conf|#&|" \ -e "N ; /${REPOS_HEADER}/ i ${VOLATILE_REPOS}\n\n" \ < "${def_pacmanconf}" \ > "${tmp_pacmanconf}" fi ## chroot setup ## if (( EUID )); then error "This program must be run as root." return $EXIT_NOPERMISSION fi umask 0022 # 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 working 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 # Create chroot seed system # NOTE: ARM chroots require 'fakeroot-tcp' (BR #2775) local fakeroot_pkg=fakeroot$( [[ "${target_arch}" == 'armv7h' ]] && echo '-tcp' ) local chroot_pkgs=$( pacman --config "${seed_pacmanconf}" -Sgq base-devel | \ sed "s|fakeroot|${fakeroot_pkg}|" ) msg "Creating 'root' seed for chroot [%s]" "$CHROOT" mkarchroot "$rootdir" ${chroot_pkgs} "$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 chroot-run "$copydir" true "$copydir/chrootexec" chmod 755 "$copydir/chrootexec" chroot-run "$copydir" /chrootexec