#!/bin/bash set -e set -o pipefail debian_mirror=http://httpredir.debian.org/debian debian_security_mirror=http://security.debian.org EXTRA_PACKAGES='apt debian-archive-keyring locales' REAL_APT_INSTALL=y die() { printf 'Error: %s\n' "$*"; exit 1; } warn() { printf 'Warning: %s\n' "$*"; } usage() { echo "Usage: $0 -t [options] [packages]" >&2 cat < Use prog for chroot command --packages= Text file contains names of packages to install --real-apt Use 'apt-get install' to install packages --skip-update Do not run 'apt-get update' at startup EOF exit ${1:-1} } GETOPT=$(getopt -n "${0##*/}" -o t:hv --long help,target:,release:,verbose,unpack,chroot:,packages:,real-apt,skip-update -- "$@") || exit eval set -- "$GETOPT" while [ $# -gt 0 ]; do case "$1" in -t|--target) TARGET=$2; shift ;; -h|--help) usage 0 ;; -v|--verbose) VERBOSE=y ;; --skip-update) SKIP_UPDATE=y ;; --unpack) UNPACK_ONLY=y ;; --packages) x=$(cat < "$2") || die "could not read file '$2'" EXTRA_PACKAGES="$EXTRA_PACKAGES $x" shift ;; --release) target_release=$2; shift ;; --chroot) CHROOT_PROG=$2; shift ;; --real-apt) REAL_APT_INSTALL=y REAL_DPKG=y;; --) shift; break ;; *) usage 1 ;; esac shift done [ "$TARGET" ] || usage if [ "$REAL_APT_INSTALL" -a "$UNPACK_ONLY" ] then warn "option --real-apt is incompatible with option --unpack; disabling --unpack" UNPACK_ONLY= fi EXTRA_PACKAGES="$EXTRA_PACKAGES $*" am_root() { [ "$(id -u)" = 0 ]; } current_debian_codename() { # lsb_release -cs sed -ne 's/^VERSION=.* (\(.*\)).*/\1/p' /etc/os-release } sanity_checks() { am_root || die 'you are not root' TARGET=$(realpath "$TARGET") || die 'realpath failed' [ "$TARGET" ] || die 'no $TARGET' [ -d "$TARGET" ] || mkdir "$TARGET" || die 'could not mkdir($TARGET)' } generate_apt_config() { APT_CONFIG=$(mktemp) || exit chmod 644 "$APT_CONFIG" cat > "$APT_CONFIG" <&2 fi set -- "$@" -o Apt::Install-Recommends=false set -- "$@" -o Apt::Architecture="$target_arch" # Set default paths to within the created system. set -- "$@" -o Dir="$TARGET" set -- "$@" -o Dir::State::Status="$TARGET"/var/lib/dpkg/status # We must also set dpkg to use the created system. # Dpkg::options requires undocumented apt CLI magic. if [ "$DEBUG_DPKG" ]; then [ "$DEBUG_DPKG" -gt 0 ] 2>/dev/null || DEBUG_DPKG=10013 set -- "$@" -o DPkg::options::arg0=--debug="${DEBUG_DPKG}" fi # This is the important one: set -- "$@" -o DPkg::options::arg1=--root="$TARGET" set -- "$@" -o DPkg::options::arg2=--force-unsafe-io # Use the calling system for these. This is an optimization. #set -- "$@" -o Dir::Etc::sourcelist=/etc/apt/sources.list #set -- "$@" -o Dir::Etc::sourceparts=/etc/apt/sources.list.d set -- "$@" -o Dir::Etc::Trusted=/etc/apt/trusted.gpg set -- "$@" -o Dir::Etc::TrustedParts=/etc/apt/trusted.gpg.d set -- "$@" -o Dir::State::lists=/var/lib/apt/lists set -- "$@" -o Dir::Cache::archives=/var/cache/apt/archives # Avoid deleting lists on the calling system. set -- "$@" -o APT::Get::List-Cleanup=false case "${apt_cmd}" in get | cache ) set -- apt-"${apt_cmd}" "$@" ;; * ) set -- apt "${apt_cmd}" "$@" ;; esac if command -v eatmydata >/dev/null then set -- eatmydata -- "$@" fi ( set -x "$@" ) r=$? if [ "$r" != 0 ] then echo "Error: apt-${apt_cmd} returned $r" >&2 return $r fi } apt_get() { apt_ get "$@"; } apt_cache() { apt_ cache "$@"; } idem() { if [ ! -e "${!#}" ]; then "$@"; fi; } idem_mknod() { if [ ! -e "$3" ]; then mknod "$@"; fi; } # This function is copied (with modifications) from debootstrap. install_devices() { [ "$TARGET" -a -d "$TARGET" ] || die 'no $TARGET' idem mkdir "$TARGET"/dev # The list of devices that can be created in a container comes from # src/core/cgroup.c in the systemd source tree. idem_mknod -m 666 "$TARGET"/dev/null c 1 3 idem_mknod -m 666 "$TARGET"/dev/zero c 1 5 idem_mknod -m 666 "$TARGET"/dev/full c 1 7 idem_mknod -m 666 "$TARGET"/dev/random c 1 8 idem_mknod -m 666 "$TARGET"/dev/urandom c 1 9 idem_mknod -m 666 "$TARGET"/dev/tty c 5 0 idem mkdir "$TARGET"/dev/pts/ idem mkdir "$TARGET"/dev/shm/ # Inside a container, we might not be allowed to create /dev/ptmx. # If not, do the next best thing. if ! idem_mknod -m 666 "$TARGET"/dev/ptmx c 5 2; then idem ln -s pts/ptmx "$TARGET"/dev/ptmx fi idem ln -s /proc/self/fd "$TARGET"/dev/fd idem ln -s /proc/self/fd/0 "$TARGET"/dev/stdin idem ln -s /proc/self/fd/1 "$TARGET"/dev/stdout idem ln -s /proc/self/fd/2 "$TARGET"/dev/stderr } prepare_chroot() { [ "$TARGET" ] || die 'no $TARGET' [ -d "$TARGET"/proc ] || mkdir "$TARGET"/proc [ -d "$TARGET"/sys ] || mkdir "$TARGET"/sys mountpoint -q "$TARGET"/proc || mount -t proc proc "$TARGET"/proc mountpoint -q "$TARGET"/sys || mount -t sysfs sysfs "$TARGET"/sys mountpoint -q "$TARGET"/dev || mount --bind /dev "$TARGET"/dev [ -e "$TARGET"/etc/resolv.conf ] || touch "$TARGET"/etc/resolv.conf mountpoint -q "$TARGET"/etc/resolv.conf || mount --bind /etc/resolv.conf "$TARGET"/etc/resolv.conf } cleanup_chroot() { [ "$TARGET" ] || die 'no $TARGET' umount "$TARGET"/proc || fail=y umount "$TARGET"/sys || fail=y umount "$TARGET"/dev || fail=y umount "$TARGET"/etc/resolv.conf || fail=y [ ! "$fail" ] } write_lines_once() { local output="$1" shift [ -e "$output" ] || printf '%s\n' "$@" > "$output" } populate_rootfs() { [ "$TARGET" ] || die 'no $TARGET' set -B mkdir -p \ "$TARGET"/etc/apt/{preferences.d,apt.conf.d,trusted.gpg.d,sources.list.d} \ "$TARGET"/var/{lib/{apt/lists/partial,dpkg/{info,parts,triggers,alternatives,updates}},cache/apt} \ "$TARGET"/var/log/apt \ "$TARGET"/tmp/user/0 touch "$TARGET"/var/lib/dpkg/status [ -d "$TARGET"/usr/bin ] || mkdir -p "$TARGET"/usr/bin [ -e "$TARGET/usr/bin/awk" -o -L "$TARGET/usr/bin/awk" ] || ln -s mawk "$TARGET/usr/bin/awk" write_lines_once "$TARGET"/var/lib/dpkg/arch "$target_arch" write_sources_list install_devices } bookworm_sources() { cat < "$dest" : > "$dest_aptconf" ;; *) "die unrecognized target release: $target_release" ;; esac } parse_apt_simul_line() { set -- $* action=$1 package=$2 # Third word might either be "(version" or "[version]" depending on which # apt is producing the output. Newer apt versions apparently add an extra # "[version]" parameter, making "(version" the fourth argument. version=${3#\(} version=${version#\[} version=${version%]} shift 3 # Find the last word, which is inside (a group of words that are inside # parentheses). while [ "$1" ]; do case "$1" in *\)) arch=${1%\)} arch=${arch#\[} arch=${arch%\]} return ;; *) shift ;; esac done return 1 } dpkg_unpack() { [ "$TARGET" -a "$TARGET" != '/' -a -d "$TARGET" ] || die 'no $TARGET' local deb="$1" multiarch="$2" SET_STATUS="$3" command PKG PKG=${deb##*/} PKG=${PKG%%_*} PKG=$PKG$multiarch extract_tmp_ci "$deb" install_metadata_from_tmp_ci # uses TARGET PKG SET_STATUS remove_tmp_ci dpkg --fsys-tarfile "$deb" | (cd "$TARGET" && tar -xv) | sed 's?^\.??; s?^/$?/.?; s?/$??' > "$TARGET/var/lib/dpkg/info/$PKG.list" } install_metadata_from_tmp_ci() # uses TARGET PKG SET_STATUS { (cd "$TARGET"/var/lib/dpkg/tmp.ci || die "cannot cd to /var/lib/dpkg/tmp.ci" # PKG=$(sed -n 's/^Package: *//p' control) for f in *; do [ "$f" = postinst ] && SET_STATUS=${SET_STATUS:+installed} || true [ "$f" = control ] || mv "$f" "$TARGET"/var/lib/dpkg/info/"$PKG"."$f" done if [ "$SET_STATUS" -a -e control ]; then (sed "/^Package:/a Status: install ok $SET_STATUS"; echo) < control >> "$TARGET"/var/lib/dpkg/status fi) } verbosely() { if [ "$VERBOSE" ]; then (set -x; "$@") else "$@" fi } remove_tmp_ci() { [ ! -e "$TARGET"/var/lib/dpkg/tmp.ci ] || rm -r "$TARGET"/var/lib/dpkg/tmp.ci } # /var/lib/dpkg/tmp.ci/control is read by debconf to determine the owner package # See `grep -A3 control /usr/share/debconf/frontend` extract_tmp_ci() { local deb="$1" remove_tmp_ci mkdir "$TARGET"/var/lib/dpkg/tmp.ci dpkg --ctrl-tarfile "$deb" | tar -C "$TARGET"/var/lib/dpkg/tmp.ci -x } dpkg_configure_from_apt_actions() { while read line; do parse_apt_simul_line "$line" || die "parse_apt_simul_line: unexpected output from apt-get: $line" export LC_ALL=C export DEBIAN_FRONTEND=noninteractive export DPKG_MAINTSCRIPT_PACKAGE="$package" DPKG_MAINTSCRIPT_ARCH="$arch" is_multiarch_same "$package" && multiarch=":$arch" || multiarch= case "$action" in Inst) export DPKG_MAINTSCRIPT_NAME=preinst preinst=/var/lib/dpkg/info/${package}${multiarch}.preinst if [ -x "$TARGET"/"$preinst" ]; then extract_tmp_ci "$deb" # prepare_chroot "$TARGET" verbosely ${CHROOT_PROG:-chroot} "$TARGET" "$preinst" install # cleanup_chroot "$TARGET" remove_tmp_ci fi ;; Conf) export DPKG_MAINTSCRIPT_NAME=postinst postinst=/var/lib/dpkg/info/${package}${multiarch}.postinst if [ -x "$TARGET"/"$postinst" ]; then extract_tmp_ci "$deb" # prepare_chroot "$TARGET" verbosely ${CHROOT_PROG:-chroot} "$TARGET" "$postinst" configure # cleanup_chroot "$TARGET" remove_tmp_ci fi ;; Remv) ;; *) die "dpkg_configure_from_apt_actions: unknown apt simul action: $action" ;; esac done } apt_extract() { apt_get -d -y install "$@" actions=$(mktemp) || die 'mktemp failed' apt_get -s -yqq install "$@" > "$actions" || die 'apt-get failed' dpkg_unpack_from_apt_actions < "$actions" || die 'dpkg unpack (using internal dpkg) failed' if [ ! "$UNPACK_ONLY" ]; then install_etc_passwd dpkg_configure_from_apt_actions < "$actions" || die 'dpkg configure (using internal dpkg) failed' fi rm "$actions" } dpkg_unpack_from_apt_actions() { set -- while read line; do parse_apt_simul_line "$line" || die "parse_apt_simul_line: unexpected output from apt-get: $line" deb=/var/cache/apt/archives/${package}_${version//:/%3a}_${arch}.deb [ -f "$deb" ] || { echo "line=$line" >&2 printf '%s\n' "$action" "$package" "$version" "$arch" >&2 die "deb not found: $deb" } case "$action" in Inst) printf 'Unpacking %s\n' "${deb##*/}" >&2 if [ "$REAL_DPKG" ]; then set -- "$@" "$deb" else is_multiarch_same "$package" && multiarch=":$arch" || multiarch= [ "$UNPACK_ONLY" ] && set_status=unpacked || set_status=installed dpkg_unpack "$deb" "$multiarch" "$set_status" || die "dpkg_unpack" fi ;; Conf) ;; Remv) ;; *) die "dpkg_unpack_from_apt_actions: unknown apt simul action: $action" ;; esac done if [ $# -gt 0 ]; then dpkg --no-triggers --unpack --force-unsafe-io --root="$TARGET" "$deb" || die 'dpkg' # dpkg --configure --pending --root="$TARGET" fi } install_etc_passwd() { [ -e "$TARGET"/etc/passwd ] || cp "$TARGET"/usr/share/base-passwd/passwd.master "$TARGET"/etc/passwd [ -e "$TARGET"/etc/group ] || cp "$TARGET"/usr/share/base-passwd/group.master "$TARGET"/etc/group } required_packages() { apt_cache dumpavail | perl -00 -ne '/^Priority: required/m || next; /^Package: (.*)$/m && print "$1\n"' | sort -u } declare -A is_multiarch_same multicheck() { local m if [ ! "$multichecked" ]; then for m in $(multiarch_same_packages); do is_multiarch_same[$m]=y done fi multichecked=y } multiarch_same_packages() { apt_cache dumpavail | perl -00 -ne '/^Multi-Arch: same/mi || next; /^Package: (.*)$/m && print "$1\n"' | sort -u } is_multiarch_same() { multicheck [ "${is_multiarch_same[$1]}" ] } apt_update_stamp=~/.selfstrap/apt-update-stamp apt_get_update() { if [ ! -e "$apt_update_stamp" ] || [ $(( $(date +%s) - $(stat -c %Y "$apt_update_stamp") )) -gt $(( 60 * 60 * 24 )) ] then apt_get update mkdir -p ~/.selfstrap touch "$apt_update_stamp" fi } target_arch=$(dpkg-architecture -q DEB_HOST_ARCH) || die 'dpkg-architecture failed' [ "$target_release" ] || target_release=$(current_debian_codename) && [ "$target_release" ] || die 'could not determine Debian release name' # Set things up so apt-get update works. sanity_checks generate_apt_config populate_rootfs export APT_CONFIG [ "$SKIP_UPDATE" ] || apt_get_update required_packages=$(required_packages) && [ "$required_packages" ] || die 'failed to determine list of required packages' export LC_ALL=C export DEBIAN_FRONTEND=noninteractive if ! (apt_ policy | (! grep 990)) then die "apt policy looks wrong" fi if [ "$REAL_APT_INSTALL" ]; then # Some files need to be present before 'apt-get install' can install anything. # In particular: # # 1. binaries used by dpkg 'inst' scripts. # 2. /etc/passwd and /etc/group so that 'chown' works prepare_chroot "$TARGET" trap 'cleanup_chroot "$TARGET"' EXIT # Unpack required packages. Handles (1) # Note: populate_rootfs() already created a necessary symlink /usr/bin/awk -> /usr/bin/mawk apt_extract $required_packages eatmydata # apt_get install -y $required_packages eatmydata # This handles (2). # An alternative (used by debootstrap) is to configure base-passwd install_etc_passwd apt_get install -y $required_packages $EXTRA_PACKAGES else apt_extract $required_packages apt_extract $EXTRA_PACKAGES fi