[meson] Make Meson generate artifacts in the style of #650.

This change refactors where Meson emits its Ninja files to make the
design proposed by imphil in #650 possible. In particular, there are now
two build directories:
- build-out, which has an unstable directory structure, is where Meson
  builds actually occur.
- build-bin, which has a stable directory structure, is where finished
  build outputs are copied by ci/make_build_bin.sh.

util/make_build_bin.sh is a simple script that copies finished build
artifacts out of build-out into build-bin, meant to be invoked during
CI.

util/build_consts.sh contains build directory structure definitions,
which other scripts can source to access build directories in a portable
manner.

A followup change will add scripts to invoke fusesoc to cause top-level
hardware artifacts to be build into build-out and build-bin.

Signed-off-by: Miguel Young de la Sota <mcyoung@google.com>
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index fbe09f4..35495d6 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -133,9 +133,12 @@
         --force
     displayName: 'Install toolchain'
   - bash: |
+      export BUILD_ROOT="$(Build.ArtifactStagingDirectory)"
+      . util/build_consts.sh
       ./meson_init.sh -f
-      ninja -C build-verilator all
-      ninja -C build-fpga all
+      ninja -C "$(sw_obj_dir sim-verilator)" all
+      ninja -C "$(sw_obj_dir fpga)" all
+      util/make_build_bin.sh
     displayName: 'Build embedded targets'
 
 - job: "deprecated_make_build"
diff --git a/ci/run_verilator_pytest.sh b/ci/run_verilator_pytest.sh
index 02d4bef..7c90abc 100755
--- a/ci/run_verilator_pytest.sh
+++ b/ci/run_verilator_pytest.sh
@@ -4,8 +4,10 @@
 # SPDX-License-Identifier: Apache-2.0
 set -e
 
+. util/build_consts.sh
+
 readonly VERILATED_SYSTEM_DEFAULT="build/lowrisc_systems_top_earlgrey_verilator_0.1/sim-verilator/Vtop_earlgrey_verilator"
-readonly SW_BUILD_DEFAULT="build-verilator"
+readonly SW_BUILD_DEFAULT="$(sw_obj_dir sim)"
 
 VERILATED_SYSTEM_PATH="${VERILATED_SYSTEM_PATH:-$VERILATED_SYSTEM_DEFAULT}"
 SW_BUILD_PATH="${SW_BUILD_PATH:-$SW_BUILD_DEFAULT}"
@@ -18,10 +20,10 @@
 )
 
 if [[ ! -z ${MAKE_BUILD+x} ]]; then
-  BOOT_ROM_TARGET="sim/boot_rom/rom.vmem"
-  TEST_TARGETS=("sim/tests/flash_ctrl/sw.vmem"
-    "sim/tests/hmac/sw.vmem"
-    "sim/tests/rv_timer/sw.vmem"
+  BOOT_ROM_TARGET="sw/device/sim/boot_rom/rom.vmem"
+  TEST_TARGETS=("sw/device/sim/tests/flash_ctrl/sw.vmem"
+    "sw/device/sim/tests/hmac/sw.vmem"
+    "sw/device/sim/tests/rv_timer/sw.vmem"
   )
 fi
 
@@ -32,8 +34,8 @@
   set +e
   set -x
   pytest -s test/systemtest/functional_verilator_test.py \
-    --test_bin "$SW_BUILD_PATH/sw/device/${target}" \
-    --rom_bin  "$SW_BUILD_PATH/sw/device/${BOOT_ROM_TARGET}" \
+    --test_bin "$SW_BUILD_PATH/${target}" \
+    --rom_bin  "$SW_BUILD_PATH/${BOOT_ROM_TARGET}" \
     --verilator_model "$VERILATED_SYSTEM_PATH"
   if [[ $? == 0 ]]; then
     PASS_TARGETS=("${PASS_TARGETS[@]}" "${target}")
diff --git a/meson.build b/meson.build
index 6607110..400b906 100644
--- a/meson.build
+++ b/meson.build
@@ -9,7 +9,7 @@
   error('target option not set. Please run meson with a valid build target option.')
 endif
 
-if target == 'verilator'
+if target == 'sim-verilator'
   # TODO: Consider using extra args array if using this flag globally is no
   # longer OK.
   add_project_arguments('-DSIMULATION', language: 'c')
@@ -48,7 +48,7 @@
 
 # RISCV linker parameters.
 riscv_linkfile = files(['sw/device/exts/common/link.ld'])
-riscv_link_args = ['-Wl,-T,@0@/@1@'.format('..', riscv_linkfile[0])]
+riscv_link_args = ['-Wl,-T,@0@/@1@'.format(meson.source_root(), riscv_linkfile[0])]
 riscv_link_deps = [riscv_linkfile]
 
 # RISCV CRT parameters
@@ -57,7 +57,7 @@
 # Additional arguments for utility in charge of generating bin and vmem outputs
 # These variables are expected to be used in custom_target rules.
 embedded_target_output = ['@BASENAME@.bin', '@BASENAME@.dis', '@BASENAME@.vmem']
-embedded_target_args = [prog_python, '../util/embedded_target.py',
+embedded_target_args = [prog_python, meson.source_root() + '/util/embedded_target.py',
   '--objcopy', prog_objcopy, '--srec_cat', prog_srec_cat, '--objdump', prog_objdump,
   '--input', '@INPUT@', '--basename', '@BASENAME@', '--outdir', '@OUTDIR@']
 
diff --git a/meson_init.sh b/meson_init.sh
index fbf6d61..f3204e9 100755
--- a/meson_init.sh
+++ b/meson_init.sh
@@ -6,11 +6,13 @@
 set -o errexit
 set -o pipefail
 set -o nounset
-set -x
 
-readonly BUILD_DIR_PREFIX="build"
-readonly TARGET_VERILATOR="verilator"
-readonly TARGET_FPGA="fpga"
+. util/build_consts.sh
+
+echo "Detected \$REPO_TOP at $REPO_TOP."
+echo "Object directory set at $OBJ_DIR."
+echo "Binary directory set at $BIN_DIR."
+echo
 
 function usage() {
   cat << USAGE
@@ -57,17 +59,16 @@
 fi
 
 if [[ "${FLAGS_force}" == true ]]; then
-  for target_suffix in "${TARGET_VERILATOR}" "${TARGET_FPGA}"; do
-    rm -rf "${BUILD_DIR_PREFIX}-${target_suffix}"
-  done
+  rm -rf $BUILD_ROOT/build-*
 fi
 
 if [[ ! -n "${FLAGS_reconfigure}" ]] ; then
-  for target_suffix in "${TARGET_VERILATOR}" "${TARGET_FPGA}"; do
-    if [[ -d "${BUILD_DIR_PREFIX}-${target_suffix}" ]]; then
+  for platform in "${PLATFORMS[@]}"; do
+    obj_dir="$(sw_obj_dir "$platform")"
+    if [[ -d "$obj_dir" ]]; then
       usage >&2
-      echo "Error: ${BUILD_DIR_PREFIX}-${target_suffix} already exists. " \
-           "Remove directory, or rerun $0 with the -r option" >&2
+      echo "Error: $obj_dir  already exists. Remove directory, or rerun $0 " \
+        "with the -r option" >&2
       exit 1
     fi
   done
@@ -99,19 +100,13 @@
   perl -pi -e 's#-I[^/][^@ ]+ # #g' -- "$ninja_file"
 }
 
-# configure_meson $target generates a build directory at build-$target.
-function configure_meson() {
-  local target="$1"
-  local build_dir="${BUILD_DIR_PREFIX}-$target"
-
-  mkdir -p "$build_dir"
+for platform in ${PLATFORMS[@]}; do
+  obj_dir="$(sw_obj_dir "$platform")"
+  mkdir -p "$obj_dir"
   meson ${FLAGS_reconfigure} \
-    -Dtarget="$target" \
+    -Dtarget="$platform" \
     --cross-file="$CROSS_FILE" \
     --buildtype=plain \
-    "$build_dir"
-  purge_includes "$build_dir"
-}
-
-configure_meson "${TARGET_VERILATOR}"
-configure_meson "${TARGET_FPGA}"
+    "$obj_dir"
+  purge_includes "$obj_dir"
+done
diff --git a/meson_options.txt b/meson_options.txt
index e78fb4c..fd89ba6 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1 +1 @@
-option('target', type : 'combo', choices: ['verilator', 'fpga', 'undef'], value : 'undef')
+option('target', type : 'combo', choices: ['sim-verilator', 'fpga', 'undef'], value : 'undef')
diff --git a/sw/device/boot_rom/meson.build b/sw/device/boot_rom/meson.build
index b84fb45..e053cef 100644
--- a/sw/device/boot_rom/meson.build
+++ b/sw/device/boot_rom/meson.build
@@ -12,7 +12,7 @@
 
 # ROM linker parameters.
 rom_linkfile = files(['rom_link.ld'])
-rom_link_args = ['-Wl,-T,@0@/@1@'.format('..', rom_linkfile[0])]
+rom_link_args = ['-Wl,-T,@0@/@1@'.format(meson.source_root(), rom_linkfile[0])]
 rom_link_deps = [rom_linkfile]
 
 custom_target(
diff --git a/util/build_consts.sh b/util/build_consts.sh
new file mode 100644
index 0000000..d0a6632
--- /dev/null
+++ b/util/build_consts.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+# This file provides common definitions for build output locations in the
+# OpenTitan repository; scripts that wish to use it should |source| it at their
+# start.
+#
+# OpenTitan has two build directories:
+# - $OBJ_DIR, which contains all outputs and intermediates of the build
+#   process, with an unstable directory structure.
+# - $BIN_DIR, which contains "executable" outputs of the build process, such as
+#   binaries, tests, and bitstreams. It has a stable directory structure.
+#
+# $OBJ_DIR and $BIN_DIR are the subdirectories build-out and build-bin of
+# $BUILD_ROOT, which can be configured to any desired directory. Build artifacts
+# can be cleaned out by running |rm -rf $BUILD_ROOT/build-*|.
+
+# We cannot rely on the location of this file for anything, since it is meant to
+# be sourced, not executed. As such, we use git to compute $REPO_TOP.
+readonly REPO_TOP="$(git rev-parse --show-toplevel)"
+BUILD_ROOT="${BUILD_ROOT:-"$REPO_TOP"}"
+
+readonly OBJ_DIR="$BUILD_ROOT/build-out"
+readonly BIN_DIR="$BUILD_ROOT/build-bin"
+
+# PLATFORMS is an array of all of the "device platforms" which OpenTitan
+# software can be built for. These include:
+# - 'sim-verilator', i.e., Verilator.
+# - 'fpga', i.e., a NexysVideo FPGA board.
+readonly PLATFORMS=(
+  'sim-verilator'
+  'fpga'
+)
+
+# sw_obj_dir takes a platform name as an argument and produces a path to a
+# subdirectory of $OBJ_DIR where its build action artifacts should be written.
+#
+# The output of this function should be considered scratch space and not stable.
+function sw_obj_dir() {
+  echo "$OBJ_DIR/sw/$1"
+}
+
+# sw_bin_dir takes a platform name as an argument and produces a path to the
+# subdirectory of $BIN_DIR where its completed build outputs should be written.
+function sw_bin_dir() {
+  echo "$BIN_DIR/sw/device/$1"
+}
+
+# $HOST_BIN_DIR is a subdirectory of $BIN_DIR where host build outputs (i.e.,
+# compiled programs that should run on a host workstation or server) should be
+# written.
+HOST_BIN_DIR="$BIN_DIR/sw/host"
diff --git a/util/fpga/splice_nexysvideo.sh b/util/fpga/splice_nexysvideo.sh
index 9546055..578740b 100755
--- a/util/fpga/splice_nexysvideo.sh
+++ b/util/fpga/splice_nexysvideo.sh
@@ -14,9 +14,10 @@
 #  lowrisc_systems_top_earlgrey_nexysvideo_0.1.splice.bit
 set -e
 
-BUILD_DIR=build-fpga
-TARGET_PREFIX="$BUILD_DIR/sw/device/boot_rom/boot_rom"
-#TARGET_PREFIX="sw/${BUILD_DIR}/rom"
+. util/build_consts.sh
+
+BUILD_DIR="$(sw_obj_dir fpga)"
+TARGET_PREFIX="/sw/device/boot_rom/boot_rom"
 FPGA_BUILD_DIR=build/lowrisc_systems_top_earlgrey_nexysvideo_0.1/synth-vivado/
 FPGA_BIT_NAME=lowrisc_systems_top_earlgrey_nexysvideo_0.1
 
diff --git a/util/make_build_bin.sh b/util/make_build_bin.sh
new file mode 100755
index 0000000..8c02ae0
--- /dev/null
+++ b/util/make_build_bin.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+set -e
+
+# make_build_bin.sh takes the unstructured contents of $OBJ_DIR and copies them
+# into the stable file structure of $BIN_DIR. By default, this script will skip
+# any subdirectory of $OBJ_DIR unknown to it, but setting $MUST_COPY_ALL will
+# cause trigger a hard error if any $OBJ_DIR subdir is missing.
+
+. util/build_consts.sh
+
+for platform in "${PLATFORMS[@]}"; do
+  obj_dir="$(sw_obj_dir "$platform")"
+  echo "Copying object directory $obj_dir."
+  if [[ ! -d "$obj_dir" ]]; then
+    if [[ -z ${MUST_COPY_ALL+x} ]]; then
+      echo "Error: Object directory for $platform does not exist; skipping."
+      continue
+    else
+      echo "Error: Object directory for $platform does not exist; aborting."
+      exit 1
+    fi
+  fi
+
+  bin_dir="$(sw_bin_dir "$platform")"
+  # NOTE: This find excludes all directory paths with '@' symbols in them, which
+  # are used by Meson to indicate unexported build artifacts, like .o and .a files.
+  for path in $(find "$obj_dir/sw/device" -type f -regex '[^@]+'); do
+    # NOTE: The '#' substitution operator strips the prefix $obj_root from $path.
+    rel_dir="$(dirname "${path#"$obj_dir/sw/device/"}")"
+    mkdir -p "$bin_dir/$rel_dir"
+    cp "$path" "$bin_dir/$rel_dir"
+  done
+
+  # TODO: "Host" binaries must be copied separately. Currently, Meson will compile
+  # them once per platform, even though they are the same for all platforms.
+  # As such, we copy them from the first object directory we encounter.
+  if [[ -z ${found_host_bins+x} ]]; then
+    host_obj_dir="$obj_dir/sw/host"
+    if [[ ! -d "$host_obj_dir" ]]; then
+      continue
+    fi
+    echo "Copying host binaries from $host_obj_dir."
+    for path in $(find "$host_obj_dir" -type f -regex '[^@]+'); do
+      rel_dir="$(dirname "${path#$host_obj_dir}")"
+      mkdir -p "$HOST_BIN_DIR/$rel_dir"
+      cp "$path" "$HOST_BIN_DIR/$rel_dir"
+    done
+    found_host_bins=true
+  fi
+done
diff --git a/util/rom_chip_info.py b/util/rom_chip_info.py
index 93cadf6..b353ef9 100755
--- a/util/rom_chip_info.py
+++ b/util/rom_chip_info.py
@@ -53,8 +53,10 @@
     outdir.mkdir(parents=True, exist_ok=True)
     out_path = outdir / "chip_info.h"
 
-
-    repo = Repo(search_parent_directories=True)
+    # This file may invoked from some random place outside the repository, so
+    # we need to make sure to do a git lookup relative to *this* file.
+    this_dir = Path(__file__).resolve().parent
+    repo = Repo(path=str(this_dir), search_parent_directories=True)
     repo_info = repo.head.object.hexsha
 
     now = datetime.now()