|
@@ -0,0 +1,383 @@
|
|
|
+#!/bin/sh
|
|
|
+# vim: set ts=4:
|
|
|
+#---help---
|
|
|
+# Usage: alpine-make-rootfs [options] [--] <dest> [<script> [<script-opts...>]]
|
|
|
+#
|
|
|
+# This script creates Alpine Linux rootfs for containers. It must be run as
|
|
|
+# root - to create files with correct permissions and use chroot (optional).
|
|
|
+# If $APK is not available on the host system, then static apk-tools
|
|
|
+# specified by $APK_TOOLS_URI is downloaded and used.
|
|
|
+#
|
|
|
+# Arguments:
|
|
|
+# <dest> Path where to write the rootfs. It may be:
|
|
|
+# - path with suffix .tar, .tar.bz2, .tbz, .tar.gz, .tgz,
|
|
|
+# or tar.xz to create a TAR archive;
|
|
|
+# - other path to keep rootfs as a directory;
|
|
|
+# - or "-" to dump TAR archive (w/o compression) to STDOUT.
|
|
|
+#
|
|
|
+# <script> Path of script to execute after installing base system in
|
|
|
+# the prepared rootfs and before clean-up.
|
|
|
+#
|
|
|
+# <script-opts> Arguments to pass to the script.
|
|
|
+#
|
|
|
+# Options and Environment Variables:
|
|
|
+# -b --branch ALPINE_BRANCH Alpine branch to install; used only when
|
|
|
+# --repositories-file is not specified. Default is v3.7.
|
|
|
+#
|
|
|
+# --keys-dir KEYS_DIR Path of directory with Alpine keys to copy into
|
|
|
+# the rootfs. Default is /etc/apk/keys. If does not exist,
|
|
|
+# keys for x86_64 embedded in this script will be used.
|
|
|
+#
|
|
|
+# -m --mirror-uri ALPINE_MIRROR URI of the Aports mirror to fetch packages; used only
|
|
|
+# when --repositories-file is not specified. Default is
|
|
|
+# https://nl.alpinelinux.org/alpine.
|
|
|
+
|
|
|
+# -C --no-cleanup (CLEANUP) Don't umount and remove temporary directories when done.
|
|
|
+#
|
|
|
+# -p --packages PACKAGES Additional packages to install into the rootfs.
|
|
|
+#
|
|
|
+# -r --repositories-file REPOS_FILE Path of repositories file to copy into the rootfs.
|
|
|
+# Default is /etc/apk/repositories. If does not exist,
|
|
|
+# repositories file with Alpine's main and community
|
|
|
+# repositories on --mirror-uri is created.
|
|
|
+#
|
|
|
+# -c --script-chroot (SCRIPT_CHROOT) Bind <script>'s directory at /mnt inside the rootfs dir
|
|
|
+# and chroot into the rootfs before executing <script>.
|
|
|
+#
|
|
|
+# -d --temp-dir TEMP_DIR Path where to create a temporary directory; used for
|
|
|
+# downloading apk-tools when not available on the host
|
|
|
+# sytem or for rootfs when <dest> is "-" (i.e. STDOUT).
|
|
|
+# This path must not exist! Defaults to using `mkdir -d`.
|
|
|
+#
|
|
|
+# -t --timezone TIMEZONE Timezone to set (e.g. Europe/Prague). Default is to leave
|
|
|
+# timezone UTC.
|
|
|
+#
|
|
|
+# -h --help Show this help message and exit.
|
|
|
+#
|
|
|
+# -v --version Print version and exit.
|
|
|
+#
|
|
|
+# APK APK command to use. Default is "apk".
|
|
|
+#
|
|
|
+# APK_OPTS Options to pass into apk on each execution.
|
|
|
+# Default is "--no-progress".
|
|
|
+#
|
|
|
+# APK_TOOLS_URI URL of static apk-tools tarball to download if $APK is
|
|
|
+# not found on the host system. Default is x86_64 apk-tools
|
|
|
+# from https://github.com/alpinelinux/apk-tools/releases.
|
|
|
+#
|
|
|
+# APK_TOOLS_SHA256 SHA-256 checksum of $APK_TOOLS_URI.
|
|
|
+#
|
|
|
+# Each option can be also provided by environment variable. If both option and
|
|
|
+# variable is specified and the option accepts only one argument, then the
|
|
|
+# option takes precedence.
|
|
|
+#
|
|
|
+# https://github.com/jirutka/alpine-make-rootfs
|
|
|
+#---help---
|
|
|
+set -eu
|
|
|
+
|
|
|
+readonly PROGNAME='alpine-make-rootfs'
|
|
|
+readonly VERSION='0.0.0'
|
|
|
+
|
|
|
+# Base Alpine packages to install in rootfs.
|
|
|
+readonly ALPINE_BASE_PKGS='alpine-baselayout busybox busybox-suid musl-utils'
|
|
|
+
|
|
|
+# Alpine APK keys for verification of packages for x86_64.
|
|
|
+readonly ALPINE_KEYS='
|
|
|
+alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yHJxQgsHQREclQu4Ohe\nqxTxd1tHcNnvnQTu/UrTky8wWvgXT+jpveroeWWnzmsYlDI93eLI2ORakxb3gA2O\nQ0Ry4ws8vhaxLQGC74uQR5+/yYrLuTKydFzuPaS1dK19qJPXB8GMdmFOijnXX4SA\njixuHLe1WW7kZVtjL7nufvpXkWBGjsfrvskdNA/5MfxAeBbqPgaq0QMEfxMAn6/R\nL5kNepi/Vr4S39Xvf2DzWkTLEK8pcnjNkt9/aafhWqFVW7m3HCAII6h/qlQNQKSo\nGuH34Q8GsFG30izUENV9avY7hSLq7nggsvknlNBZtFUcmGoQrtx3FmyYsIC8/R+B\nywIDAQAB
|
|
|
+alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwlzMkl7b5PBdfMzGdCT0\ncGloRr5xGgVmsdq5EtJvFkFAiN8Ac9MCFy/vAFmS8/7ZaGOXoCDWbYVLTLOO2qtX\nyHRl+7fJVh2N6qrDDFPmdgCi8NaE+3rITWXGrrQ1spJ0B6HIzTDNEjRKnD4xyg4j\ng01FMcJTU6E+V2JBY45CKN9dWr1JDM/nei/Pf0byBJlMp/mSSfjodykmz4Oe13xB\nCa1WTwgFykKYthoLGYrmo+LKIGpMoeEbY1kuUe04UiDe47l6Oggwnl+8XD1MeRWY\nsWgj8sF4dTcSfCMavK4zHRFFQbGp/YFJ/Ww6U9lA3Vq0wyEI6MCMQnoSMFwrbgZw\nwwIDAQAB
|
|
|
+'
|
|
|
+# List of directories to remove when empty.
|
|
|
+readonly FUTILE_DIRS='
|
|
|
+ /home /media/cdrom /media/floppy /media/usb /mnt /srv /usr/local/bin
|
|
|
+ /usr/local/lib /usr/local/share
|
|
|
+'
|
|
|
+# Name used as a "virtual package" for temporarily installed packages.
|
|
|
+readonly VIRTUAL_PKG=".make-$PROGNAME"
|
|
|
+
|
|
|
+: ${APK:="apk"}
|
|
|
+: ${APK_OPTS:="--no-progress"}
|
|
|
+: ${APK_TOOLS_URI:="https://github.com/alpinelinux/apk-tools/releases/download/v2.9.1/apk-tools-2.9.1-x86_64-linux.tar.gz"}
|
|
|
+: ${APK_TOOLS_SHA256:="a0546d814a85fcc94a6e560360c4f1997119e40d79d9bc818f1571b2cf2ea5e9"}
|
|
|
+
|
|
|
+
|
|
|
+# Set pipefail if supported.
|
|
|
+if ( set -o pipefail 2>/dev/null ); then
|
|
|
+ set -o pipefail
|
|
|
+fi
|
|
|
+
|
|
|
+# For compatibility with systems that does not have "realpath" command.
|
|
|
+if ! command -v realpath >/dev/null; then
|
|
|
+ alias realpath='readlink -f'
|
|
|
+fi
|
|
|
+
|
|
|
+die() {
|
|
|
+ printf '\033[1;31mERROR:\033[0m %s\n' "$@" >&2 # bold red
|
|
|
+ exit 1
|
|
|
+}
|
|
|
+
|
|
|
+einfo() {
|
|
|
+ printf '\n\033[1;36m> %s\033[0m\n' "$@" >&2 # bold cyan
|
|
|
+}
|
|
|
+
|
|
|
+# Prints help and exists with the specified status.
|
|
|
+help() {
|
|
|
+ sed -En '/^#---help---/,/^#---help---/p' "$0" | sed -E 's/^# ?//; 1d;$d;'
|
|
|
+ exit ${1:-0}
|
|
|
+}
|
|
|
+
|
|
|
+# Cleans the host system. This function is executed before exiting the script.
|
|
|
+cleanup() {
|
|
|
+ set +eu
|
|
|
+ trap '' EXIT HUP INT TERM # unset trap to avoid loop
|
|
|
+
|
|
|
+ if [ "$HOST_DISTRO" = alpine ]; then
|
|
|
+ _apk del $VIRTUAL_PKG >&2
|
|
|
+ fi
|
|
|
+ if [ -d "$TEMP_DIR" ]; then
|
|
|
+ rm -Rf "$TEMP_DIR"
|
|
|
+ fi
|
|
|
+ if [ -d "$rootfs" ]; then
|
|
|
+ umount_recursively "$rootfs" \
|
|
|
+ || die "Failed to unmount mounts inside $rootfs!"
|
|
|
+ [ "$rootfs" = "$ROOTFS_DEST" ] || rm -Rf "$rootfs"
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+_apk() {
|
|
|
+ "$APK" $APK_OPTS "$@"
|
|
|
+}
|
|
|
+
|
|
|
+# Writes Alpine APK keys embedded in this script into directory $1.
|
|
|
+dump_alpine_keys() {
|
|
|
+ local dest_dir="$1"
|
|
|
+ local content file line
|
|
|
+
|
|
|
+ mkdir -p "$dest_dir"
|
|
|
+ for line in $ALPINE_KEYS; do
|
|
|
+ file=${line%%:*}
|
|
|
+ content=${line#*:}
|
|
|
+
|
|
|
+ printf "-----BEGIN PUBLIC KEY-----\n$content\n-----END PUBLIC KEY-----\n" \
|
|
|
+ > "$dest_dir/$file"
|
|
|
+ done
|
|
|
+}
|
|
|
+
|
|
|
+# Prepares chroot at the specified path.
|
|
|
+prepare_chroot() {
|
|
|
+ local dest="$1"
|
|
|
+
|
|
|
+ mkdir -p "$dest"/proc "$dest"/dev "$dest"/sys
|
|
|
+ mount -t proc none "$dest"/proc
|
|
|
+ mount --bind /dev "$dest"/dev
|
|
|
+ mount --bind /sys "$dest"/sys
|
|
|
+
|
|
|
+ install -D -m 644 /etc/resolv.conf "$dest"/etc/resolv.conf
|
|
|
+}
|
|
|
+
|
|
|
+# Sets up timezone $1 in Alpine rootfs.
|
|
|
+setup_timezone() {
|
|
|
+ local timezone="$1"
|
|
|
+ local rootfs="${2:-}"
|
|
|
+
|
|
|
+ _apk add --root "$rootfs" tzdata
|
|
|
+
|
|
|
+ install -D "$rootfs"/usr/share/zoneinfo/$timezone \
|
|
|
+ "$rootfs"/etc/zoneinfo/$timezone
|
|
|
+ ln -sf zoneinfo/$timezone "$rootfs"/etc/localtime
|
|
|
+
|
|
|
+ _apk del --root "$rootfs" tzdata
|
|
|
+}
|
|
|
+
|
|
|
+# Unmounts all filesystems under the directory tree $1 (must be absolute path).
|
|
|
+umount_recursively() {
|
|
|
+ local mount_point=$(realpath "$1")
|
|
|
+
|
|
|
+ cat /proc/mounts \
|
|
|
+ | cut -d ' ' -f 2 \
|
|
|
+ | { grep "^$mount_point" || true; } \
|
|
|
+ | sort -r \
|
|
|
+ | xargs -r -n 1 umount
|
|
|
+}
|
|
|
+
|
|
|
+# Downloads the specified file using wget and checks checksum.
|
|
|
+wgets() (
|
|
|
+ local url="$1"
|
|
|
+ local sha256="$2"
|
|
|
+ local dest="${3:-.}"
|
|
|
+
|
|
|
+ cd "$dest" \
|
|
|
+ && wget -T 10 --no-verbose "$url" \
|
|
|
+ && echo "$sha256 ${url##*/}" | sha256sum -c
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+#============================= M a i n ==============================#
|
|
|
+
|
|
|
+opts=$(getopt -n $PROGNAME -o b:m:Cp:r:cd:t:hV \
|
|
|
+ -l branch:,keys-dir:,mirror-uri:,no-cleanup,packages:,repositories-file:,script-chroot,temp-dir:,timezone:,help,version \
|
|
|
+ -- "$@") || help 1 >&2
|
|
|
+
|
|
|
+eval set -- "$opts"
|
|
|
+while [ $# -gt 0 ]; do
|
|
|
+ n=2
|
|
|
+ case "$1" in
|
|
|
+ -b | --branch) ALPINE_BRANCH="$2";;
|
|
|
+ --keys-dir) KEYS_DIR="$2";;
|
|
|
+ -m | --mirror-uri) ALPINE_MIRROR="$2";;
|
|
|
+ -C | --no-cleanup) CLEANUP='no'; n=1;;
|
|
|
+ -p | --packages) PACKAGES="${PACKAGES:-} $2";;
|
|
|
+ -r | --repositories-file) REPOS_FILE="$2";;
|
|
|
+ -c | --script-chroot) SCRIPT_CHROOT='yes'; n=1;;
|
|
|
+ -d | --temp-dir) TEMP_DIR="$2";;
|
|
|
+ -t | --timezone) TIMEZONE="$2";;
|
|
|
+ -h | --help) help 0;;
|
|
|
+ -V | --version) echo "$PROGNAME $VERSION"; exit 0;;
|
|
|
+ --) shift; break;;
|
|
|
+ esac
|
|
|
+ shift $n
|
|
|
+done
|
|
|
+
|
|
|
+[ $# -ne 0 ] || help 1 >&2
|
|
|
+
|
|
|
+ROOTFS_DEST="$1"; shift
|
|
|
+SCRIPT=
|
|
|
+[ $# -eq 0 ] || { SCRIPT=$(realpath "$1"); shift; }
|
|
|
+
|
|
|
+[ "$(id -u)" -eq 0 ] || die 'This script must be run as root!'
|
|
|
+
|
|
|
+[ ! -e "${TEMP_DIR:-}" ] || die "Temp path $TEMP_DIR must not exist!"
|
|
|
+
|
|
|
+: ${ALPINE_BRANCH:="v3.7"}
|
|
|
+: ${ALPINE_MIRROR:="https://nl.alpinelinux.org/alpine"}
|
|
|
+: ${CLEANUP:="yes"}
|
|
|
+: ${KEYS_DIR:="/etc/apk/keys"}
|
|
|
+: ${PACKAGES:=}
|
|
|
+: ${REPOS_FILE:="/etc/apk/repositories"}
|
|
|
+: ${SCRIPT_CHROOT:="no"}
|
|
|
+: ${TEMP_DIR:="$(mktemp -d /tmp/$PROGNAME.XXXXXX)"}
|
|
|
+: ${TIMEZONE:=}
|
|
|
+
|
|
|
+host_pkgs=''
|
|
|
+case "$ROOTFS_DEST" in
|
|
|
+ *.tar.bz2 | *.tbz) tar_opts='-cj';;
|
|
|
+ *.tar.gz | *.tgz) tar_opts='-cz';;
|
|
|
+ *.tar.xz) tar_opts='-cJ'; host_pkgs="$host_pkgs xz";;
|
|
|
+ *.tar | -) tar_opts='-c';;
|
|
|
+ *) tar_opts='';;
|
|
|
+esac
|
|
|
+
|
|
|
+rootfs="$ROOTFS_DEST"
|
|
|
+if [ "$ROOTFS_DEST" = '-' ]; then
|
|
|
+ rootfs="$TEMP_DIR/rootfs"
|
|
|
+elif [ "$tar_opts" ]; then
|
|
|
+ rootfs="${rootfs%.*}"
|
|
|
+ rootfs="${rootfs%.tar}"
|
|
|
+fi
|
|
|
+
|
|
|
+if [ -f /etc/os-release ]; then
|
|
|
+ : ${HOST_DISTRO:="$(. /etc/os-release && echo "$ID")"}
|
|
|
+else
|
|
|
+ : ${HOST_DISTRO:="unknown"}
|
|
|
+fi
|
|
|
+
|
|
|
+[ "$CLEANUP" = no ] || trap cleanup EXIT HUP INT TERM
|
|
|
+
|
|
|
+#-----------------------------------------------------------------------
|
|
|
+if [ "$HOST_DISTRO" = alpine ] && [ "$host_pkgs" ]; then
|
|
|
+ einfo "Installing $host_pkgs on host system"
|
|
|
+ _apk add -t $VIRTUAL_PKG $host_pkgs >&2
|
|
|
+fi
|
|
|
+
|
|
|
+#-----------------------------------------------------------------------
|
|
|
+if ! command -v "$APK" >/dev/null; then
|
|
|
+ einfo "$APK not found, downloading static apk-tools"
|
|
|
+
|
|
|
+ wgets "$APK_TOOLS_URI" "$APK_TOOLS_SHA256" "$TEMP_DIR"
|
|
|
+ tar -C "$TEMP_DIR" -xzf "$TEMP_DIR/${APK_TOOLS_URI##*/}"
|
|
|
+ APK="$(ls "$TEMP_DIR"/apk-tools-*/apk)"
|
|
|
+fi
|
|
|
+
|
|
|
+#-----------------------------------------------------------------------
|
|
|
+einfo 'Installing base system'
|
|
|
+
|
|
|
+mkdir -p "$rootfs"/etc/apk/keys
|
|
|
+
|
|
|
+if [ -f "$REPOS_FILE" ]; then
|
|
|
+ install -m 644 "$REPOS_FILE" "$rootfs"/etc/apk/repositories
|
|
|
+else
|
|
|
+ cat > "$rootfs"/etc/apk/repositories <<-EOF
|
|
|
+ $ALPINE_MIRROR/$ALPINE_BRANCH/main
|
|
|
+ $ALPINE_MIRROR/$ALPINE_BRANCH/community
|
|
|
+ EOF
|
|
|
+fi
|
|
|
+
|
|
|
+if [ -d "$KEYS_DIR" ]; then
|
|
|
+ cp "$KEYS_DIR"/* "$rootfs"/etc/apk/keys/
|
|
|
+else
|
|
|
+ dump_alpine_keys "$rootfs"/etc/apk/keys/
|
|
|
+fi
|
|
|
+
|
|
|
+_apk add --root "$rootfs" --update-cache --initdb $ALPINE_BASE_PKGS >&2
|
|
|
+
|
|
|
+# This package contains /etc/os-release, /etc/alpine-release and /etc/issue,
|
|
|
+# but we don't wanna install all its dependencies (e.g. openrc).
|
|
|
+_apk fetch --root "$rootfs" --stdout alpine-base \
|
|
|
+ | tar -xz -C "$rootfs" etc >&2
|
|
|
+ln -sf /run "$rootfs"/var/run
|
|
|
+
|
|
|
+_apk add --root "$rootfs" -t "$VIRTUAL_PKG" apk-tools >&2
|
|
|
+
|
|
|
+#-----------------------------------------------------------------------
|
|
|
+if [ "$TIMEZONE" ]; then
|
|
|
+ einfo "Setting timezone $TIMEZONE"
|
|
|
+ setup_timezone "$TIMEZONE" "$rootfs" >&2
|
|
|
+fi
|
|
|
+
|
|
|
+#-----------------------------------------------------------------------
|
|
|
+if [ "$PACKAGES" ]; then
|
|
|
+ einfo 'Installing additional packages'
|
|
|
+ _apk add --root "$rootfs" $PACKAGES >&2
|
|
|
+fi
|
|
|
+
|
|
|
+#-----------------------------------------------------------------------
|
|
|
+if [ "$SCRIPT" ]; then
|
|
|
+ script_name="${SCRIPT##*/}"
|
|
|
+
|
|
|
+ if [ "$SCRIPT_CHROOT" = 'no' ]; then
|
|
|
+ einfo "Executing script: $script_name $*"
|
|
|
+
|
|
|
+ ( cd "$rootfs" && "$SCRIPT" "$@" >&2 ) || die 'Script failed'
|
|
|
+ else
|
|
|
+ einfo "Executing script in chroot: $script_name $*"
|
|
|
+
|
|
|
+ prepare_chroot "$rootfs"
|
|
|
+ mount --bind "${SCRIPT%/*}" "$rootfs"/mnt
|
|
|
+ chroot "$rootfs" \
|
|
|
+ sh -c "cd /mnt && ./$script_name \"\$@\"" -- "$@" >&2 \
|
|
|
+ || die 'Script failed'
|
|
|
+ umount_recursively "$rootfs"
|
|
|
+ fi
|
|
|
+fi
|
|
|
+
|
|
|
+#-----------------------------------------------------------------------
|
|
|
+einfo 'Cleaning-up rootfs'
|
|
|
+
|
|
|
+_apk del --root "$rootfs" --purge "$VIRTUAL_PKG" >&2
|
|
|
+
|
|
|
+rm -Rf "$rootfs"/var/cache/apk "$rootfs"/etc/resolv.conf
|
|
|
+rm -Rf "$rootfs"/dev/*
|
|
|
+
|
|
|
+[ -f "$rootfs"/sbin/apk ] \
|
|
|
+ || rm -Rf "$rootfs"/etc/apk "$rootfs"/lib/apk
|
|
|
+
|
|
|
+for dir in $FUTILE_DIRS; do
|
|
|
+ rmdir -p "$rootfs$dir" 2>/dev/null || true
|
|
|
+done
|
|
|
+
|
|
|
+#-----------------------------------------------------------------------
|
|
|
+if [ "$tar_opts" ]; then
|
|
|
+ einfo 'Creating rootfs archive'
|
|
|
+
|
|
|
+ tar -C "$rootfs" $tar_opts --numeric-owner -f "$ROOTFS_DEST" .
|
|
|
+ ls -la "$ROOTFS_DEST" >&2
|
|
|
+fi
|