| #!/bin/bash |
| # |
| # Copyright (C) 2018 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| |
| set -e |
| set -u |
| |
| SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) |
| |
| usage() { |
| echo -n "usage: $0 [-h] [-s bullseye|bullseye-cuttlefish|bullseye-rockpi] " |
| echo -n "[-a i386|amd64|armhf|arm64] -k /path/to/kernel " |
| echo -n "-i /path/to/initramfs.gz [-d /path/to/dtb:subdir] " |
| echo "[-m http://mirror/debian] [-n rootfs] [-r initrd] [-e]" |
| exit 1 |
| } |
| |
| mirror=http://ftp.debian.org/debian |
| suite=bullseye |
| arch=amd64 |
| |
| embed_kernel_initrd_dtb= |
| dtb_subdir= |
| ramdisk= |
| rootfs= |
| dtb= |
| |
| while getopts ":hs:a:m:n:r:k:i:d:e" opt; do |
| case "${opt}" in |
| h) |
| usage |
| ;; |
| s) |
| if [[ "${OPTARG%-*}" != "bullseye" ]]; then |
| echo "Invalid suite: ${OPTARG}" >&2 |
| usage |
| fi |
| suite="${OPTARG}" |
| ;; |
| a) |
| arch="${OPTARG}" |
| ;; |
| m) |
| mirror="${OPTARG}" |
| ;; |
| n) |
| rootfs="${OPTARG}" |
| ;; |
| r) |
| ramdisk="${OPTARG}" |
| ;; |
| k) |
| kernel="${OPTARG}" |
| ;; |
| i) |
| initramfs="${OPTARG}" |
| ;; |
| d) |
| dtb="${OPTARG%:*}" |
| if [ "${OPTARG#*:}" != "${dtb}" ]; then |
| dtb_subdir="${OPTARG#*:}/" |
| fi |
| ;; |
| e) |
| embed_kernel_initrd_dtb=1 |
| ;; |
| \?) |
| echo "Invalid option: ${OPTARG}" >&2 |
| usage |
| ;; |
| :) |
| echo "Invalid option: ${OPTARG} requires an argument" >&2 |
| usage |
| ;; |
| esac |
| done |
| |
| # Disable Debian's "persistent" network device renaming |
| cmdline="net.ifnames=0 rw 8250.nr_uarts=2 PATH=/usr/sbin:/usr/bin" |
| |
| # Pass down embedding option, if specified |
| if [ -n "${embed_kernel_initrd_dtb}" ]; then |
| cmdline="${cmdline} embed_kernel_initrd_dtb=${embed_kernel_initrd_dtb}" |
| fi |
| |
| case "${arch}" in |
| i386) |
| cmdline="${cmdline} console=ttyS0 exitcode=/dev/ttyS1" |
| machine="pc-i440fx-2.8,accel=kvm" |
| qemu="qemu-system-i386" |
| cpu="max" |
| ;; |
| amd64) |
| cmdline="${cmdline} console=ttyS0 exitcode=/dev/ttyS1" |
| machine="pc-i440fx-2.8,accel=kvm" |
| qemu="qemu-system-x86_64" |
| cpu="max" |
| ;; |
| armhf) |
| cmdline="${cmdline} console=ttyAMA0 exitcode=/dev/ttyS0" |
| machine="virt,gic-version=2" |
| qemu="qemu-system-arm" |
| cpu="cortex-a15" |
| ;; |
| arm64) |
| cmdline="${cmdline} console=ttyAMA0 exitcode=/dev/ttyS0" |
| machine="virt,gic-version=2" |
| qemu="qemu-system-aarch64" |
| cpu="cortex-a53" # "max" is too slow |
| ;; |
| *) |
| echo "Invalid arch: ${OPTARG}" >&2 |
| usage |
| ;; |
| esac |
| |
| if [[ -z "${rootfs}" ]]; then |
| rootfs="rootfs.${arch}.${suite}.$(date +%Y%m%d)" |
| fi |
| rootfs=$(realpath "${rootfs}") |
| |
| if [[ -z "${ramdisk}" ]]; then |
| ramdisk="initrd.${arch}.${suite}.$(date +%Y%m%d)" |
| fi |
| ramdisk=$(realpath "${ramdisk}") |
| |
| if [[ -z "${kernel}" ]]; then |
| echo "$0: Path to kernel image must be specified (with '-k')" |
| usage |
| elif [[ ! -e "${kernel}" ]]; then |
| echo "$0: Kernel image not found at '${kernel}'" |
| exit 2 |
| fi |
| |
| if [[ -z "${initramfs}" ]]; then |
| echo "Path to initial ramdisk image must be specified (with '-i')" |
| usage |
| elif [[ ! -e "${initramfs}" ]]; then |
| echo "Initial ramdisk image not found at '${initramfs}'" |
| exit 3 |
| fi |
| |
| # Sometimes it isn't obvious when the script fails |
| failure() { |
| echo "Filesystem generation process failed." >&2 |
| rm -f "${rootfs}" "${ramdisk}" |
| } |
| trap failure ERR |
| |
| # Import the package list for this release |
| packages=$(cpp "${SCRIPT_DIR}/rootfs/${suite}.list" | grep -v "^#" | xargs | tr -s ' ' ',') |
| |
| # For the debootstrap intermediates |
| tmpdir=$(mktemp -d) |
| tmpdir_remove() { |
| echo "Removing temporary files.." >&2 |
| sudo rm -rf "${tmpdir}" |
| } |
| trap tmpdir_remove EXIT |
| |
| workdir="${tmpdir}/_" |
| mkdir "${workdir}" |
| chmod 0755 "${workdir}" |
| sudo chown root:root "${workdir}" |
| |
| # Run the debootstrap first |
| cd "${workdir}" |
| sudo debootstrap --arch="${arch}" --variant=minbase --include="${packages}" \ |
| --foreign "${suite%-*}" . "${mirror}" |
| |
| # Copy some bootstrapping scripts into the rootfs |
| sudo cp -a "${SCRIPT_DIR}"/rootfs/*.sh root/ |
| sudo cp -a "${SCRIPT_DIR}"/rootfs/net_test.sh sbin/net_test.sh |
| sudo chown root:root sbin/net_test.sh |
| |
| # Extract the ramdisk to bootstrap with to / |
| lz4 -lcd "${initramfs}" | sudo cpio -idum lib/modules/* |
| |
| # Create /host, for the pivot_root and 9p mount use cases |
| sudo mkdir host |
| |
| # Leave the workdir, to build the filesystem |
| cd - |
| |
| # For the initial ramdisk, and later for the final rootfs |
| mount=$(mktemp -d) |
| mount_remove() { |
| rmdir "${mount}" |
| tmpdir_remove |
| } |
| trap mount_remove EXIT |
| |
| # The initial ramdisk filesystem must be <=512M, or QEMU's -initrd |
| # option won't touch it |
| initrd=$(mktemp) |
| initrd_remove() { |
| rm -f "${initrd}" |
| mount_remove |
| } |
| trap initrd_remove EXIT |
| truncate -s 512M "${initrd}" |
| mke2fs -F -t ext3 -L ROOT "${initrd}" |
| |
| # Mount the new filesystem locally |
| sudo mount -o loop -t ext3 "${initrd}" "${mount}" |
| image_unmount() { |
| sudo umount "${mount}" |
| initrd_remove |
| } |
| trap image_unmount EXIT |
| |
| # Copy the patched debootstrap results into the new filesystem |
| sudo cp -a "${workdir}"/* "${mount}" |
| sudo rm -rf "${workdir}" |
| |
| # Unmount the initial ramdisk |
| sudo umount "${mount}" |
| trap initrd_remove EXIT |
| |
| # Copy the initial ramdisk to the final rootfs name and extend it |
| sudo cp -a "${initrd}" "${rootfs}" |
| truncate -s 2G "${rootfs}" |
| e2fsck -p -f "${rootfs}" || true |
| resize2fs "${rootfs}" |
| |
| # Create another fake block device for initrd.img writeout |
| raw_initrd=$(mktemp) |
| raw_initrd_remove() { |
| rm -f "${raw_initrd}" |
| initrd_remove |
| } |
| trap raw_initrd_remove EXIT |
| truncate -s 64M "${raw_initrd}" |
| |
| # Complete the bootstrap process using QEMU and the specified kernel |
| ${qemu} -machine "${machine}" -cpu "${cpu}" -m 2048 >&2 \ |
| -kernel "${kernel}" -initrd "${initrd}" -no-user-config -nodefaults \ |
| -no-reboot -display none -nographic -serial stdio -parallel none \ |
| -smp 8,sockets=8,cores=1,threads=1 \ |
| -object rng-random,id=objrng0,filename=/dev/urandom \ |
| -device virtio-rng-pci-non-transitional,rng=objrng0,id=rng0,max-bytes=1024,period=2000 \ |
| -drive file="${rootfs}",format=raw,if=none,aio=threads,id=drive-virtio-disk0 \ |
| -device virtio-blk-pci-non-transitional,scsi=off,drive=drive-virtio-disk0 \ |
| -drive file="${raw_initrd}",format=raw,if=none,aio=threads,id=drive-virtio-disk1 \ |
| -device virtio-blk-pci-non-transitional,scsi=off,drive=drive-virtio-disk1 \ |
| -chardev file,id=exitcode,path=exitcode \ |
| -device pci-serial,chardev=exitcode \ |
| -append "root=/dev/ram0 ramdisk_size=524288 init=/root/stage1.sh ${cmdline}" |
| [[ -s exitcode ]] && exitcode=$(cat exitcode | tr -d '\r') || exitcode=2 |
| rm -f exitcode |
| if [ "${exitcode}" != "0" ]; then |
| echo "Second stage debootstrap failed (err=${exitcode})" |
| exit "${exitcode}" |
| fi |
| |
| # Fix up any issues from the unclean shutdown |
| e2fsck -p -f "${rootfs}" || true |
| |
| # New workdir for the initrd extraction |
| workdir="${tmpdir}/initrd" |
| mkdir "${workdir}" |
| chmod 0755 "${workdir}" |
| sudo chown root:root "${workdir}" |
| |
| # Change into workdir to repack initramfs |
| cd "${workdir}" |
| |
| # Process the initrd to remove kernel-specific metadata |
| kernel_version=$(basename $(lz4 -lcd "${raw_initrd}" | sudo cpio -idumv 2>&1 | grep usr/lib/modules/ - | head -n1)) |
| sudo rm -rf usr/lib/modules |
| sudo mkdir -p usr/lib/modules |
| |
| # Debian symlinks /usr/lib to /lib, but we'd prefer the other way around |
| # so that it more closely matches what happens in Android initramfs images. |
| # This enables 'cat ramdiskA.img ramdiskB.img >ramdiskC.img' to "just work". |
| sudo rm -f lib |
| sudo mv usr/lib lib |
| sudo ln -s /lib usr/lib |
| |
| # Repack the ramdisk to the final output |
| find * | sudo cpio -H newc -o --quiet | lz4 -lc9 >"${ramdisk}" |
| |
| # Pack another ramdisk with the combined artifacts, for boot testing |
| cat "${ramdisk}" "${initramfs}" >"${initrd}" |
| |
| # Leave workdir to boot-test combined initrd |
| cd - |
| |
| # Mount the new filesystem locally |
| sudo mount -o loop -t ext3 "${rootfs}" "${mount}" |
| image_unmount2() { |
| sudo umount "${mount}" |
| raw_initrd_remove |
| } |
| trap image_unmount2 EXIT |
| |
| # Embed the kernel and dtb images now, if requested |
| if [ -n "${embed_kernel_initrd_dtb}" ]; then |
| if [ -n "${dtb}" ]; then |
| sudo mkdir -p "${mount}/boot/dtb/${dtb_subdir}" |
| sudo cp -a "${dtb}" "${mount}/boot/dtb/${dtb_subdir}" |
| sudo chown -R root:root "${mount}/boot/dtb/${dtb_subdir}" |
| fi |
| sudo cp -a "${kernel}" "${mount}/boot/vmlinuz-${kernel_version}" |
| sudo chown root:root "${mount}/boot/vmlinuz-${kernel_version}" |
| fi |
| |
| # Unmount the initial ramdisk |
| sudo umount "${mount}" |
| trap raw_initrd_remove EXIT |
| |
| # Boot test the new system and run stage 3 |
| ${qemu} -machine "${machine}" -cpu "${cpu}" -m 2048 >&2 \ |
| -kernel "${kernel}" -initrd "${initrd}" -no-user-config -nodefaults \ |
| -no-reboot -display none -nographic -serial stdio -parallel none \ |
| -smp 8,sockets=8,cores=1,threads=1 \ |
| -object rng-random,id=objrng0,filename=/dev/urandom \ |
| -device virtio-rng-pci-non-transitional,rng=objrng0,id=rng0,max-bytes=1024,period=2000 \ |
| -drive file="${rootfs}",format=raw,if=none,aio=threads,id=drive-virtio-disk0 \ |
| -device virtio-blk-pci-non-transitional,scsi=off,drive=drive-virtio-disk0 \ |
| -chardev file,id=exitcode,path=exitcode \ |
| -device pci-serial,chardev=exitcode \ |
| -netdev user,id=usernet0,ipv6=off \ |
| -device virtio-net-pci-non-transitional,netdev=usernet0,id=net0 \ |
| -append "root=LABEL=ROOT init=/root/${suite}.sh ${cmdline}" |
| [[ -s exitcode ]] && exitcode=$(cat exitcode | tr -d '\r') || exitcode=2 |
| rm -f exitcode |
| if [ "${exitcode}" != "0" ]; then |
| echo "Root filesystem finalization failed (err=${exitcode})" |
| exit "${exitcode}" |
| fi |
| |
| # Fix up any issues from the unclean shutdown |
| e2fsck -p -f "${rootfs}" || true |
| |
| # Mount the final rootfs locally |
| sudo mount -o loop -t ext3 "${rootfs}" "${mount}" |
| image_unmount3() { |
| sudo umount "${mount}" |
| raw_initrd_remove |
| } |
| trap image_unmount3 EXIT |
| |
| # Fill the rest of the space with zeroes, to optimize compression |
| sudo dd if=/dev/zero of="${mount}/sparse" bs=1M 2>/dev/null || true |
| sudo rm -f "${mount}/sparse" |
| |
| echo "Debian ${suite} for ${arch} filesystem generated at '${rootfs}'." |
| echo "Initial ramdisk generated at '${ramdisk}'." |