#!/bin/bash

# Copyright (c) 2011 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.

# This script moves ebuilds between 'stable' and 'live' states.
# By default 'stable' ebuilds point at and build from source at the
# last known good commit. Moving an ebuild to 'live' (via cros_workon start)
# is intended to support development. The current source tip is fetched,
# source modified and built using the unstable 'live' (9999) ebuild.

. "$(dirname "$0")/common.sh" || exit 1

assert_not_root_user

# Script must be run inside the chroot

DEFINE_string board "${DEFAULT_BOARD}" \
  "The board to set package keywords for."
DEFINE_boolean host "${FLAGS_FALSE}" \
  "Uses the host instead of board"
DEFINE_string remote "" \
  "For non-workon projects, the git remote to use."
DEFINE_string revision "" \
  "Use to override the manifest defined default revision used for a project"
DEFINE_string command "git status" \
  "The command to be run by forall."
DEFINE_boolean all "${FLAGS_FALSE}" \
  "Apply to all possible packages for the given command"

FLAGS_HELP="usage: $0 <command> [flags] [<list of packages>|.|--all]
commands:
  start:     Moves an ebuild to live (intended to support development)
  stop:      Moves an ebuild to stable (use last known good)
  info:      Print package name, repo name, and source directory.
  list:      List of live ebuilds (workon ebuilds if --all)
  list-all:  List all of the live ebuilds for all setup boards
  iterate:   For each ebuild, cd to the source dir and run a command"
FLAGS "$@" || { [ "${FLAGS_help}" = "${FLAGS_TRUE}" ]  && exit 0; } || exit 1
eval set -- "${FLAGS_ARGV}"


# eat the workon command keywords: start, stop or list.
WORKON_CMD=$1
shift

# Board dir config

# If both are specified, just use host, because board does not
# have to be specified and may come from default, in which case
# there's no way to override.
[ -n "${FLAGS_board}" ] && [ "${FLAGS_host}" = ${FLAGS_TRUE} ] && \
  FLAGS_board="" # kill board
if [ -z "${FLAGS_board}" ] && \
   [ "${FLAGS_host}" = ${FLAGS_FALSE} ] && \
   [ "${WORKON_CMD}" != "list-all" ]; then
  flags_help
  die "You must specify either --host or --board="
fi

if [ -n "${FLAGS_board}" ]; then
  BOARD_DIR=/build/"${FLAGS_board}" # --board specified
  EQUERYCMD=equery-"${FLAGS_board}"
  EBUILDCMD=ebuild-"${FLAGS_board}"
  PORTAGEQCMD=portageq-"${FLAGS_board}"
  BOARD_STR="${FLAGS_board}"
else
  BOARD_DIR="" # --host specified
  EQUERYCMD=equery
  EBUILDCMD=ebuild
  PORTAGEQCMD=portageq
  BOARD_STR="host"
fi

WORKON_DIR=${CHROOT_TRUNK_DIR}/.config/cros_workon
CONFIG_DIR=${BOARD_DIR}/etc/portage
KEYWORDS_DIR=${CONFIG_DIR}/package.keywords
MASK_DIR=${CONFIG_DIR}/package.mask
UNMASK_DIR=${CONFIG_DIR}/package.unmask
WORKON_FILE=${WORKON_DIR}/${FLAGS_board:-host}
MASK_WORKON_FILE=${WORKON_FILE}.mask
KEYWORDS_FILE=${KEYWORDS_DIR}/cros-workon
MASK_FILE=${MASK_DIR}/cros-workon
UNMASK_FILE=${UNMASK_DIR}/cros-workon
CHROME_ATOM=chromeos-base/chromeos-chrome

mkdir -p "${WORKON_DIR}" || die "mkdir -p ${WORKON_DIR}"
touch "${WORKON_FILE}" "${MASK_WORKON_FILE}" || \
  die "touch ${WORKON_FILE} ${MASK_WORKON_FILE}"
cmds=(
  "mkdir -p '${KEYWORDS_DIR}' '${MASK_DIR}' '${UNMASK_DIR}'"

  # Clobber and re-create the WORKON_FILE symlinks every time. This
  # is a trivial operation and eliminates all kinds of corner cases
  # as well as any possible future renames of WORKON_FILE.
  # In particular, chroot is usually built as "amd64-host" but becomes
  # just "host" after installation. crosbug.com/23096
  "ln -sf '${WORKON_FILE}' '${KEYWORDS_FILE}'"
  "ln -sf '${MASK_WORKON_FILE}' '${MASK_FILE}'"
  "ln -sf '${WORKON_FILE}' '${UNMASK_FILE}'"
)
# If the board dir doesn't exist yet, we don't want to create it as
# that'll screw up ./setup_board later on.
if [[ -d ${BOARD_DIR:-/} ]] ; then
  sudo_multi "${cmds[@]}"
else
  die "${BOARD_STR} has not been setup yet"
fi


find_keyword_workon_ebuilds() {
  local keyword="${1}"
  local overlay

  # NOTE: overlay may be a symlink, and we have to use ${overlay}/
  for overlay in ${PORTDIR_OVERLAY}; do
    # only look up ebuilds named 9999 to eliminate duplicates
    find "${overlay}"/*-* -maxdepth 2 -type f -name '*9999.ebuild' \
      -exec grep -l 'inherit.*cros-workon' {} + 2>/dev/null | \
      xargs egrep -l "KEYWORDS=\".*~([*]|${keyword})[ \"]"
  done
}

ebuild_to_package() {
  # This changes the absolute path to ebuilds into category/package.
  sed -e 's/.*\/\([^/]*\)\/\([^/]*\)\/.*\.ebuild/\1\/\2/'
}

show_project_ebuild_map() {
  local keyword="$1"

  # Column 1: Repo name
  # Column 2: Package name
  for EBUILD in $(find_keyword_workon_ebuilds ${keyword}); do
    (
      eval $(grep -E '^CROS_WORKON' "${EBUILD}")
      CP=$(echo "$EBUILD" | ebuild_to_package)
      echo "${CROS_WORKON_PROJECT}" "${CP}"
    )
  done
}

show_project_path_map() {
  # Column 1: Repo name
  # Column 2: Source directory
  repo list | awk -F ' : ' '{ print $2, $1 }'
}

show_workon_ebuilds() {
  local keyword="$1"
  find_keyword_workon_ebuilds "${keyword}" | ebuild_to_package | sort -u
}

show_workon_info() {
   local atoms="$1"
   local keyword="$2"
   local sort="sort -k1b,1"
   # Column 1: Package name
   # Column 2: Repo name
   # Column 3: Source directory (if present locally)
   join \
     <(echo "${atoms}" | sed -e 's/ /\n/g' | ${sort}) \
     <(join -a 1 -e - -o 1.2,1.1,2.2 \
       <(show_project_ebuild_map "${keyword}" | ${sort}) \
       <(show_project_path_map | ${sort}) \
       | ${sort})
}

# Canonicalize package name to category/package.
canonicalize_name() {
  local pkgfile
  local pkgname

  if grep -qx "=$1-9999" "${WORKON_FILE}" ; then
    echo $1
    return 0
  fi

  if ! pkgfile=$(ACCEPT_KEYWORDS="~${ARCH}" ${EQUERYCMD} \
                 which --include-masked $1); then
    # Try to auto-complete it.
    local autopkgs=()
    for pkgname in $(show_workon_ebuilds); do
      if [[ ${pkgname} == *"$1"* ]]; then
        autopkgs+=( ${pkgname} )
      fi
    done
    case ${#autopkgs[@]} in
    0)
      warn "error looking up package $1" 1>&2
      return 1
      ;;
    1)
      # Do nothing -- fall through.
      ;;
    *)
      warn "Multiple autocompletes found: ${autopkgs[*]}"
      ;;
    esac
    pkgname=${autopkgs[0]}
    info "Autocompleted '$1' to: ${pkgname}"
    canonicalize_name "${pkgname}"
    return
  fi

  pkgname=$(echo "${pkgfile}" | awk -F '/' '{ print $(NF-2) "/" $(NF-1) }')

  # TODO(rcui): remove special casing of chromeos-chrome here when we make it
  # inherit from cros-workon class.  Tracked in chromium-os:19259.
  if [ "${pkgname}" != "${CHROME_ATOM}" ] && \
     ! grep -q "cros-workon" ${pkgfile}; then
    warn "${pkgname} is not a cros-workon package" 1>&2
    return 1
  fi
  echo "${pkgname}"
  return 0
}

# Canonicalize a list of names.
canonicalize_names() {
  local atoms=$1
  local names=()
  local atom

  for atom in ${atoms}; do
    names+=( $(canonicalize_name "${atom}") ) || return 1
  done

  echo "${names[*]}"
}

# Locate the package name based on the current directory
locate_package() {
  local projectname=$(git config --get remote.cros.projectname ||
                      git config --get remote.cros-internal.projectname)
  if [ -z "${projectname}" ]; then
    die "No project in git config: Can not cros_workon . here"
  fi
  local reponame=$(show_project_ebuild_map |
                   grep "^${projectname} " | cut -d" " -f2)
  if [ -z "${reponame}" ]; then
    die "No matching package for ${projectname}: Can not cros_workon . here"
  else
    echo "${reponame}"
  fi
}

# Display ebuilds currently part of the live branch and open for development.
show_live_ebuilds() {
  sed -n 's/^=\(.*\)-9999$/\1/p' "${WORKON_FILE}"
}

# Display ebuilds currently part of the live branch and open for development
# for any board that currently has live ebuilds.
show_all_live_ebuilds() {
  local workon_file
  for workon_file in ${WORKON_DIR}/*; do
    if [[ -s ${workon_file} && ${workon_file} != *.mask ]] ; then
      echo -e "${V_BOLD_GREEN}$(basename ${workon_file}):${V_VIDOFF}"
      sed -n 's/^=\(.*\)-9999$/    \1/p' "${workon_file}"
      echo ""
    fi
  done
}

# This is called only for "cros-workon start". We dont handle the
# "stop" case since the local changes are ignored anyway since the
# 9999.ebuild is masked and we dont want to deal with what to do with
# the user's local changes.
regen_manifest_and_sync() {
  # Nothing to do unless you are working on the minilayout
  local manifest=${CHROOT_TRUNK_DIR}/.repo/manifest.xml
  if [ $(basename $(readlink -f ${manifest})) != "minilayout.xml" ]; then
    if [ -z "$(git config -f "${CHROOT_TRUNK_DIR}/.repo/manifests.git/config" \
               --get manifest.groups)" ]; then
      # Reaching here means that it's a full manifest w/out any groups set-
      # literal full manifest.
      return
    fi
  fi

  local need_repo_sync=
  local pkgname
  for pkgname in $(show_live_ebuilds); do
    local pkgpath="$(${EQUERYCMD} which "${pkgname}")"
    local pkginfo="$(${EBUILDCMD} "${pkgpath}" info |
                   grep -v 'pkg_info() is not defined')"
    if [ -z "${pkginfo}" ]; then
      continue  # No package information available
    fi

    eval "${pkginfo}"
    local trunkdir=$(readlink -m "${CHROOT_TRUNK_DIR}")

    local i=0
    need_repo_sync='yes'
    for S in "${CROS_WORKON_SRCDIR[@]}"; do
      local srcdir=$(readlink -m "${S}")
      local revision="${FLAGS_revision:+--revision=${FLAGS_revision}}"
      if [ -z "${FLAGS_remote}" ]; then
        loman add --workon "${CROS_WORKON_PROJECT[i]}" ${revision}
      else
        loman add --remote "${FLAGS_remote}" ${revision} \
          "${CROS_WORKON_PROJECT[i]}" "${srcdir#${trunkdir}/}"
      fi
      : $(( ++i ))
    done
  done
  if [ -n "${need_repo_sync}" ]; then
    echo "Please run \"repo sync\" now."
  fi
}

# Move a stable ebuild to the live development catgeory.  The ebuild
# src_unpack step fetches the package source for local development.
ebuild_to_live() {
  local atoms=$1
  local atoms_success=()
  local atom

  for atom in ${atoms}; do
    if ! grep -qx "=${atom}-9999" "${WORKON_FILE}" ; then
      echo "=${atom}-9999" >> "${WORKON_FILE}" || \
        die "Could not update ${WORKON_FILE} with ${atom}"
      echo "<${atom}-9999" >> "${MASK_WORKON_FILE}" || \
        die "Could not update ${MASK_WORKON_FILE} with ${atom}"
      atoms_success+=( ${atom} )
    else
      warn "Already working on ${atom}"
    fi
  done
  [ ${#atoms_success[@]} -gt 0 ] && regen_manifest_and_sync && \
    info "Started working on '${atoms_success[*]}' for '${BOARD_STR}'"
}

# Move a live development ebuild back to stable.
ebuild_to_stable() {
  local atoms=$1
  local atoms_success=()
  local atom

  for atom in ${atoms}; do
    if grep -qx "=${atom}-9999" "${WORKON_FILE}" "${MASK_WORKON_FILE}" ; then
      sed -i -e "/^=${atom/\//\\/}-9999\$/d" "${WORKON_FILE}" || \
        die "Could not update ${WORKON_FILE} with ${atom}"
      sed -i -e "/^<${atom/\//\\/}-9999\$/d" "${MASK_WORKON_FILE}" || \
        die "Could not update ${WORKON_FILE} with ${atom}"
      atoms_success+=( ${atom} )
    else
      warn "Not working on ${atom}"
    fi
  done
  [ ${#atoms_success[@]} -gt 0 ] && \
    info "Stopped working on '${atoms_success[*]}' for '${BOARD_STR}'"
}

# Run a command on all or a set of repos.
ebuild_iterate() {
  local atoms=$1
  local atom

  for atom in ${atoms}; do
    info "Running \"${FLAGS_command}\" on ${atom}"
    eval $(${EBUILDCMD} $(${EQUERYCMD} which ${atom}) info)
    for S in "${CROS_WORKON_SRCDIR[@]}"; do
      (cd "${S}" && bash -c "${FLAGS_command}")
    done
  done
}

# Only call portageq when absolutely required, and when we do, only run it
# once -- it's a slow beast and can easily take hundreds of milliseconds :(.
if [[ ${WORKON_CMD} != "list" || ${FLAGS_all} != ${FLAGS_FALSE} ]] ; then
  portageq_vars="ARCH PORTDIR_OVERLAY"
  unset ${portageq_vars}
  eval $(${PORTAGEQCMD} envvar -v ${portageq_vars})
fi

# --all makes commands operate on different lists
if [ ${FLAGS_all} = "${FLAGS_TRUE}" ]; then
  case ${WORKON_CMD} in
    start|info) ATOM_LIST=$(show_workon_ebuilds ${ARCH});;
    stop|iterate) ATOM_LIST=$(show_live_ebuilds);;
    list) ;;
    *) die "--all is invalid for the given command";;
  esac
else # not selected --all
  case ${WORKON_CMD} in
    start|stop|info|iterate)
      ATOM_LIST=$@
      if [ -z "${ATOM_LIST}" ]; then
        die "${WORKON_CMD}: No packages specified"
      fi
      if [ "${ATOM_LIST}" = "." ]; then
        ATOM_LIST=$(locate_package)
      fi
      if ! ATOM_LIST=$(canonicalize_names "${ATOM_LIST}"); then
        die "Error parsing package list"
      fi;;
    *) ;;
  esac
fi

case ${WORKON_CMD} in
  start) ebuild_to_live "${ATOM_LIST}" ;;
  stop) ebuild_to_stable "${ATOM_LIST}" ;;
  info) show_workon_info "${ATOM_LIST}" "${ARCH}" ;;
  list) [ ${FLAGS_all} = "${FLAGS_FALSE}" ] && show_live_ebuilds || \
    show_workon_ebuilds ${ARCH} ;;
  list-all) show_all_live_ebuilds ;;
  iterate) ebuild_iterate "${ATOM_LIST}" ;;
  *)
    flags_help
    die "$(basename $0): command '${WORKON_CMD}' not recognized"
    ;;
esac
