Add upstream-workon.bash script

upstream-workon is a script which aids toolchain developers by
keeping the local, fully patched source in some non-ephemeral
local directory inside the chroot.

At the moment, it's still in a prototype phase (the end goal
would be to make it quite a bit safer, and also move it out of
bash).

This is the first commit for the script, meant to act as a
base for future work.

BUG=b:186803200
TEST=mkdir ~/llvm; upstream_workon.bash init sys-devel/llvm ~/llvm
TEST=upstream_workon.bash build sys-devel/llvm
TEST=upstream_workon.bash install sys-devel/llvm
TEST=upstream_workon.bash link sys-devel/llvm ~/llvm_src_tree

Change-Id: Ifa1b1520d621cbe8b435d4ebb1ae71dc28548fcf
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/3068075
Commit-Queue: Jordan R Abrahams <ajordanr@google.com>
Tested-by: Jordan R Abrahams <ajordanr@google.com>
Reviewed-by: George Burgess <gbiv@chromium.org>
diff --git a/upstream_workon/upstream_workon.bash b/upstream_workon/upstream_workon.bash
new file mode 100755
index 0000000..f066bbe
--- /dev/null
+++ b/upstream_workon/upstream_workon.bash
@@ -0,0 +1,273 @@
+#!/bin/bash -eu
+#
+# Copyright 2021 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.
+
+USAGE=\
+'Usage: upstream-workon [-h]
+                       init <PACKAGE> <DEV_WORK_DIR>
+                       link <PACKAGE> <DEV_WORK_DIR>
+                       build <PACKAGE>
+                       install <PACKAGE>
+                       clean <PACKAGE>
+                       help'
+
+set +e
+read -r -d '' HELP <<'EOF'
+Usage: upstream-workon [-h]
+                       init <PACKAGE> <DEV_WORK_DIR>
+                       link <PACKAGE> <DEV_WORK_DIR>
+                       build <PACKAGE>
+                       install <PACKAGE>
+                       clean <PACKAGE>
+                       help
+
+Flags:
+    -h  --help      Print this help message
+
+Commands:
+    init            Initialize in a developer workdir using a new tree
+    link            Link an existing developer source dir to portage workdir
+    build           Build the package using ebuild ... compile
+    install         Install the package using ebuild ... install
+    clean           Clean up your work without deleting the developer workdir
+    help            Print this help message
+
+Examples:
+
+    # Start work
+    mkdir "$HOME/llvm"
+    upstream-workon init sys-devel/llvm "$HOME/llvm"
+
+    # Link your existing work
+    upstream-workon link sys-devel/llvm "$HOME/llvm"
+
+    # Compile your work
+    upstream-workon build sys-devel/llvm
+
+    # Install your changes to the chroot
+    upstream-workon install sys-devel/llvm
+
+    # Clean up
+    upstream-workon clean sys-devel/llvm
+
+EOF
+set -e
+
+incorrect_number_of_arguments() {
+    echo 'ERROR: Please use correct command syntax' >&2
+    echo "${USAGE}" >&2
+    exit 1
+}
+
+print_experimental_warning() {
+    echo >&2
+    echo '!!! WARNING: This tool is EXPERIMENTAL--please do not rely on the API.' >&2
+    echo '!!! WARNING: Please recommend new features for Version 2, but this' >&2
+    echo '!!! WARNING: implementation will not be actively developed and' >&2
+    echo '!!! WARNING: exists only to receive feedback and minor fixes.' >&2
+}
+
+# ------------------------------------------------------------------------------
+# Actual logic
+# ------------------------------------------------------------------------------
+
+# We probably can just pass through "USE", but I think this gives a bit more
+# flexibility in the future.
+USE_FLAGS="${USE:-}"
+
+if [[ -n "${USE_FLAGS}" ]]; then
+    echo 'USE flags set to:'
+    echo "    ${USE_FLAGS}"
+fi
+
+init() {
+    local package="$1"
+    local desired_src_loc="$2"
+    local ebuild_loc
+    ebuild_loc="$(resolve_ebuild_for "${package}")"
+
+    local ebuild_name
+    ebuild_name="$(basename "${ebuild_loc}" | sed 's/\.ebuild$//g')"
+    local package_name
+    # SC2001 complains about not using variable replace syntax.
+    # However, variable remove syntax is not sufficiently expansive
+    # to do this replacement easily.
+    # shellcheck disable=2001
+    package_name="$(sed 's/-r[0-9]\+$//g' <<< "${ebuild_name}")"
+    local ebuild_category
+    ebuild_category="$(basename "$(dirname "$(dirname "${ebuild_loc}")")")"
+    local portage_dir='/var/tmp/portage'
+
+    local work_dir="${portage_dir}/${ebuild_category}/${ebuild_name}/work/${package_name}"
+
+    ebuild "${ebuild_loc}" clean
+    USE="${USE_FLAGS}" ebuild "${ebuild_loc}" unpack
+
+    # May need to init git if it doesn't already exist.
+    # Probably could just use git -C instead of the pushd/popd.
+    pushd "${work_dir}" >& /dev/null
+    if [[ ! -d '.git' ]]; then
+        git init
+        git add .
+        git commit -m 'Initial commit'
+    fi
+    popd >& /dev/null
+
+    USE="${USE_FLAGS}" ebuild "${ebuild_loc}" configure
+
+    cp -r -p "${work_dir}/." "${desired_src_loc}"
+    local backup_dir="${work_dir}.bk"
+    mv "${work_dir}" "${backup_dir}"
+    ln -s "$(realpath "${desired_src_loc}")" "${work_dir}"
+
+    pushd "${desired_src_loc}" >& /dev/null
+
+    git add .
+    git commit -m 'Ebuild configure commit'
+    popd >& /dev/null
+
+    echo
+    echo '----------------------------------------'
+    echo 'Successfully created local mirror!'
+    echo "Developer work directory set up at: ${desired_src_loc}"
+    echo 'To build the package, run:'
+    echo "    upstream-workon build ${package}"
+    echo 'To install the package, run:'
+    echo "    sudo upstream-workon install ${package}"
+    echo "To clean up (without deleting ${desired_src_loc}), run:"
+    echo "    upstream-workon clean ${package}"
+    echo "WARNING: Moving original workdir to ${backup_dir}, consider deleting" >&2
+}
+
+clean() {
+    local package="$1"
+    echo 'WARNING: You may need to run this with sudo' >&2
+    local ebuild_loc
+    ebuild_loc="$(resolve_ebuild_for "${package}")"
+
+    ebuild "${ebuild_loc}" clean
+
+    echo '----------------------------------------'
+    echo "Successfully cleaned up ${package}!"
+}
+
+
+compile() {
+    local package="$1"
+    local ebuild_loc
+    ebuild_loc="$(resolve_ebuild_for "${package}")"
+
+    USE="${USE_FLAGS}" ebuild "${ebuild_loc}" compile
+
+    echo '----------------------------------------'
+    echo "Successfully compiled ${package}!"
+}
+
+
+install_src() {
+    local package="$1"
+    echo 'WARNING: You may need to run this with sudo' >&2
+    local ebuild_loc
+    ebuild_loc="$(resolve_ebuild_for "${package}")"
+
+    USE="${USE_FLAGS}" ebuild "${ebuild_loc}" install
+
+    echo '----------------------------------------'
+    echo "Successfully installed ${package}!"
+}
+
+link_src() {
+    local package="$1"
+    local desired_src_loc="$2"
+    local ebuild_loc
+    ebuild_loc="$(resolve_ebuild_for "${package}")"
+
+    local ebuild_name
+    ebuild_name="$(basename "${ebuild_loc}" | sed 's/\.ebuild$//g')"
+    local package_name
+    # shellcheck disable=2001
+    package_name="$(sed 's/-r[0-9]\+$//g' <<< "${ebuild_name}")"
+    local ebuild_category
+    ebuild_category="$(basename "$(dirname "$(dirname "${ebuild_loc}")")")"
+    local portage_dir='/var/tmp/portage'
+
+    local work_dir="${portage_dir}/${ebuild_category}/${ebuild_name}/work/${package_name}"
+
+    local backup_dir="${work_dir}.bk"
+
+    # Because of some annoying permissions issues, we have to configure directly in
+    # /var/tmp/portage/...
+    # We then copy over those changes into our local source directory.
+    # To make sure the proper deletions get done, we delete everything except
+    # your local git directory.
+
+    ebuild "${ebuild_loc}" clean
+    USE="${USE_FLAGS}" ebuild "${ebuild_loc}" configure
+    # TODO(ajordanr): This is a rough edge, and I don't want users to delete their
+    # home directory without knowing what they are doing. So we're copying
+    # everything instead.
+    # TODO(ajordanr): This will ignore git submodules, which I don't want.
+    mv "${desired_src_loc}" "${desired_src_loc}.bk"
+    mkdir "${desired_src_loc}"
+    cp -rP "${desired_src_loc}.bk/.git" "${desired_src_loc}/.git"
+    rsync -a --exclude=".git" "${work_dir}"/* "${desired_src_loc}"
+    rsync -a --exclude=".git" "${work_dir}"/.[^.]* "${desired_src_loc}"
+    mv "${work_dir}" "${backup_dir}"
+    ln -s "$(realpath "${desired_src_loc}")" "${work_dir}"
+
+    echo '----------------------------------------'
+    echo 'Successfully linked to local mirror!'
+    echo "Developer work directory linked to: ${desired_src_loc}"
+    echo "WARNING: Moving original workdir to ${backup_dir}, consider deleting" >&2
+    echo "WARNING: Moving original dev dir to ${desired_src_loc}.bk, consider deleting" >&2
+}
+
+resolve_ebuild_for() {
+    equery w "$1"
+}
+
+CMD="${1:-}"
+
+case "${CMD}" in
+    -h|--help|help)
+        shift
+        echo "${HELP}"
+        print_experimental_warning
+        exit 1
+        ;;
+    init)
+        shift
+        [[ -z "${1:-}" || -z "${2:-}" ]] && incorrect_number_of_arguments
+        print_experimental_warning
+        init "$1" "$2"
+        ;;
+    link)
+        shift
+        [[ -z "${1:-}" || -z "${2:-}" ]] && incorrect_number_of_arguments
+        print_experimental_warning
+        link_src "$1" "$2"
+        ;;
+    build)
+        shift
+        [[ -z "${1:-}" ]] && incorrect_number_of_arguments
+        print_experimental_warning
+        compile "$1"
+        ;;
+    clean)
+        shift
+        [[ -z "${1:-}" ]] && incorrect_number_of_arguments
+        print_experimental_warning
+        clean "$1"
+        ;;
+    install)
+        shift
+        [[ -z "${1:-}" ]] && incorrect_number_of_arguments
+        print_experimental_warning
+        install_src "$1"
+        ;;
+    *)
+        incorrect_number_of_arguments
+        ;;
+esac