alpine-make-rootfs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. #!/bin/sh
  2. # vim: set ts=4:
  3. #---help---
  4. # Usage: alpine-make-rootfs [options] [--] <dest> [<script> [<script-opts...>]]
  5. #
  6. # This script creates Alpine Linux rootfs for containers. It must be run as
  7. # root - to create files with correct permissions and use chroot (optional).
  8. # If $APK is not available on the host system, then static apk-tools
  9. # specified by $APK_TOOLS_URI is downloaded and used.
  10. #
  11. # Arguments:
  12. # <dest> Path where to write the rootfs. It may be:
  13. # - path with suffix .tar, .tar.bz2, .tbz, .tar.gz, .tgz,
  14. # or tar.xz to create a TAR archive;
  15. # - other path to keep rootfs as a directory;
  16. # - or "-" to dump TAR archive (w/o compression) to STDOUT.
  17. #
  18. # <script> Path of script to execute after installing base system in
  19. # the prepared rootfs and before clean-up. Use "-" to read
  20. # the script from STDIN; if it doesn't start with a shebang,
  21. # "#!/bin/sh -e" is prepended.
  22. #
  23. # <script-opts> Arguments to pass to the script.
  24. #
  25. # Options and Environment Variables:
  26. # -b --branch ALPINE_BRANCH Alpine branch to install; used only when
  27. # --repositories-file is not specified. Default is v3.9.
  28. #
  29. # --keys-dir KEYS_DIR Path of directory with Alpine keys to copy into
  30. # the rootfs. Default is /etc/apk/keys. If does not exist,
  31. # keys for x86_64 embedded in this script will be used.
  32. #
  33. # -m --mirror-uri ALPINE_MIRROR URI of the Aports mirror to fetch packages; used only
  34. # when --repositories-file is not specified. Default is
  35. # https://nl.alpinelinux.org/alpine.
  36. #
  37. # -C --no-cleanup (CLEANUP) Don't umount and remove temporary directories when done.
  38. #
  39. # -p --packages PACKAGES Additional packages to install into the rootfs.
  40. #
  41. # -r --repositories-file REPOS_FILE Path of repositories file to copy into the rootfs.
  42. # Default is /etc/apk/repositories. If does not exist,
  43. # repositories file with Alpine's main and community
  44. # repositories on --mirror-uri is created.
  45. #
  46. # -c --script-chroot (SCRIPT_CHROOT) Bind <script>'s directory at /mnt inside the rootfs dir
  47. # and chroot into the rootfs before executing <script>.
  48. # Otherwise <script> is executed in the current directory
  49. # and $ROOTFS variable points to the rootfs directory.
  50. #
  51. # -d --temp-dir TEMP_DIR Path where to create a temporary directory; used for
  52. # downloading apk-tools when not available on the host
  53. # sytem or for rootfs when <dest> is "-" (i.e. STDOUT).
  54. # This path must not exist! Defaults to using `mkdir -d`.
  55. #
  56. # -t --timezone TIMEZONE Timezone to set (e.g. Europe/Prague). Default is to leave
  57. # timezone UTC.
  58. #
  59. # -h --help Show this help message and exit.
  60. #
  61. # -v --version Print version and exit.
  62. #
  63. # APK APK command to use. Default is "apk".
  64. #
  65. # APK_OPTS Options to pass into apk on each execution.
  66. # Default is "--no-progress".
  67. #
  68. # APK_TOOLS_URI URL of static apk-tools tarball to download if $APK is
  69. # not found on the host system. Default is x86_64 apk-tools
  70. # from https://github.com/alpinelinux/apk-tools/releases.
  71. #
  72. # APK_TOOLS_SHA256 SHA-256 checksum of $APK_TOOLS_URI.
  73. #
  74. # Each option can be also provided by environment variable. If both option and
  75. # variable is specified and the option accepts only one argument, then the
  76. # option takes precedence.
  77. #
  78. # https://github.com/alpinelinux/alpine-make-rootfs
  79. #---help---
  80. set -eu
  81. readonly PROGNAME='alpine-make-rootfs'
  82. readonly VERSION='0.3.0'
  83. # Base Alpine packages to install in rootfs.
  84. readonly ALPINE_BASE_PKGS='alpine-baselayout busybox busybox-suid musl-utils'
  85. # Alpine APK keys for verification of packages for x86_64.
  86. readonly ALPINE_KEYS='
  87. 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
  88. 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
  89. '
  90. # List of directories to remove when empty.
  91. readonly FUTILE_DIRS='
  92. /home /media/cdrom /media/floppy /media/usb /mnt /srv /usr/local/bin
  93. /usr/local/lib /usr/local/share
  94. '
  95. # An opaque string used to detect changes in resolv.conf.
  96. readonly RESOLVCONF_MARK="### created by $PROGNAME ###"
  97. # Name used as a "virtual package" for temporarily installed packages.
  98. readonly VIRTUAL_PKG=".make-$PROGNAME"
  99. : ${APK:="apk"}
  100. : ${APK_OPTS:="--no-progress"}
  101. : ${APK_TOOLS_URI:="https://github.com/alpinelinux/apk-tools/releases/download/v2.10.3/apk-tools-2.10.3-x86_64-linux.tar.gz"}
  102. : ${APK_TOOLS_SHA256:="4d0b2cda606720624589e6171c374ec6d138867e03576d9f518dddde85c33839"}
  103. # Set pipefail if supported.
  104. if ( set -o pipefail 2>/dev/null ); then
  105. set -o pipefail
  106. fi
  107. # For compatibility with systems that does not have "realpath" command.
  108. if ! command -v realpath >/dev/null; then
  109. alias realpath='readlink -f'
  110. fi
  111. die() {
  112. printf '\033[1;31mERROR:\033[0m %s\n' "$@" >&2 # bold red
  113. exit 1
  114. }
  115. einfo() {
  116. printf '\n\033[1;36m> %s\033[0m\n' "$@" >&2 # bold cyan
  117. }
  118. # Prints help and exists with the specified status.
  119. help() {
  120. sed -En '/^#---help---/,/^#---help---/p' "$0" | sed -E 's/^# ?//; 1d;$d;'
  121. exit ${1:-0}
  122. }
  123. # Cleans the host system. This function is executed before exiting the script.
  124. cleanup() {
  125. set +eu
  126. trap '' EXIT HUP INT TERM # unset trap to avoid loop
  127. if [ "$INSTALL_HOST_PKGS" = yes ]; then
  128. _apk del $VIRTUAL_PKG >&2
  129. fi
  130. if [ -d "$TEMP_DIR" ]; then
  131. rm -Rf "$TEMP_DIR"
  132. fi
  133. if [ -d "$rootfs" ]; then
  134. umount_recursively "$rootfs" \
  135. || die "Failed to unmount mounts inside $rootfs!"
  136. [ "$rootfs" = "$ROOTFS_DEST" ] || rm -Rf "$rootfs"
  137. fi
  138. }
  139. _apk() {
  140. "$APK" $APK_OPTS "$@"
  141. }
  142. # Writes Alpine APK keys embedded in this script into directory $1.
  143. dump_alpine_keys() {
  144. local dest_dir="$1"
  145. local content file line
  146. mkdir -p "$dest_dir"
  147. for line in $ALPINE_KEYS; do
  148. file=${line%%:*}
  149. content=${line#*:}
  150. printf -- "-----BEGIN PUBLIC KEY-----\n$content\n-----END PUBLIC KEY-----\n" \
  151. > "$dest_dir/$file"
  152. done
  153. }
  154. # Binds the directory $1 at the mountpoint $2 and sets propagation to private.
  155. mount_bind() {
  156. mkdir -p "$2"
  157. mount --bind "$1" "$2"
  158. mount --make-private "$2"
  159. }
  160. # Prepares chroot at the specified path.
  161. prepare_chroot() {
  162. local dest="$1"
  163. mkdir -p "$dest"/proc
  164. mount -t proc none "$dest"/proc
  165. mount_bind /dev "$dest"/dev
  166. mount_bind /sys "$dest"/sys
  167. install -D -m 644 /etc/resolv.conf "$dest"/etc/resolv.conf
  168. echo "$RESOLVCONF_MARK" >> "$dest"/etc/resolv.conf
  169. }
  170. # Sets up timezone $1 in Alpine rootfs.
  171. setup_timezone() {
  172. local timezone="$1"
  173. local rootfs="${2:-}"
  174. _apk add --root "$rootfs" tzdata
  175. install -D "$rootfs"/usr/share/zoneinfo/$timezone \
  176. "$rootfs"/etc/zoneinfo/$timezone
  177. ln -sf zoneinfo/$timezone "$rootfs"/etc/localtime
  178. _apk del --root "$rootfs" tzdata
  179. }
  180. # Unmounts all filesystems under the directory tree $1 (must be absolute path).
  181. umount_recursively() {
  182. local mount_point=$(realpath "$1")
  183. cat /proc/mounts \
  184. | cut -d ' ' -f 2 \
  185. | { grep "^$mount_point" || true; } \
  186. | sort -r \
  187. | xargs -r -n 1 umount
  188. }
  189. # Downloads the specified file using wget and checks checksum.
  190. wgets() (
  191. local url="$1"
  192. local sha256="$2"
  193. local dest="${3:-.}"
  194. cd "$dest" \
  195. && wget -T 10 --no-verbose "$url" \
  196. && echo "$sha256 ${url##*/}" | sha256sum -c
  197. )
  198. # Writes STDIN into file $1 and sets it executable bit. If the content does not
  199. # start with a shebang, prepends "#!/bin/sh -e" before the first line.
  200. write_script() {
  201. local filename="$1"
  202. cat > "$filename.tmp"
  203. if ! grep -q -m 1 '^#!' "$filename.tmp"; then
  204. echo "#!/bin/sh -e" > "$filename"
  205. fi
  206. cat "$filename.tmp" >> "$filename"
  207. rm "$filename.tmp"
  208. chmod +x "$filename"
  209. }
  210. #============================= M a i n ==============================#
  211. opts=$(getopt -n $PROGNAME -o b:m:Cp:r:cd:t:hV \
  212. -l branch:,keys-dir:,mirror-uri:,no-cleanup,packages:,repositories-file:,script-chroot,temp-dir:,timezone:,help,version \
  213. -- "$@") || help 1 >&2
  214. eval set -- "$opts"
  215. while [ $# -gt 0 ]; do
  216. n=2
  217. case "$1" in
  218. -b | --branch) ALPINE_BRANCH="$2";;
  219. --keys-dir) KEYS_DIR="$2";;
  220. -m | --mirror-uri) ALPINE_MIRROR="$2";;
  221. -C | --no-cleanup) CLEANUP='no'; n=1;;
  222. -p | --packages) PACKAGES="${PACKAGES:-} $2";;
  223. -r | --repositories-file) REPOS_FILE="$2";;
  224. -c | --script-chroot) SCRIPT_CHROOT='yes'; n=1;;
  225. -d | --temp-dir) TEMP_DIR="$2";;
  226. -t | --timezone) TIMEZONE="$2";;
  227. -h | --help) help 0;;
  228. -V | --version) echo "$PROGNAME $VERSION"; exit 0;;
  229. --) shift; break;;
  230. esac
  231. shift $n
  232. done
  233. [ $# -ne 0 ] || help 1 >&2
  234. ROOTFS_DEST="$1"; shift
  235. SCRIPT=${1:-}; shift || true
  236. [ "$(id -u)" -eq 0 ] || die 'This script must be run as root!'
  237. [ ! -e "${TEMP_DIR:-}" ] || die "Temp path $TEMP_DIR must not exist!"
  238. : ${ALPINE_BRANCH:="v3.9"}
  239. : ${ALPINE_MIRROR:="https://nl.alpinelinux.org/alpine"}
  240. : ${CLEANUP:="yes"}
  241. : ${KEYS_DIR:="/etc/apk/keys"}
  242. : ${PACKAGES:=}
  243. : ${REPOS_FILE:="/etc/apk/repositories"}
  244. : ${SCRIPT_CHROOT:="no"}
  245. : ${TEMP_DIR:="$(mktemp -d /tmp/$PROGNAME.XXXXXX)"}
  246. : ${TIMEZONE:=}
  247. host_pkgs=''
  248. case "$ROOTFS_DEST" in
  249. *.tar.bz2 | *.tbz) tar_opts='-cj';;
  250. *.tar.gz | *.tgz) tar_opts='-cz';;
  251. *.tar.xz) tar_opts='-cJ'; host_pkgs="$host_pkgs xz";;
  252. *.tar | -) tar_opts='-c';;
  253. *) tar_opts='';;
  254. esac
  255. rootfs="$ROOTFS_DEST"
  256. if [ "$ROOTFS_DEST" = '-' ]; then
  257. rootfs="$TEMP_DIR/rootfs"
  258. elif [ "$tar_opts" ]; then
  259. rootfs="${rootfs%.*}"
  260. rootfs="${rootfs%.tar}"
  261. fi
  262. if [ "$SCRIPT" = '-' ]; then
  263. SCRIPT="$TEMP_DIR/setup.sh"
  264. write_script "$SCRIPT"
  265. fi
  266. if [ "$SCRIPT" ]; then
  267. SCRIPT=$(realpath "$SCRIPT")
  268. fi
  269. if [ -f /etc/alpine-release ]; then
  270. : ${INSTALL_HOST_PKGS:="yes"}
  271. else
  272. : ${INSTALL_HOST_PKGS:="no"}
  273. fi
  274. [ "$CLEANUP" = no ] || trap cleanup EXIT HUP INT TERM
  275. #-----------------------------------------------------------------------
  276. if [ "$INSTALL_HOST_PKGS" = yes ] && [ "$host_pkgs" ]; then
  277. einfo "Installing $host_pkgs on host system"
  278. _apk add -t $VIRTUAL_PKG $host_pkgs >&2
  279. fi
  280. #-----------------------------------------------------------------------
  281. if ! command -v "$APK" >/dev/null; then
  282. einfo "$APK not found, downloading static apk-tools"
  283. wgets "$APK_TOOLS_URI" "$APK_TOOLS_SHA256" "$TEMP_DIR"
  284. tar -C "$TEMP_DIR" -xzf "$TEMP_DIR/${APK_TOOLS_URI##*/}"
  285. APK="$(ls "$TEMP_DIR"/apk-tools-*/apk)"
  286. fi
  287. #-----------------------------------------------------------------------
  288. einfo 'Installing base system'
  289. mkdir -p "$rootfs"/etc/apk/keys
  290. if [ -f "$REPOS_FILE" ]; then
  291. install -m 644 "$REPOS_FILE" "$rootfs"/etc/apk/repositories
  292. else
  293. cat > "$rootfs"/etc/apk/repositories <<-EOF
  294. $ALPINE_MIRROR/$ALPINE_BRANCH/main
  295. $ALPINE_MIRROR/$ALPINE_BRANCH/community
  296. EOF
  297. fi
  298. if [ -d "$KEYS_DIR" ]; then
  299. cp "$KEYS_DIR"/* "$rootfs"/etc/apk/keys/
  300. else
  301. dump_alpine_keys "$rootfs"/etc/apk/keys/
  302. fi
  303. _apk add --root "$rootfs" --update-cache --initdb $ALPINE_BASE_PKGS >&2
  304. # This package contains /etc/os-release, /etc/alpine-release and /etc/issue,
  305. # but we don't wanna install all its dependencies (e.g. openrc).
  306. _apk fetch --root "$rootfs" --stdout alpine-base \
  307. | tar -xz -C "$rootfs" etc >&2
  308. ln -sf /run "$rootfs"/var/run
  309. _apk add --root "$rootfs" -t "$VIRTUAL_PKG" apk-tools >&2
  310. #-----------------------------------------------------------------------
  311. if [ "$TIMEZONE" ]; then
  312. einfo "Setting timezone $TIMEZONE"
  313. setup_timezone "$TIMEZONE" "$rootfs" >&2
  314. fi
  315. #-----------------------------------------------------------------------
  316. if [ "$PACKAGES" ]; then
  317. einfo 'Installing additional packages'
  318. _apk add --root "$rootfs" $PACKAGES >&2
  319. fi
  320. #-----------------------------------------------------------------------
  321. if [ "$SCRIPT" ]; then
  322. script_name="${SCRIPT##*/}"
  323. if [ "$SCRIPT_CHROOT" = 'no' ]; then
  324. einfo "Executing script: $script_name $*"
  325. ROOTFS="$rootfs" "$SCRIPT" "$@" >&2 || die 'Script failed'
  326. else
  327. einfo "Executing script in chroot: $script_name $*"
  328. prepare_chroot "$rootfs"
  329. mount_bind "${SCRIPT%/*}" "$rootfs"/mnt
  330. chroot "$rootfs" \
  331. sh -c "cd /mnt && ./$script_name \"\$@\"" -- "$@" >&2 \
  332. || die 'Script failed'
  333. umount_recursively "$rootfs"
  334. fi
  335. fi
  336. #-----------------------------------------------------------------------
  337. einfo 'Cleaning-up rootfs'
  338. _apk del --root "$rootfs" --purge "$VIRTUAL_PKG" >&2
  339. if grep -qw "$RESOLVCONF_MARK" "$rootfs"/etc/resolv.conf 2>/dev/null; then
  340. rm "$rootfs"/etc/resolv.conf
  341. fi
  342. rm -Rf "$rootfs"/var/cache/apk
  343. rm -Rf "$rootfs"/dev/*
  344. [ -f "$rootfs"/sbin/apk ] \
  345. || rm -Rf "$rootfs"/etc/apk "$rootfs"/lib/apk
  346. for dir in $FUTILE_DIRS; do
  347. rmdir -p "$rootfs$dir" 2>/dev/null || true
  348. done
  349. #-----------------------------------------------------------------------
  350. if [ "$tar_opts" ]; then
  351. einfo 'Creating rootfs archive'
  352. tar -C "$rootfs" $tar_opts --numeric-owner -f "$ROOTFS_DEST" .
  353. if [ -f "$ROOTFS_DEST" ] && [ "${SUDO_UID:-}" ] && [ "${SUDO_GID:-}" ]; then
  354. chown "$SUDO_UID:$SUDO_GID" "$ROOTFS_DEST"
  355. fi
  356. ls -la "$ROOTFS_DEST" >&2
  357. fi