contrib: Add a script to update program firmware

Updates firmware from src/program and src/project repos.
BUG=none
TEST=Run script for puff firmware updates.

Change-Id: Ibd0b6321381961fcd8ae651474e5d65efacb3130
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2249203
Reviewed-by: Sam McNally <sammc@chromium.org>
Commit-Queue: Andrew McRae <amcrae@chromium.org>
Tested-by: Andrew McRae <amcrae@chromium.org>
diff --git a/contrib/update_program_fw b/contrib/update_program_fw
new file mode 100755
index 0000000..dfb9778
--- /dev/null
+++ b/contrib/update_program_fw
@@ -0,0 +1,317 @@
+#!/bin/bash
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# TODO(amcrae): Refactor to use common.sh
+#
+# Script to update base firmware in a program's config, and then
+# regenerate the configs of projects that are part of the program.
+# Also updates the firmware manifest, and uploads all of the changes
+# for review.
+#
+# Usage:
+#   ./update_program_fw program release [ reviewer ]
+# E.g:
+#   ./update_program_fw puff 13291 amcrae@google.com
+#
+# Environment variables:
+# SRC = base of repo tree (default is ~/trunk/src)
+# HOME = home directory
+#
+# Variables
+#
+TEMPDIR=$(mktemp -d -t fw-XXXXXXXXXX)
+CMDNAME=$(basename "$0")
+SRC="${SRC:-/mnt/host/source/src}"
+PATH="${PATH}:${SRC}/config/bin"
+DIGITS="[1-9][0-9][0-9][0-9][0-9]"
+PROG=""
+BRANCH=""
+REVIEWER=""
+PROGRAM_CL=""
+PROGRAM="program.star"
+EDITOR="ex -s"; export EDITOR
+#
+# Common functions
+#
+cleanup() {
+  rm -rf "${TEMPDIR}"
+}
+
+trap "exit 1"           HUP INT PIPE QUIT TERM
+trap 'cleanup' EXIT
+
+usage() {
+  if [[ "$#" -ge 1 ]]; then
+    echo "$*"
+  fi
+  echo "Usage: ${CMDNAME} program-name release [ reviewer ]"
+  echo "e.g:"
+  echo "    ${CMDNAME} puff 13271 amcrae@google.com"
+  exit 1
+}
+#
+# Abort the update, and clean up branches and CLs
+#
+abort() {
+  echo "$*"
+  echo "Aborting..."
+  CLS=$(gerrit -i --raw search "owner:me status:open hashtag:${BRANCH}")
+  if [[ -n "${CLS}" ]]; then
+    echo "Abandoning uploaded CLs"
+    gerrit -i abandon "${CLS}"
+  fi
+  "cros_workon-${PROG}" stop "chromeos-base/chromeos-firmware-${PROG}"
+  "cros_workon-${PROG}" stop "chromeos-base/chromeos-config-bsp-${PROG}-private"
+  repo abandon "${BRANCH}"
+  exit 1
+}
+#
+# Extract a CL number from the file containing
+# the output of repo upload
+#
+getcl() {
+  CL=$(grep -o "https://chrome-internal-review.googlesource.com/c/chromeos/$1/+/[0-9][0-9]*" "$2")
+  if [[ -z "${CL}" ]]; then
+    cat "$2"
+    abort CL number not found in repo upload output
+  fi
+  echo "${CL}" | grep -o "[0-9][0-9]*"
+}
+#
+# If not on this branch, start a branch
+#
+branch() {
+  if ! (git branch --show-current | grep -q "${BRANCH}"); then
+     repo start "${BRANCH}"
+  else
+     echo "${BRANCH} already exists, skipping repo start"
+  fi
+}
+#
+# Return true if repo has changes.
+changed() {
+  [[ -n $(git status -s) ]]
+}
+#
+# Add a Cq-Depend line to a commit.
+# Use ex as a line editor to insert it.
+#
+amend() {
+  git commit --amend <<EOF
+/^Change-Id/
+i
+$1
+.
+wq
+EOF
+}
+#
+# Validate arguments
+#
+if [[ "$#" -lt 2 ]] || [[ "$#" -gt 3 ]]; then
+  usage
+fi
+#
+# Program must exist as a directory
+#
+PROG="$1"
+PROGDIR="${SRC}/program/${PROG}"
+if [[ ! -d "${PROGDIR}" ]]; then
+  usage "${PROG} is not a valid program (${PROGDIR} missing)"
+fi
+# Release must be a 5 digit number
+RELEASE="$2"
+if [[ ! "${RELEASE}" =~ ^${DIGITS}$ ]]; then
+      usage "release must be a 5 digit number"
+fi
+#
+# Check for reviewer argument.
+#
+if [[ "$#" -eq 3 ]]; then
+  REVIEWER=$3
+fi
+BRANCH="update_${PROG}_fw_${RELEASE}"
+#
+# Update the firmware version in the program config
+# From now on, all errors should invoke 'abort'
+# so that the branches and CLs are cleaned up.
+#
+cd "${PROGDIR}" || exit 1
+branch
+sed "/^  *major_version = ${DIGITS}$/s/${DIGITS}/${RELEASE}/" "${PROGRAM}" > "${TEMPDIR}/new-${PROGRAM}"
+#
+# Verify that only 1-5 characters have changed.
+#
+DIFF=$(cmp -l "${PROGRAM}" "${TEMPDIR}/new-${PROGRAM}" | wc -l)
+if [[ "${DIFF}" -gt 5 ]]; then
+  diff "${PROGRAM}" "new-${TEMPDIR}/${PROGRAM}"
+  abort "${PROGDIR}/${PROGRAM} update error"
+  exit 1
+fi
+#
+# If program config has changed, create a CL.
+#
+if [[ "${DIFF}" -ne 0 ]]; then
+  cp "${TEMPDIR}/new-${PROGRAM}" "${PROGRAM}"
+  git add .
+  git commit -F - <<EOF
+${PROG}: Update firmware to ${RELEASE}
+
+BUG=none
+TEST=FAFT tests on ${PROG}
+EOF
+  repo upload -y "--ht=${BRANCH}" --cbr . > "${TEMPDIR}/upload.output" 2>&1
+  PROGRAM_CL=$(getcl "program/${PROG}" "${TEMPDIR}/upload.output")
+fi
+#
+# Now walk through the projects and regenerate the configs.
+# Create and upload a CL and capture the CL number and project directory
+# if the project has changed.
+#
+PROJ_CLS=()
+PROJ_DIRS=()
+for PDIR in "${SRC}/project/${PROG}"/*; do
+  PROJ=$(basename "${PDIR}")
+  cd "${PDIR}" || abort "Missing directory: ${PDIR}"
+  branch
+  ./config.star || abort "Generate config failed for ${PROJ}"
+  check_config > "${TEMPDIR}/check_config-${PROJ}.output" || abort "check_config failed for ${PROJ}"
+  #
+  # Check if any files changed.
+  #
+  if changed; then
+    git add .
+    git commit -F - <<EOF
+${PROJ}: Update firmware to ${RELEASE}
+BUG=none
+TEST=FAFT tests on ${PROG}
+EOF
+    repo upload -y "--ht=${BRANCH}" --cbr . > "${TEMPDIR}/upload.${PROJ}.output" 2>&1
+    P_CL=$(getcl "project/${PROG}/${PROJ}" "${TEMPDIR}/upload.${PROJ}.output")
+    PROJ_CLS+=("${P_CL}")
+    PROJ_DIRS+=("${PDIR}")
+  fi
+done
+#
+# Create a Cq-Depend line with all the project CLs
+#
+if [[ -n "${PROJ_CLS[*]}" ]];then
+  SEP=" "
+  PROG_CQD="Cq-Depend:"
+  for CL in "${PROJ_CLS[@]}"; do
+    PROG_CQD="${PROG_CQD}${SEP}chrome-internal:${CL}"
+    SEP=", "
+  done
+  #
+  # Add the Cq-Depend line to the program CL commit message.
+  #
+  cd "${PROGDIR}" || abort "Missing directory: ${PROGDIR}"
+  amend "${PROG_CQD}"
+  repo upload --cbr .
+fi
+#
+# All the boxster configs have been uploaded.
+# Now run the update script and update the firmware manifest.
+#
+# Build base coreboot files
+# TODO: Should be selective here.
+#
+echo "Running emerge for coreboot. This may take a while..."
+if ! ("emerge-${PROG}" --quiet-build chromeos-ec coreboot depthcharge vboot_reference \
+                 libpayload chromeos-bootimage coreboot-private-files \
+                 "coreboot-private-files-${PROG}"); then
+  abort "emerge for coreboot failed!"
+fi
+echo "emerge of coreboot successful"
+OVERLAY="${SRC}/private-overlays/overlay-${PROG}-private/chromeos-base/chromeos-firmware-${PROG}"
+EB9999="chromeos-firmware-${PROG}-9999.ebuild"
+#
+# Remove any previous attempts to build the firmware.
+#
+"cros_workon-${PROG}" stop "chromeos-base/chromeos-firmware-${PROG}"
+"cros_workon-${PROG}" stop "chromeos-base/chromeos-config-bsp-${PROG}-private"
+cd "${OVERLAY}" || abort "Missing directory: ${OVERLAY}"
+branch
+cd "${SRC}/platform/dev/contrib" || abort "Missing directory: ${SRC}/platform/dev/contrib"
+if ! (./cros_update_firmware -q "--board=${PROG}"); then
+  abort "cros_update_firmware failed for ${PROG}"
+  exit 1
+fi
+cd "${OVERLAY}" || abort "Missing directory: ${OVERLAY}"
+#
+# If files have been updated, then create a CL for the changes.
+#
+OVERLAY_CL=""
+if changed; then
+  #
+  # Bump the version in the ebuild file. Relies on the format
+  # of the version so that the last number is at the end of the line.
+  #
+  CURVERS=$(grep "VERSION=REVBUMP" "${EB9999}" | grep -o "[0-9][0-9]*$")
+  NEXTVERS=$((CURVERS + 1))
+  sed "/VERSION=REVBUMP/s/${CURVERS}$/${NEXTVERS}/" "${EB9999}" > "${TEMPDIR}/new-${EB9999}"
+  cp "${TEMPDIR}/new-${EB9999}" "${EB9999}"
+  git add .
+  git commit -F - <<EOF
+${PROG}: Update firmware to ${RELEASE}
+
+BUG=none
+TEST=FAFT tests on ${PROG}
+
+${CQD}
+EOF
+  #
+  # Upload with no-verify since the file lines are too long.
+  #
+  repo upload "--ht=${BRANCH}" -y --no-verify --cbr . > "${TEMPDIR}/overlay.output" 2>&1
+  OVERLAY_CL=$(getcl "overlays/overlay-${PROG}-private" "${TEMPDIR}/overlay.output")
+  #
+  # Go back and amend all the project commit messages with a Cq-Depend on
+  # the program and overlay CLs.
+  #
+  CQD="Cq-Depend: chrome-internal:${OVERLAY_CL}"
+  if [[ -n "${PROGRAM_CL}" ]]; then
+    CQD="${CQD}, chrome-internal:${PROGRAM_CL}"
+  fi
+  for DIR in "${PROJ_DIRS[@]}"; do
+    cd "${DIR}" || abort "Missing directory: ${DIR}"
+    amend "${CQD}"
+    repo upload --cbr .
+  done
+fi
+#
+# Send all of the CLs to the CQ for a dry run.
+#
+ALL_CLS=$(gerrit -i --raw search "owner:me status:open hashtag:${BRANCH}")
+if [[ -z "${ALL_CLS}" ]]; then
+  echo "No changes required for program ${PROG}"
+  repo abandon "${BRANCH}"
+  exit 0
+fi
+for cl in ${ALL_CLS}; do
+  gerrit -i label-cq "${cl}" 1
+  gerrit -i label-v "${cl}" 1
+  gerrit -i label-as "${cl}" 1
+done
+#
+# If reviewer is set, then add them to the CLs
+#
+if [[ -n "${REVIEWER}" ]]; then
+  echo "Sending CLs ${ALL_CLS} to ${REVIEWER} for review"
+  for cl in ${ALL_CLS}; do
+    gerrit -i reviewers "${cl}" "${REVIEWER}"
+  done
+else
+  echo "Send CLs for review by running:"
+  echo "   for cl in ${ALL_CLS}; do gerrit -i reviewers \$cl <reviewer>; done"
+fi
+#
+# Final instructions.
+#
+echo "Run:"
+echo "  /build/${PROG}/usr/sbin/chromeos-firmwareupdate --manifest"
+echo "to verify firmware update"
+echo "When submitted, cleanup by running:"
+echo "repo abandon ${BRANCH}"