termina_build_image: rewrite to build from CrOS disk images

BUG=chromium:703919
TEST=./termina_build_image --image=chromiumos_test_image.bin --output=tatl

Change-Id: I766e912c02463fd9babd60bdf04fee5f79929140
Reviewed-on: https://chromium-review.googlesource.com/508056
Commit-Ready: Stephen Barber <smbarber@chromium.org>
Tested-by: Stephen Barber <smbarber@chromium.org>
Reviewed-by: Chirantan Ekbote <chirantan@chromium.org>
diff --git a/common.sh b/common.sh
index 91d8e5d..1e6d721 100644
--- a/common.sh
+++ b/common.sh
@@ -1338,6 +1338,30 @@
   fi
 }
 
+# Display a prompt that asks the user to choose yes or no.
+# $1 - The prompt to be displayed to the user, with " [y/N]: " appended.
+#
+# Usage example:
+#
+#  prompt_yesno "Would you like a cup of tea?"
+#
+# The function will return 0 for yes and 1 for no, appropriate for using
+# in an if statement or loop.
+prompt_yesno() {
+  local prompt=$1
+  local reply
+
+  assert_interactive
+  read -p "${prompt} [y/N]: " reply
+
+  # Be strict. No is the default.
+  if [[ "${reply}" != y && "${reply}" != Y ]]; then
+    return 1
+  fi
+
+  return 0
+}
+
 # Display --help if requested. This is used to hide options from help
 # that are not intended for developer use.
 #
diff --git a/termina_build_image b/termina_build_image
index a23bebb..1770084 100755
--- a/termina_build_image
+++ b/termina_build_image
@@ -5,135 +5,221 @@
 
 SCRIPT_ROOT=$(dirname "$(readlink -f "$0")")
 . "${SCRIPT_ROOT}/build_library/build_common.sh" || exit 1
+. "${SCRIPT_ROOT}/build_library/filesystem_util.sh" || exit 1
 
 assert_inside_chroot "$@"
 
-DEFINE_string board "${DEFAULT_BOARD}" \
-  "The board to build an image for."
-DEFINE_boolean dev "${FLAGS_FALSE}" \
-  "Include dev packages and allow serial login."
-DEFINE_boolean test "${FLAGS_FALSE}" \
-  "Include test and dev packages, and allow serial login."
-DEFINE_string output_root "${DEFAULT_BUILD_ROOT}/images" \
-  "Directory in which to place image result directories (named by version)"
+DEFINE_string arch "amd64" \
+  "Architecture of the VM image"
+DEFINE_string filesystem "ext4" \
+  "Filesystem for the rootfs image"
+DEFINE_string image "" \
+  "Chromium OS disk image to build the Termina image from"
+DEFINE_string output "" \
+  "Output directory"
+DEFINE_boolean upload ${FLAGS_FALSE} \
+  "Upload resulting image to Google Storage" u
+DEFINE_boolean test_image ${FLAGS_FALSE} \
+  "True if the image is a test image" t
 
 FLAGS_HELP="USAGE: ${SCRIPT_NAME} [flags]
 
-To build a tatl dev image, try:
-$ ${SCRIPT_NAME} --board=tatl --dev
+To build a tatl test image, try:
+$ ./build_image --board=tatl test
+$ ${SCRIPT_NAME} --image=../build/images/tatl/latest/chromiumos_test_image.bin --output=tatl
 "
 FLAGS "$@" || exit 1
 eval set -- "${FLAGS_ARGV}"
 switch_to_strict_mode
 
-IMAGE_TYPE_BASE="base"
-IMAGE_TYPE_DEV="dev"
-IMAGE_TYPE_TEST="test"
+get_version() {
+  local output_dir="$1"
+  awk -F'=' -v key='CHROMEOS_RELEASE_VERSION' '$1==key { print $2 }' \
+    "${output_dir}"/lsb-release
+}
 
-# Produce the VM image.
-build_vm_image() {
-  local image_name="$1"
-  local image_type="$2"
-  local extra=()
-  local ROOTDIR="${OUTPUT_DIR}/rootfs"
-  local image_suffix=""
+is_test_image() {
+  local output_dir="$1"
+  grep -q testimage-channel "${output_dir}"/lsb-release
+}
 
-  case "${image_type}" in
-    "${IMAGE_TYPE_TEST}")
-      extra+=( "virtual/target-termina-os-dev" )
-      extra+=( "virtual/target-termina-os-test" )
-      image_suffix="-test"
-      ;;
-    "${IMAGE_TYPE_DEV}")
-      extra+=( "virtual/target-termina-os-dev" )
-      image_suffix="-dev"
-      ;;
+read_le_int() {
+  local disk="$1"
+  local offset="$2"
+  local size="$3"
+
+  case "${size}" in
+  1 | 2 | 4 | 8)
+    ;;
+  *) die "${size} is not a valid int size to read"
+    ;;
   esac
 
-  export INSTALL_MASK="${DEFAULT_INSTALL_MASK}"
+  local raw="$(xxd -g ${size} -e -s ${offset} -l ${size} ${disk} | cut -d' ' -f2)"
+  local result="$(( 16#${raw} ))"
 
-  mkdir -p "${OUTPUT_DIR}"
-
-  install_with_root_deps "${ROOTDIR}" "virtual/target-termina-os" \
-                                      "sys-libs/gcc-libs" \
-                                      "${extra[@]}"
-
-  info "Installing C library ... "
-  install_libc "${ROOTDIR}"
-
-  info "Cleaning excess files ... "
-  sudo rm -rf "${ROOTDIR}"/var "${ROOTDIR}"/usr/lib*/gconv/ \
-    "${ROOTDIR}"/sbin/ldconfig "${ROOTDIR}"/boot "${ROOTDIR}"/lib/firmware
-  # Remove empty directories from the rootfs.
-  sudo find "${ROOTDIR}"/ -type d -depth -exec rmdir {} + 2>/dev/null || :
-
-  info "Creating top level dirs and socket dirs ... "
-  sudo mkdir -p "${ROOTDIR}"/{dev,proc,root,sys,home/user,run,tmp,var}
-
-  info "Creating container mounts ..."
-  sudo mkdir -p "${ROOTDIR}"/mnt/{container_rootfs,container_private}
-
-  info "Installing container pubkey ..."
-  "${VBOOT_SIGNING_DIR}"/insert_container_publickey.sh \
-    "${ROOTDIR}" \
-    "${VBOOT_DEVKEYS_DIR}"/cros-oci-container-pub.pem
-
-  info "Generating squashfs file ... "
-  local args=(
-    -all-root
-    -noappend
-  )
-  local img="${OUTPUT_DIR}/rootfs${image_suffix}.bin"
-  sudo mksquashfs "${ROOTDIR}" "${img}" "${args[@]}"
-  sudo chown $(id -u):$(id -g) "${img}"
-
-  info "Copying in kernel ..."
-  cp /build/"${FLAGS_board}"/boot/vmlinuz \
-     "${OUTPUT_DIR}/termina-kernel${image_suffix}.bin"
-
-  info "Cleaning up ... "
-  sudo rm -rf "${ROOTDIR}"
+  echo "${result}"
 }
 
-run_emerge() {
-  emerge-${BOARD} \
-    --quiet --jobs ${NUM_JOBS} \
-    --usepkgonly \
-    "$@"
+extract_squashfs_partition() {
+  local src_disk="$1"
+  local src_part="$2"
+  local dst_file="$3"
+
+  local part_start_blks="$(cgpt show -i "${src_part}" -b "${src_disk}")"
+  local part_start_bytes="$(( part_start_blks * 512 ))"
+  local part_size_blks="$(cgpt show -i "${src_part}" -s "${src_disk}")"
+  local part_size_bytes="$(( part_size_blks * 512 ))"
+
+  # To be sure we're extracting a squashfs partition, verify the magic.
+  # See fs/squashfs/squashfs_fs.h.
+  local magic="$(read_le_int ${src_disk} ${part_start_bytes} 4)"
+  if [[ "${magic}" -ne 0x73717368 ]]; then
+    die "Partition ${src_part} doesn't look like a squashfs partition"
+  fi
+
+  dd if="${src_disk}" of="${dst_file}" skip="${part_start_bytes}c" \
+    count="${part_size_bytes}c" iflag=skip_bytes,count_bytes
 }
 
-# Emerge with root deps.
-install_with_root_deps() {
-  local root_dir="$1"
-  shift
-  info "Installing '$*' with root deps ... "
-  run_emerge --root="${root_dir}" "$@" --root-deps=rdeps
+# Repack termina rootfs.
+repack_rootfs() {
+  local output_dir="$1"
+  local fs_type="$2"
+  local rootfs_img="${output_dir}/vm_rootfs.img"
+  local stateful_img="${output_dir}/vm_stateful.img"
+
+  # Create image in a temporary directory to avoid the need for extra space
+  # on the final rootfs.
+  local rootfs="${output_dir}/rootfs"
+  local stateful="${output_dir}/stateful"
+
+  sudo unsquashfs -d "${rootfs}" "${rootfs_img}"
+  sudo unsquashfs -d "${stateful}" "${stateful_img}"
+
+  # Remove source images.
+  sudo rm -f "${rootfs_img}" "${stateful_img}"
+
+  # Fix up rootfs.
+
+  # Remove efi cruft.
+  sudo rm -rf "${rootfs}/boot"/{efi,syslinux}
+  # Don't need firmware if you don't have hardware!
+  sudo rm -rf "${rootfs}/lib/firmware"
+  # Get rid of stateful, it's not needed on termina.
+  sudo rm -rf "${rootfs}/mnt/stateful_partition"
+  # Create container rootfs and private dirs.
+  sudo mkdir "${rootfs}"/mnt/{container_rootfs,container_private}
+  # Copy the dev_image into its location at /usr/local.
+  sudo cp -aT "${stateful}"/dev_image "${rootfs}/usr/local"
+
+  # TODO(smbarber): Don't put kernel onto host rootfs once we can
+  # boot 64-bit kernels directly.
+  sudo cp "${rootfs}/boot/vmlinuz" "${output_dir}/vm_kernel"
+  # Remove vmlinuz from the rootfs since it's already on the host rootfs.
+  sudo rm -rf "${rootfs}"/boot/vmlinuz*
+
+  sudo cp "${rootfs}/etc/lsb-release" "${output_dir}/lsb-release"
+
+  case "${fs_type}" in
+  squashfs)
+    sudo mksquashfs "${rootfs}" "${rootfs_img}" -comp lzo
+    ;;
+  ext4)
+    # Start with 300MB, then shrink.
+    local image_size=300
+    truncate --size "${image_size}M" "${rootfs_img}"
+    /sbin/mkfs.ext4 -F -m 0 -i 16384 -b 1024 -O "^has_journal" "${rootfs_img}"
+    local rootfs_mnt="$(mktemp -d)"
+    fs_mount "${rootfs_img}" "${rootfs_mnt}" ext4 rw
+    sudo cp -aT "${rootfs}" "${rootfs_mnt}"
+    fs_umount "${rootfs_img}" "${rootfs_mnt}" ext4 rw
+    # Shrink to minimum size.
+    /sbin/e2fsck -f "${rootfs_img}"
+    /sbin/resize2fs -M "${rootfs_img}"
+    rmdir "${rootfs_mnt}"
+    ;;
+  *)
+    die_notrace "Unsupported fs type ${fs_type}."
+    ;;
+  esac
+
+  sudo rm -rf "${rootfs}" "${stateful}"
+}
+
+upload() {
+  local output_dir="$1"
+  local gspath="gs://chromeos-localmirror/distfiles"
+
+  local release="$(get_version "${output_dir}")"
+  release="${release/-/_}"
+  if is_test_image "${output_dir}"; then
+    local pkg_name="termina-vm-image-test_${FLAGS_arch}"
+  else
+    local pkg_name="termina-vm-image_${FLAGS_arch}"
+  fi
+
+  local target_file="${pkg_name}-${release}.tar.xz"
+
+  info "Will upload: '${target_file}'"
+
+  if ! prompt_yesno "Are you sure you want to upload this image?"; then
+    echo "Not uploading image."
+    return
+  fi
+
+  info "Generating tarball..."
+  tar caf "${output_dir}/termina-vm-image.tar.xz" -C "${output_dir}" \
+    vm_kernel vm_rootfs.img
+
+  if ! gsutil cp -a public-read "${output_dir}/termina-vm-image.tar.xz" \
+       "${gspath}/${target_file}"; then
+    die_notrace "Couldn't upload '${gspath}/${target_file}'."
+  fi
+  info "Uploaded to ${gspath}/${target_file}"
 }
 
 main() {
-  OVERLAY_CHROMEOS_DIR="${SRC_ROOT}/third_party/chromiumos-overlay/chromeos"
-  . "${OVERLAY_CHROMEOS_DIR}/config/chromeos_version.sh" || exit 1
-  . "${BUILD_LIBRARY_DIR}/board_options.sh" || exit 1
-  . "${BUILD_LIBRARY_DIR}/base_image_util.sh" || exit 1
-  . "${BUILD_LIBRARY_DIR}/build_image_util.sh" || exit 1
-
-  local image_name="${FLAGS_board}"
-  info "Building '${image_name}' ..."
-  build_vm_image "${image_name}" "${IMAGE_TYPE_BASE}"
-
-  if [[ ${FLAGS_dev} -eq ${FLAGS_true} ]]; then
-    info "Building '${image_name}-dev' ..."
-    build_vm_image "${image_name}-dev" "${IMAGE_TYPE_DEV}"
+  if [[ -z "${FLAGS_image}" ]]; then
+    die_notrace "Please provide an image using --image"
+  elif [[ ! -f "${FLAGS_image}" ]]; then
+    die_notrace "'${FLAGS_image}' does not exist"
   fi
 
-  if [[ ${FLAGS_test} -eq ${FLAGS_true} ]]; then
-    info "Building '${image_name}-test' ..."
-    build_vm_image "${image_name}-test" "${IMAGE_TYPE_TEST}"
+  if [[ "${FLAGS_arch}" != "amd64" && "${FLAGS_arch}" != "armv8" ]]; then
+    die_notrace "Architecture '${FLAGS_arch}' is not valid. Options are 'amd64' and 'armv8'"
   fi
 
-  # Set up symlink to latest image directory.
-  LINK_NAME="${FLAGS_output_root}/${BOARD}/latest"
-  ln -sfT $(basename ${OUTPUT_DIR}) ${LINK_NAME}
+  case "${FLAGS_filesystem}" in
+  squashfs|ext4)
+    ;;
+  *)
+    die_notrace "Filesystem '${FLAGS_filesystem}' is not valid. Options are 'squashfs' and 'ext4'"
+    ;;
+  esac
+
+  if [[ -z "${FLAGS_output}" ]]; then
+    die_notrace "Output directory was not specified"
+  elif [[ -e "${FLAGS_output}" ]]; then
+    die_notrace "${FLAGS_output} already exists"
+  fi
+
+  local output_dir="${FLAGS_output}"
+  local stateful_img="${output_dir}/vm_stateful.img"
+  local rootfs_img="${output_dir}/vm_rootfs.img"
+  local image="${FLAGS_image}"
+
+  mkdir -p "${output_dir}"
+  extract_squashfs_partition "${image}" "3" "${rootfs_img}"
+  extract_squashfs_partition "${image}" "1" "${stateful_img}"
+
+  repack_rootfs "${output_dir}" "${FLAGS_filesystem}"
+
+  if [[ "${FLAGS_upload}" -eq "${FLAGS_TRUE}" ]]; then
+    upload "${output_dir}"
+  fi
+
+  info "Done! The resulting image is in '${output_dir}'"
 }
 
 main "$@"