#!/bin/sh imgdir=./debootstrap warn() { printf 'Warning: %s\n' "$*" >&2; } die() { printf 'Error: %s\n' "$*" >&2; exit 1; } [ -d "$imgdir" ] || die "directory does not exist: $imgdir" usage() { cat <&2 Usage: $0 init $0 new $0 clone $0 chroot $0 list $0 show unpackaged $0 show unpackaged-du EOF list exit 1 } list() { local f suite name header_printed= for f in $imgdir/*.btrfs; do [ -e "$f" ] || continue f=${f##*/} f=${f%.btrfs} case "$f" in *.*) continue ;; esac suite=${f%%-*} [ "$header_printed" ] || echo 'Existing initialized suites:' printf ' %s\n' "$suite" header_printed=y done header_printed= for f in $imgdir/*.*.btrfs; do [ -e "$f" ] || continue f=${f##*/} f=${f%.btrfs} suite=${f%%-*} name=${f##*.} [ "$header_printed" ] || echo 'Existing images:' printf ' %s %s\n' "$suite" "$name" header_printed=y done } validate_suite() { case "$1" in jessie|stretch|sid) return 0 ;; *) return 1 ;; esac } i_am_root() { [ "$(id -u)" = 0 ]; } btrfs_show_subvolume_id() { local result path="$1" result=$(btrfs subvolume show "$path" | sed -n -e 's/^[ \t]*Subvolume ID:[ \t]*//p; s/.*is toplevel subvolume/5/p') if [ "$result" ] then printf '%s\n' "$result" else false fi } mkfs_btrfs() { local target="$1" mountpoint="$2" # can't be local because of trap mkfs.btrfs "$target" || die "mkfs.btrfs failed" mount -t btrfs "$target" "$mountpoint" || die "mount failed" trap 'umount "$mountpoint"' EXIT btrfs subvolume create "$mountpoint"/root || die "command 'btrfs subvolume create' failed" mkdir -p "$mountpoint"/root/var/cache/apt btrfs subvolume create "$mountpoint"/root/var/cache/apt/archives || die "command 'btrfs subvolume create' failed" subvol_id=$(btrfs_show_subvolume_id "$mountpoint"/root) || die "could not find btrfs subvolume name" btrfs subvolume set-default "$subvol_id" "$mountpoint" || die "command 'btrfs subvolume set-default' failed" trap - EXIT umount "$mountpoint" } suite_to_basename() { suite=$1 variant=minbase arch=$(dpkg-architecture -q DEB_HOST_ARCH) || die "command 'dpkg-architecture' failed" printf '%s\n' "$suite-$variant-$arch.btrfs" } suite_name_to_imagename() { suite=$1 name=$2 variant=minbase arch=$(dpkg-architecture -q DEB_HOST_ARCH) || die "command 'dpkg-architecture' failed" printf '%s/%s-%s-%s.%s.btrfs\n' "$imgdir" "$suite" "$variant" "$arch" "$name" } show_unpackaged() { suite=$1 name=$2 imagename=$(suite_name_to_imagename "$suite" "$name") sh_image "$suite" "$name" find -type f | sed 's?^\./??' | sort > ${imagename}.find.txt sh_image "$suite" "$name" sh -c 'sort -u var/lib/dpkg/info/*.list' | sed -e 's?^/??' -e 's?^\.$/??' > ${imagename}.dpkg-list.txt comm -23 ${imagename}.find.txt ${imagename}.dpkg-list.txt | tee ${imagename}.unpackaged.txt } show_unpackaged_du() { if [ "$3" = filtered ]; then filter='var/lib/dpkg/|var/lib/apt/|var/log/' show_unpackaged "$@" | egrep -v "^($filter)" | sh_image "$1" "$2" xargs du -csh else show_unpackaged "$@" | sh_image "$1" "$2" xargs du -csh fi } sh_image() { suite=$1 name=$2 shift 2 if [ $# = 0 ]; then set -- /bin/bash fi [ "$suite" -a "$name" ] || usage imagename=$(suite_name_to_imagename "$suite" "$name") [ -e "$imagename" ] || die "no such file: $imagename" [ -d "$imagename".mnt ] || mkdir "$imagename".mnt || die "mkdir" mountpoint -q "$imagename".mnt || mount "$imagename" "$imagename".mnt || die "mount" (cd "$imagename".mnt; "$@") r=$? umount "$imagename".mnt rmdir "$imagename".mnt return $r } chroot_image() { suite=$1 name=$2 shift 2 [ $# = 0 ] && set -- /bin/bash set -- \ unshare -f -m -p \ chroot . \ /bin/sh -c 'mount -t proc proc /proc; mount -t devpts devpts /dev/pts; exec "$@"' sh \ "$@" sh_image "$suite" "$name" "$@" } clone() { suite=$1 source_name=$2 name=$3 [ "$suite" -a "$name" -a "$source_name" ] || usage image_basename=$imgdir/$(suite_to_basename "$suite") || die 'suite_to_basename' source=${image_basename%.btrfs}.$source_name.btrfs dest=${image_basename%.btrfs}.$name.btrfs [ -e "$source" ] || die "source image not found -- run '$0 new $suite $source_name' to create it" [ ! -e "$dest" ] || die "file exists: $dest" cp --reflink=always "$source" "$dest" } new() { suite=$1 name=$2 [ "$suite" -a "$name" ] || usage image_basename=$imgdir/$(suite_to_basename "$suite") || die 'suite_to_basename' source=$image_basename dest=${source%.btrfs}.$name.btrfs [ -e "$source" ] || die "source image not found -- run '$0 init $suite' to create it" [ ! -e "$dest" ] || die "file exists: $dest" cp --reflink=always "$source" "$dest" } init() { suite=$1 variant=minbase arch=$(dpkg-architecture -q DEB_HOST_ARCH) || die "command 'dpkg-architecture' failed" size=1G # truncate(1) format i_am_root || die 'you are not root' validate_suite "$suite" || die "invalid suite: '$suite'" target=$imgdir/$suite-$variant-$arch.btrfs [ -e "$target" ] && return [ -e "$target".tmp ] && die "refusing to overwrite existing temporary file: $target.tmp" [ -d "$target".mnt ] || mkdir "$target".mnt || die "could not create directory $target.mnt" truncate -s "$size" "$target".tmp || die "truncate failed" mkfs_btrfs "$target".tmp "$target".mnt || die "btrfs filesystem creation failed" mount -t btrfs "$target".tmp "$target".mnt || die "mount failed" trap 'umount "$target.mnt"' EXIT debootstrap_efficiently "$arch" "$variant" "$suite" \ "$target".mnt "${target%.btrfs}".debs.txt || die "debootstrap failed" if umount "$target".mnt then trap - EXIT else warn "umount failed" fi mv "$target".tmp "$target" } debootstrap_efficiently() { local arch="$1" variant="$2" suite="$3" target="$4" savedebs="$5" set -- --arch "$arch" --variant "$variant" "$suite" "$target" debs=$(debootstrap --print-debs --keep-debootstrap-dir "$@" | tee "$savedebs") || die "debootstrap failed" for deb in $debs; do cp /var/cache/apt/archives/${deb}_* $target/var/cache/apt/archives/ done debootstrap "$@" || die "debootstrap failed" } show() { case "$1" in unpackaged) shift; show_unpackaged "$@" ;; unpackaged-du) shift; show_unpackaged_du "$@" ;; *) usage ;; esac } case "$1" in init|new|clone|list|show) cmd=$1; shift; $cmd "$@" ;; chroot|sh) cmd=${1}_image; shift; $cmd "$@" ;; *) usage ;; esac