From 9a82d7d939df39b23e0435bf1f27916d368284f8 Mon Sep 17 00:00:00 2001
From: Cam Saul <1455846+camsaul@users.noreply.github.com>
Date: Thu, 8 Oct 2020 13:51:39 -0700
Subject: [PATCH] New driver build scripts (#13383)

* New driver build scripts and fix Google driver build

* Install Clojure CLI in Circle CI

* Fix driver build script

* Build script fix :wrench:

* More dox

* Enforce minimum build Clojure CLI version and address feedback

* New metabuild-common directory for shared stuff. Address some PR feedback
---
 .circleci/config.yml                          |   7 +-
 .gitignore                                    |   2 +-
 bin/build-driver.sh                           | 235 +-----------------
 bin/build-drivers.sh                          |  46 +---
 bin/build-drivers/README.md                   |  45 ++++
 bin/build-drivers/build_driver.clj            |  10 +
 bin/build-drivers/build_drivers.clj           |  20 ++
 .../build_drivers/build_driver.clj            | 168 +++++++++++++
 bin/build-drivers/build_drivers/checksum.clj  |  76 ++++++
 bin/build-drivers/build_drivers/common.clj    |  75 ++++++
 .../build_drivers/install_driver_locally.clj  |  50 ++++
 bin/build-drivers/build_drivers/metabase.clj  |  88 +++++++
 .../build_drivers/plugin_manifest.clj         |  55 ++++
 bin/build-drivers/build_drivers/verify.clj    |  39 +++
 bin/build-drivers/deps.edn                    |  13 +
 bin/build-drivers/verify_driver.clj           |  10 +
 bin/check-clojure-cli.sh                      |  33 +++
 bin/metabuild_common/core.clj                 |  38 +++
 bin/metabuild_common/entrypoint.clj           |  20 ++
 bin/metabuild_common/files.clj                |  79 ++++++
 bin/metabuild_common/output.clj               |  34 +++
 bin/metabuild_common/shell.clj                |  76 ++++++
 bin/metabuild_common/steps.clj                |  30 +++
 bin/metabuild_common/util.clj                 |   0
 bin/verify-driver                             |  46 +---
 modules/drivers/bigquery/parents              |   1 -
 modules/drivers/googleanalytics/parents       |   1 -
 27 files changed, 982 insertions(+), 315 deletions(-)
 create mode 100644 bin/build-drivers/README.md
 create mode 100644 bin/build-drivers/build_driver.clj
 create mode 100644 bin/build-drivers/build_drivers.clj
 create mode 100644 bin/build-drivers/build_drivers/build_driver.clj
 create mode 100644 bin/build-drivers/build_drivers/checksum.clj
 create mode 100644 bin/build-drivers/build_drivers/common.clj
 create mode 100644 bin/build-drivers/build_drivers/install_driver_locally.clj
 create mode 100644 bin/build-drivers/build_drivers/metabase.clj
 create mode 100644 bin/build-drivers/build_drivers/plugin_manifest.clj
 create mode 100644 bin/build-drivers/build_drivers/verify.clj
 create mode 100644 bin/build-drivers/deps.edn
 create mode 100644 bin/build-drivers/verify_driver.clj
 create mode 100755 bin/check-clojure-cli.sh
 create mode 100644 bin/metabuild_common/core.clj
 create mode 100644 bin/metabuild_common/entrypoint.clj
 create mode 100644 bin/metabuild_common/files.clj
 create mode 100644 bin/metabuild_common/output.clj
 create mode 100644 bin/metabuild_common/shell.clj
 create mode 100644 bin/metabuild_common/steps.clj
 create mode 100644 bin/metabuild_common/util.clj
 delete mode 100644 modules/drivers/bigquery/parents
 delete mode 100644 modules/drivers/googleanalytics/parents

diff --git a/.circleci/config.yml b/.circleci/config.yml
index ba8048ccf2e..aa132bfed66 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -566,6 +566,12 @@ jobs:
       - restore_cache:
           keys:
             - frontend-{{ checksum "./frontend-checksums.txt" }}
+      - run:
+          name: Install Clojure CLI
+          command: >
+            curl -O https://download.clojure.org/install/linux-install-1.10.1.708.sh &&
+            chmod +x linux-install-1.10.1.708.sh &&
+            sudo ./linux-install-1.10.1.708.sh
       - run:
           name: Build frontend if needed
           command: >
@@ -942,7 +948,6 @@ workflows:
           requires:
             - build-uberjar
             - fe-deps
-
       - fe-tests-cypress:
           name: fe-tests-cypress-1
           requires:
diff --git a/.gitignore b/.gitignore
index 0e0906a0b71..772e1c944bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,7 @@
 *.xcworkspacedata
 .DS_Store
 .\#*
-.cpcache
+.cpcache/
 .eastwood
 .idea/
 .nrepl-port
diff --git a/bin/build-driver.sh b/bin/build-driver.sh
index 445e1bf832d..ed438085bb6 100755
--- a/bin/build-driver.sh
+++ b/bin/build-driver.sh
@@ -1,8 +1,6 @@
 #! /usr/bin/env bash
 
-set -euo pipefail
-
-project_root=`pwd`
+set -eo pipefail
 
 driver="$1"
 
@@ -11,231 +9,8 @@ if [ ! "$driver" ]; then
     exit -1
 fi
 
-driver_project_dir="$project_root/modules/drivers/$driver"
-driver_jar="$driver.metabase-driver.jar"
-dest_location="$project_root/resources/modules/$driver_jar"
-metabase_uberjar="$project_root/target/uberjar/metabase.jar"
-target_jar="$driver_project_dir/target/uberjar/$driver_jar"
-parents=''
-checksum_file="$driver_project_dir/target/checksum.txt"
-
-################################ DELETING OLD INCORRECTLY BUILT DRIVERS ###############################
-
-verify_existing_build() {
-    verification_failed=''
-    ./bin/verify-driver "$driver" || verification_failed=true
-
-    if [ "$verification_failed" ]; then
-        echo 'No existing build, or existing build is invalid. (Re)building driver.'
-        # By removing the checksum it will force rebuilding the driver
-        rm -f "$checksum_file"
-    fi
-}
-
-
-######################################## CALCULATING CHECKSUMS ########################################
-
-md5_command=''
-if [ `command -v md5` ]; then
-    md5_command=md5
-elif [ `command -v md5sum` ]; then
-    md5_command=md5sum
-else
-    echo "Don't know what command to use to calculate md5sums."
-    exit -2
-fi
-
-# Calculate a checksum of all the driver source files. If we've already built the driver and the checksum is the same
-# there's no need to build the driver a second time
-calculate_checksum() {
-    find "$driver_project_dir" -name '*.clj' -or -name '*.yaml' | sort | xargs cat | $md5_command
-}
-
-# Check whether the saved checksum for the driver sources from the last build is the same as the current one. If so,
-# we don't need to build again.
-checksum_is_same() {
-    if [ -f "$checksum_file" ]; then
-        old_checksum=`cat "$checksum_file"`
-        current_checksum=`calculate_checksum`
-        echo "Checksum of source files for previous build: $old_checksum"
-        echo "Current checksum of source files: $current_checksum"
-        if [  "$current_checksum" == "$old_checksum" ]; then
-            # Make sure the target driver JAR actually exists as well!
-            if [ -f "$target_jar" ]; then
-                echo "$driver driver source unchanged since last build. Skipping re-build."
-                return 0
-            fi
-        fi
-    fi
-    return 1
-}
-
-######################################## BUILDING THE DRIVER ########################################
-
-# Delete existing saved copies of the driver in the plugins and resources directories
-delete_old_drivers() {
-    echo "Deleting old versions of $driver driver..."
-    rm -f plugins/"$driver_jar"
-    rm -f "$dest_location"
-}
-
-# Check if Metabase is installed locally for building drivers; install it if not
-install_metabase_core() {
-    if [ ! "$(find ~/.m2/repository/metabase-core/metabase-core -name '*.jar')" ]; then
-        echo "Building Metabase and installing locally..."
-        lein clean
-        lein install-for-building-drivers
-    else
-        echo "metabase-core already installed to local Maven repo."
-    fi
-}
-
-
-# Build Metabase uberjar if needed, we'll need this for stripping duplicate classes
-build_metabase_uberjar() {
-    if [ ! -f "$metabase_uberjar" ]; then
-        echo 'Building Metabase uberjar...'
-        lein uberjar
-    else
-        echo "Metabase uberjar already built."
-    fi
-}
-
-# Take a look at the `parents` file listing the parents to build, if applicable
-build_parents() {
-    echo "Building parent drivers (if needed)..."
-
-    parents_list="$driver_project_dir"/parents
-
-    if [ -f "$parents_list" ]; then
-        parents=`cat "$parents_list"`
-        echo "Found driver parents: $parents"
-    fi
-
-    # Check and see if we need to recursively build or install any of our parents before proceeding
-    for parent in $parents; do
-        if [ ! -f resources/modules/"$parent.metabase-driver.jar" ]; then
-            echo "Building $parent..."
-            ./bin/build-driver.sh "$parent"
-        fi
-
-        # Check whether built parent driver *JAR* exists in local Maven repo
-        parent_install_dir="~/.m2/repository/metabase/$parent-driver"
-        parent_installed_jar=''
-        if [ -f "$parent_install_dir" ]; then
-            parent_installed_jar=`find "$parent_install_dir" -name '*.jar'`
-        fi
+source "./bin/check-clojure-cli.sh"
+check_clojure_cli
 
-        if [ ! "$parent_installed_jar" ]; then
-            parent_project_dir="$project_root/modules/drivers/$parent"
-            echo "Installing $parent locally..."
-            cd "$parent_project_dir"
-            lein clean
-            lein install-for-building-drivers
-            cd "$project_root"
-        else
-            echo "$parent already installed to local Maven repo"
-        fi
-    done
-}
-
-# Build the driver uberjar itself
-build_driver_uberjar() {
-    echo "Building $driver driver..."
-
-    cd "$driver_project_dir"
-
-    rm -rf target
-
-    lein clean
-    DEBUG=1 LEIN_SNAPSHOTS_IN_RELEASE=true lein uberjar
-
-    cd "$project_root"
-
-    if [ ! -f "$target_jar" ]; then
-        echo "Error: could not find $target_jar. Build failed."
-        return -3
-    fi
-}
-
-# Strip out any classes in driver JAR found in core Metabase uberjar or parent JARs; recompress with higher compression ratio
-strip_and_compress() {
-    # ok, first things first, strip out any classes also found in the core Metabase uberjar
-    lein strip-and-compress "$target_jar"
-
-    # next, remove any classes also found in any of the parent JARs
-    for parent in $parents; do
-        echo "Removing duplicate classes with $parent uberjar..."
-        lein strip-and-compress "$target_jar" "resources/modules/$parent.metabase-driver.jar"
-    done
-}
-
-# copy finished JAR to the resources dir
-copy_target_to_dest() {
-    echo "Copying $target_jar -> $dest_location"
-    cp "$target_jar" "$dest_location"
-}
-
-# check that JAR in resources dir looks correct
-verify_build () {
-    verification_failed=''
-    ./bin/verify-driver "$driver" || verification_failed=true
-
-    if [ "$verification_failed" ]; then
-        echo "./bin/build-driver.sh $driver FAILED."
-        rm -f "$checksum_file"
-        rm -f "$target_jar"
-        rm -f "$dest_location"
-        return -4
-    fi
-}
-
-# Save the checksum for the newly built JAR
-save_checksum() {
-    echo "Saving checksum for source files to $checksum_file"
-    checksum=`calculate_checksum`
-    echo "$checksum" > "$checksum_file"
-}
-
-# Runs all the steps needed to build the driver.
-build_driver() {
-    verify_existing_build &&
-        delete_old_drivers &&
-        install_metabase_core &&
-        build_metabase_uberjar &&
-        build_parents &&
-        build_driver_uberjar &&
-        strip_and_compress &&
-        copy_target_to_dest &&
-        verify_build &&
-        save_checksum
-}
-
-######################################## PUTTING IT ALL TOGETHER ########################################
-
-clean_local_repo() {
-    echo "Deleting existing installed metabase-core and driver dependencies..."
-    rm -rf ~/.m2/repository/metabase-core
-    rm -rf ~/.m2/repository/metabase/*-driver
-}
-
-retry_clean_build() {
-    echo "Building without cleaning failed. Retrying clean build..."
-    clean_local_repo
-    build_driver
-}
-
-mkdir -p resources/modules
-
-# run only a specific step with ./bin/build-driver.sh <driver> <step>
-if [ $# -eq 2 ]; then
-    $2
-# Build driver if checksum has changed
-elif ! checksum_is_same; then
-    echo "Checksum has changed."
-    build_driver || retry_clean_build
-# Either way, always copy the target uberjar to the dest location
-else
-    echo "Checksum is unchanged."
-    (copy_target_to_dest && verify_build) || retry_clean_build
-fi
+cd bin/build-drivers
+clojure -M -m build-driver "$driver"
diff --git a/bin/build-drivers.sh b/bin/build-drivers.sh
index 163814628cf..f3b78e8f959 100755
--- a/bin/build-drivers.sh
+++ b/bin/build-drivers.sh
@@ -1,45 +1,9 @@
 #! /usr/bin/env bash
 
-set -eo pipefail
+set -euo pipefail
 
-# If ran as `./bin/build-drivers.sh clean` then uninstall metabase-core from the local Maven repo and delete
-if [ "$1" == clean ]; then
-    echo "Deleting existing installed metabase-core and driver dependencies..."
-    rm -rf ~/.m2/repository/metabase-core
-    rm -rf ~/.m2/repository/metabase/*-driver
+source "./bin/check-clojure-cli.sh"
+check_clojure_cli
 
-    echo "Deleting built drivers in resources/modules..."
-    rm -rf resources/modules
-    echo "Deleting build Metabase uberjar..."
-    rm -rf target
-
-    for target in `find modules -name 'target' -type d`; do
-        echo "Deleting $target..."
-        rm -rf "$target"
-    done
-fi
-
-# strip trailing slashes if `ls` is set to include them
-drivers=`ls modules/drivers/ | sed 's|/$||'`
-
-for driver in $drivers; do
-    echo "Build: $driver"
-
-    build_failed=''
-    ./bin/build-driver.sh "$driver" || build_failed=true
-
-    if [ "$build_failed" ]; then
-        echo "Failed to build driver $driver."
-        exit -1
-    fi
-done
-
-# Double-check that all drivers were built successfully
-for driver in $drivers; do
-    verification_failed=''
-    ./bin/verify-driver "$driver" || verification_failed=true
-
-    if [ "$verification_failed" ]; then
-        exit -2
-    fi
-done
+cd bin/build-drivers
+clojure -M -m build-drivers
diff --git a/bin/build-drivers/README.md b/bin/build-drivers/README.md
new file mode 100644
index 00000000000..0033455aba5
--- /dev/null
+++ b/bin/build-drivers/README.md
@@ -0,0 +1,45 @@
+# Build-drivers scripts
+
+Scripts for building Metabase driver plugins. You must install the [Clojure CLI
+tools](https://www.clojure.org/guides/getting_started) to use these.
+
+There are three main entrypoints. Shell script wrappers are provided for convenience and compatibility.
+
+### `build-drivers`
+
+Builds *all* drivers as needed. If drivers were recently built and no relevant source code changed, skips rebuild.
+
+```
+cd bin/build-drivers
+clojure -M -m build-drivers
+
+# or
+
+./bin/build-drivers.sh
+```
+
+### `build-driver`
+
+Build a single driver as needed. Builds parent drivers if needed first.
+
+```
+cd bin/build-driver redshift
+clojure -M -m build-driver redshift
+
+# or
+
+./bin/build-driver.sh redshift
+```
+
+### `verify-driver`
+
+Verify that a built driver looks correctly built.
+
+```
+cd bin/verify-driver redshift
+clojure -M -m verify-driver redshift
+
+# or
+
+./bin/verify-driver redshift
+```
diff --git a/bin/build-drivers/build_driver.clj b/bin/build-drivers/build_driver.clj
new file mode 100644
index 00000000000..a934d4cd26f
--- /dev/null
+++ b/bin/build-drivers/build_driver.clj
@@ -0,0 +1,10 @@
+(ns build-driver
+  "Entrypoint for `bin/build-driver.sh`. Builds a single driver, if needed."
+  (:require [build-drivers.build-driver :as build-driver]
+            [metabuild-common.core :as u]))
+
+(defn -main [& [driver]]
+  (u/exit-when-finished-nonzero-on-exception
+    (when-not (seq driver)
+      (throw (ex-info "Usage: clojure -m build-driver <driver>" {})))
+    (build-driver/build-driver! (keyword driver))))
diff --git a/bin/build-drivers/build_drivers.clj b/bin/build-drivers/build_drivers.clj
new file mode 100644
index 00000000000..d65dfee59a5
--- /dev/null
+++ b/bin/build-drivers/build_drivers.clj
@@ -0,0 +1,20 @@
+(ns build-drivers
+  "Entrypoint for `bin/build-drivers.sh`. Builds all drivers, if needed."
+  (:require [build-drivers
+             [build-driver :as build-driver]
+             [common :as c]]
+            [clojure.java.io :as io]
+            [metabuild-common.core :as u]))
+
+(defn- all-drivers []
+  (map keyword (.list (io/file (c/filename c/project-root-directory "modules" "drivers")))))
+
+(defn- build-drivers! []
+  (u/step "Building all drivers"
+    (doseq [driver (all-drivers)]
+      (build-driver/build-driver! driver))
+    (u/announce "Successfully built all drivers.")))
+
+(defn -main []
+  (u/exit-when-finished-nonzero-on-exception
+    (build-drivers!)))
diff --git a/bin/build-drivers/build_drivers/build_driver.clj b/bin/build-drivers/build_drivers/build_driver.clj
new file mode 100644
index 00000000000..bc34a167c75
--- /dev/null
+++ b/bin/build-drivers/build_drivers/build_driver.clj
@@ -0,0 +1,168 @@
+(ns build-drivers.build-driver
+  "Logic for building a single driver."
+  (:require [build-drivers
+             [checksum :as checksum]
+             [common :as c]
+             [install-driver-locally :as install-locally]
+             [metabase :as metabase]
+             [plugin-manifest :as manifest]
+             [verify :as verify]]
+            [colorize.core :as colorize]
+            [environ.core :as env]
+            [metabuild-common.core :as u]))
+
+(defn- copy-driver!
+  "Copy the driver JAR from its `target/` directory to `resources/modules`/."
+  [driver]
+  (u/step (format "Copy %s driver uberjar from %s -> %s"
+                  driver
+                  (u/assert-file-exists (c/driver-jar-build-path driver))
+                  (c/driver-jar-destination-path driver))
+    (u/delete-file! (c/driver-jar-destination-path driver))
+    (u/create-directory-unless-exists! c/driver-jar-destination-directory)
+    (u/copy-file! (c/driver-jar-build-path driver)
+                  (c/driver-jar-destination-path driver))))
+
+(defn- clean-driver-artifacts!
+  "Delete built JARs of `driver`."
+  [driver]
+  (u/step (format "Delete %s driver artifacts" driver)
+    (u/delete-file! (c/driver-target-directory driver))
+    (u/delete-file! (c/driver-jar-destination-path driver))))
+
+(defn- clean-parents!
+  "Delete built JARs and local Maven installations of the parent drivers of `driver`."
+  [driver]
+  (u/step (format "Clean %s parent driver artifacts" driver)
+    (doseq [parent (manifest/parent-drivers driver)]
+      (clean-driver-artifacts! parent)
+      (install-locally/clean! parent)
+      (clean-parents! parent))))
+
+(defn- clean-all!
+  "Delete all artifacts relating to building `driver`, including the driver JAR itself and installed
+  `metabase-core`/Metabase uberjar and any parent driver artifacts."
+  [driver]
+  (u/step "Clean all"
+    (clean-driver-artifacts! driver)
+    (clean-parents! driver)
+    (metabase/clean-metabase!)))
+
+(declare build-driver!)
+
+(defn- build-parents!
+  "Build and install to the local Maven repo any parent drivers of `driver` (e.g. `:google` is a parent of `:bigquery`).
+  The driver must be built as an uberjar so we can remove duplicate classes during the `strip-and-compress` stage; it
+  must be installed as a library so we can use it as a `:provided` dependency when building the child driver."
+  [driver]
+  (u/step (format "Build %s parent drivers" driver)
+    (doseq [parent (manifest/parent-drivers driver)]
+      (build-parents! parent)
+      (install-locally/install-locally! parent)
+      (build-driver! parent))
+    (u/announce "%s parents built successfully." driver)))
+
+(defn- strip-and-compress-uberjar!
+  "Remove any classes in compiled `driver` that are also present in the Metabase uberjar or parent drivers. The classes
+  will be available at runtime, and we don't want to make things unpredictable by including them more than once in
+  different drivers.
+
+  This is only needed because `lein uberjar` does not seem to reliably exclude classes from `:provided` Clojure
+  dependencies like `metabase-core` and the parent drivers."
+  ([driver]
+   (u/step (str (format "Strip out any classes in %s driver JAR found in core Metabase uberjar or parent JARs" driver)
+                " and recompress with higher compression ratio")
+     (let [uberjar (u/assert-file-exists (c/driver-jar-build-path driver))]
+       (u/step "strip out any classes also found in the core Metabase uberjar"
+         (strip-and-compress-uberjar! uberjar (u/assert-file-exists c/metabase-uberjar-path)))
+       (u/step "remove any classes also found in any of the parent JARs"
+         (doseq [parent (manifest/parent-drivers driver)]
+           (strip-and-compress-uberjar! uberjar (u/assert-file-exists (c/driver-jar-build-path parent))))))))
+
+  ([target source]
+   (u/step (format "Remove classes from %s that are present in %s and recompress" target source)
+     (u/sh {:dir c/project-root-directory}
+           "lein"
+           "strip-and-compress"
+           (u/assert-file-exists target)
+           (u/assert-file-exists source)))))
+
+(defn- build-uberjar! [driver]
+  (u/step (format "Build %s uberjar" driver)
+    (u/delete-file! (c/driver-target-directory driver))
+    (u/sh {:dir (c/driver-project-dir driver)} "lein" "clean")
+    (u/sh {:dir (c/driver-project-dir driver)
+           :env {"LEIN_SNAPSHOTS_IN_RELEASE" "true"
+                 #_"DEBUG"                     #_"1"
+                 "JAVA_HOME"                 (env/env :java-home)
+                 "HOME"                      (env/env :user-home)}}
+          "lein" "uberjar")
+    (strip-and-compress-uberjar! driver)
+    (u/announce "%s uberjar build successfully." driver)))
+
+(defn- build-and-verify!
+  "Build `driver` and verify the built JAR. This function ignores any existing artifacts and will always rebuild."
+  [driver]
+  (u/step (str (colorize/green "Build ") (colorize/yellow driver) (colorize/green " driver"))
+    (clean-driver-artifacts! driver)
+    (metabase/build-metabase!)
+    (build-parents! driver)
+    (build-uberjar! driver)
+    (copy-driver! driver)
+    (verify/verify-driver driver)
+    (u/step (format "Save checksum for %s driver to %s" driver (c/driver-checksum-filename driver))
+      (spit (c/driver-checksum-filename driver) (checksum/driver-checksum driver)))))
+
+(defn- driver-checksum-matches?
+  "Check whether the saved checksum for the driver from the last build is the same as the current one. If so, we don't
+  need to build again. This checksum is based on driver sources as well as the checksums for Metabase sources and
+  parent drivers."
+  [driver]
+  (u/step (format "Determine whether %s driver source files have changed since last build" driver)
+    (let [existing-checksum (checksum/existing-driver-checksum driver)
+          current-checksum  (checksum/driver-checksum driver)
+          same?             (= existing-checksum current-checksum)]
+      (u/announce (if same?
+                    "Checksum is the same. Do not need to rebuild driver."
+                    "Checksum is different. Need to rebuild driver."))
+      same?)))
+
+(defn build-driver!
+  "Build `driver`, if needed."
+  [driver]
+  {:pre [(keyword? driver)]}
+  (u/step (str (colorize/green "Build ") (colorize/yellow driver) (colorize/green " driver if needed"))
+    ;; When we build a driver, we save a checksum of driver source code + metabase source code + parent drivers
+    ;; alongside the built driver JAR. The next time this script is called, we recalculate that checksum -- if the
+    ;; current checksum matches the saved one associated with the built driver JAR, we do not need to rebuild the
+    ;; driver. If anything relevant has changed, we have to rebuild the driver.
+    (if (driver-checksum-matches? driver)
+      ;; even if we're not rebuilding the driver, copy the artifact from `modules/drivers/<driver>/target/uberjar/`
+      ;; to `resources/modules` so we can be sure we have the most up-to-date version there.
+      (try
+        (verify/verify-driver driver)
+        (copy-driver! driver)
+        ;; if verification fails, delete all the existing artifacts and just rebuild the driver from scratch.
+        (catch Throwable e
+          (u/error "Error verifying existing driver:\n%s" (pr-str e))
+          (u/announce "Deleting existing driver artifacts and rebuilding.")
+          (clean-driver-artifacts! driver)
+          (build-driver! driver)))
+      ;; if checksum does not match, build and verify the driver
+      (try
+        (build-and-verify! driver)
+        ;; if building fails, clean everything, including metabase-core, the metabase uberjar, and parent
+        ;; dependencies, *then* retry.
+        (catch Throwable e
+          (u/announce "Cleaning ALL and retrying...")
+          (clean-all! driver)
+          (try
+            (build-and-verify! driver)
+            ;; if building the driver failed again, even after cleaning, delete anything that was built and then
+            ;; give up.
+            (catch Throwable e
+              (u/safe-println (colorize/red (format "Failed to build %s driver." driver)))
+              (clean-driver-artifacts! driver)
+              (throw e))))))
+    ;; if we make it this far, we've built the driver successfully.
+    (u/announce "Success.")))
diff --git a/bin/build-drivers/build_drivers/checksum.clj b/bin/build-drivers/build_drivers/checksum.clj
new file mode 100644
index 00000000000..b6f8e2ae6f9
--- /dev/null
+++ b/bin/build-drivers/build_drivers/checksum.clj
@@ -0,0 +1,76 @@
+(ns build-drivers.checksum
+  "Shared code for calculating and reading hex-encoded MD5 checksums for relevant files."
+  (:require [build-drivers
+             [common :as c]
+             [plugin-manifest :as manifest]]
+            [clojure.java.io :as io]
+            [clojure.string :as str]
+            [metabuild-common.core :as u])
+  (:import org.apache.commons.codec.digest.DigestUtils))
+
+(defn checksum-from-file
+  "Read a saved MD5 hash checksum from a file."
+  [filename]
+  (u/step (format "Read saved checksum from %s" filename)
+    (let [file (io/file filename)]
+      (if-not (.exists file)
+        (u/announce "%s does not exist" filename)
+        (when-let [[checksum-line] (not-empty (str/split-lines (slurp file)))]
+          (when-let [[_ checksum-hex] (re-matches #"(^[0-9a-f]{32}).*$" checksum-line)]
+            (u/safe-println (format "Saved checksum is %s" checksum-hex))
+            checksum-hex))))))
+
+;;; -------------------------------------------- Metabase source checksum --------------------------------------------
+
+(defn- metabase-source-paths []
+  (sort
+   (cons
+    (c/filename c/project-root-directory "project.clj")
+    (mapcat (fn [dir]
+              (try
+                (u/find-files dir #(str/ends-with? % ".clj"))
+                (catch Throwable _
+                  [])))
+            [(c/filename c/project-root-directory "src")
+             (c/filename c/project-root-directory "enterprise" "backend" "src")
+             (c/filename c/project-root-directory "backend" "mbql")]))))
+
+(defn metabase-source-checksum
+  "Checksum of Metabase backend source files and `project.clj`."
+  ^String []
+  (let [paths (metabase-source-paths)]
+    (u/step (format "Calculate checksum for %d Metabase source files" (count paths))
+      (let [checksum (DigestUtils/md5Hex (str/join (map slurp paths)))]
+        (u/safe-println (format "Current checksum is %s" checksum))
+        checksum))))
+
+
+;;; ---------------------------------------------- Driver source files -----------------------------------------------
+
+(defn existing-driver-checksum
+  "Checksum from the relevant sources from last time we built `driver`."
+  [driver]
+  (checksum-from-file (c/driver-checksum-filename driver)))
+
+(defn- driver-source-paths
+  "Returns sequence of the source filenames for `driver`."
+  [driver]
+  (u/find-files (c/driver-project-dir driver)
+                (fn [path]
+                  (or (and (str/ends-with? path ".clj")
+                           (not (str/starts-with? path (c/filename (c/driver-project-dir driver) "test"))))
+                      (str/ends-with? path ".yaml")))))
+
+(defn driver-checksum
+  "The driver checksum is based on a checksum of all the driver source files (`.clj` files and the plugin manifest YAML
+  file) combined with the checksums for `metabase-core` *and* the parent drivers. After building a driver, we save
+  this checksum. Next time the script is ran, we recalculate the checksum to determine whether anything relevant has
+  changed -- if it has, and the current checksum doesn't match the saved one, we need to rebuild the driver."
+  ^String [driver]
+  (let [source-paths (driver-source-paths driver)]
+    (u/step (format "Calculate checksum for %d files: %s ..." (count source-paths) (first source-paths))
+      (let [checksum (DigestUtils/md5Hex (str/join (concat [(metabase-source-checksum)]
+                                                           (map driver-checksum (manifest/parent-drivers driver))
+                                                           (map slurp (driver-source-paths driver)))))]
+        (u/safe-println (format "Current checksum is %s" checksum))
+        checksum))))
diff --git a/bin/build-drivers/build_drivers/common.clj b/bin/build-drivers/build_drivers/common.clj
new file mode 100644
index 00000000000..21f45bea25d
--- /dev/null
+++ b/bin/build-drivers/build_drivers/common.clj
@@ -0,0 +1,75 @@
+(ns build-drivers.common
+  "Shared constants and functions related to source and artifact paths used throughout this code."
+  (:require [clojure.string :as str]
+            [environ.core :as env])
+  (:import java.io.File))
+
+;; since this file is used pretty much everywhere, this seemed like a good place to put this.
+(set! *warn-on-reflection* true)
+
+(when-not (str/ends-with? (env/env :user-dir) "/build-drivers")
+  (throw (ex-info "Please run build-driver scripts from the `bin/build-drivers` directory e.g. `cd bin/build-drivers; clojure -m build-driver`"
+                  {:user-dir (env/env :user-dir)})))
+
+(defn env-or-throw
+  "Fetch an env var value or throw an Exception if it is unset."
+  [k]
+  (or (get env/env k)
+      (throw (Exception. (format "%s is unset. Please set it and try again." (str/upper-case (str/replace (name k) #"-" "_")))))))
+
+(defn filename [& path-components]
+  (str/join File/separatorChar path-components))
+
+(def ^String project-root-directory
+  "e.g. /Users/cam/metabase"
+  (.. (File. ^String (env/env :user-dir)) getParentFile getParentFile getAbsolutePath))
+
+(def ^String maven-repository-path
+  (filename (env/env :user-home) ".m2" "repository"))
+
+
+;;; -------------------------------------------------- Driver Paths --------------------------------------------------
+
+(defn driver-project-dir
+  "e.g. \"/home/cam/metabase/modules/drivers/redshift\""
+  [driver]
+  (filename project-root-directory "modules" "drivers" (name driver)))
+
+(defn driver-jar-name
+  "e.g. \"redshift.metabase-driver.jar\""
+  [driver]
+  (format "%s.metabase-driver.jar" (name driver)))
+
+(defn driver-target-directory
+  [driver]
+  (filename (driver-project-dir driver) "target"))
+
+(defn driver-jar-build-path
+  "e.g. \"/home/cam/metabase/modules/drivers/redshift/target/uberjar/redshift.metabase-driver.jar\""
+  [driver]
+  (filename (driver-target-directory driver) "uberjar" (driver-jar-name driver)))
+
+(def ^String driver-jar-destination-directory
+  (filename project-root-directory "resources" "modules"))
+
+(defn driver-jar-destination-path
+  "e.g. \"/home/cam/metabase/resources/modules/redshift.metabase-driver.jar\""
+  [driver]
+  (filename driver-jar-destination-directory (driver-jar-name driver)))
+
+(defn driver-checksum-filename
+  "e.g. \"/home/cam/metabase/modules/drivers/redshift/target/checksum.txt\""
+  [driver]
+  (filename (driver-project-dir driver) "target" "checksum.txt")) ; TODO - rename to checksum.md5
+
+(defn driver-plugin-manifest-filename
+  "e.g. \"/home/cam/metabase/modules/drivers/bigquery/resources/plugin-manifest.yaml\""
+  [driver]
+  (filename (driver-project-dir driver) "resources" "metabase-plugin.yaml"))
+
+
+;;; ------------------------------------------ Metabase Local Install Paths ------------------------------------------
+
+(def ^String metabase-uberjar-path
+  "e.g. \"home/cam/metabase/target/uberjar/metabase.jar\""
+  (filename project-root-directory "target" "uberjar" "metabase.jar"))
diff --git a/bin/build-drivers/build_drivers/install_driver_locally.clj b/bin/build-drivers/build_drivers/install_driver_locally.clj
new file mode 100644
index 00000000000..484e880cc90
--- /dev/null
+++ b/bin/build-drivers/build_drivers/install_driver_locally.clj
@@ -0,0 +1,50 @@
+(ns build-drivers.install-driver-locally
+  "Logic related to installing a driver as a library in the local Maven repository so it can be used as a dependency
+  when building descandant drivers. Right now this is only used for `:google`, which is used by `:bigquery` and
+  `:googleanalytics`."
+  (:require [build-drivers
+             [checksum :as checksum]
+             [common :as c]]
+            [colorize.core :as colorize]
+            [metabuild-common.core :as u]))
+
+(defn- driver-local-install-path [driver]
+  (c/filename c/maven-repository-path "metabase" (format "%s-driver" (name driver))))
+
+(defn- driver-local-install-checksum-filename [driver]
+  (c/filename (driver-local-install-path driver) "checksum.md5"))
+
+(defn clean!
+  "Delete local Maven installation of the library version of `driver`."
+  [driver]
+  (u/step (format "Deleting existing Maven installation of %s driver" driver)
+    (u/delete-file! (driver-local-install-path driver))))
+
+(defn- local-install-checksum-matches?
+  "After installing the library version of `driver`, we save a checksum based on its sources; next time we call
+  `install-locally!`, we can recalculate the checksum; if the saved one matches the current one, we do not need to
+  reinstall."
+  [driver]
+  (u/step "Determine whether %s driver source files have changed since last local install"
+    (let [existing-checksum (checksum/checksum-from-file (driver-local-install-checksum-filename driver))
+          current-checksum  (checksum/driver-checksum driver)
+          same?             (= existing-checksum current-checksum)]
+      (u/announce (if same?
+                    "Checksum is the same. Do not need to rebuild driver."
+                    "Checksum is different. Need to rebuild driver."))
+      same?)))
+
+(defn install-locally!
+  "Install `driver` as a library in the local Maven repository IF NEEDED so descendant drivers can use it as a
+  `:provided` dependency when building. E.g. before building `:bigquery` we need to install `:google` as a library
+  locally."
+  [driver]
+  {:pre [(keyword? driver)]}
+  (u/step (str (colorize/green "Install ") (colorize/yellow driver) (colorize/green " driver to local Maven repo if needed"))
+    (if (local-install-checksum-matches? driver)
+      (u/announce "Already installed locally.")
+      (u/step (str (colorize/green "Install ") (colorize/yellow driver) (colorize/green " driver to local Maven repo"))
+        (u/sh {:dir (c/driver-project-dir driver)} "lein" "clean")
+        (u/sh {:dir (c/driver-project-dir driver)} "lein" "install-for-building-drivers")
+        (u/step (format "Save checksum to %s" driver (driver-local-install-checksum-filename driver))
+          (spit (driver-local-install-checksum-filename driver) (checksum/driver-checksum driver)))))))
diff --git a/bin/build-drivers/build_drivers/metabase.clj b/bin/build-drivers/build_drivers/metabase.clj
new file mode 100644
index 00000000000..5ba36429de0
--- /dev/null
+++ b/bin/build-drivers/build_drivers/metabase.clj
@@ -0,0 +1,88 @@
+(ns build-drivers.metabase
+  "Code for installing the main Metabase project as a library (`metabase-core`) in the local Maven repository, and for
+  building a Metabase uberjar. Both are needed when building drivers."
+  (:require [build-drivers
+             [checksum :as checksum]
+             [common :as c]]
+            [metabuild-common.core :as u]))
+
+(def ^String ^:private uberjar-checksum-path
+  (str c/metabase-uberjar-path ".md5"))
+
+(def ^String ^:private metabase-core-install-path
+  (c/filename c/maven-repository-path "metabase-core"))
+
+(def ^String ^:private metabase-core-checksum-path
+  (c/filename metabase-core-install-path "checksum.md5"))
+
+(defn metabase-core-checksum-matches? []
+  (u/step "Determine whether Metabase source files checksum has changed since last install of metabase-core"
+    (let [existing-checksum (checksum/checksum-from-file metabase-core-checksum-path)
+          current-checksum  (checksum/metabase-source-checksum)
+          same?             (= existing-checksum current-checksum)]
+      (u/announce (if same?
+                    "Checksum is the same. Do not need to reinstall metabase-core locally."
+                    "Checksum is different. Need to reinstall metabase-core locally."))
+      same?)))
+
+(defn- delete-metabase-core-install! []
+  (u/step "Delete local installation of metabase-core"
+    (u/delete-file! metabase-core-install-path)))
+
+(defn- install-metabase-core! []
+  (u/step "Install metabase-core locally if needed"
+    (if (metabase-core-checksum-matches?)
+      (u/announce "Up-to-date metabase-core already installed to local Maven repo")
+      (do
+        (delete-metabase-core-install!)
+        (u/sh {:dir c/project-root-directory} "lein" "clean")
+        (u/sh {:dir c/project-root-directory} "lein" "install-for-building-drivers")
+        (u/step "Save checksum for local installation of metabase-core"
+          (spit metabase-core-checksum-path (checksum/metabase-source-checksum)))
+        (u/announce "metabase-core dep installed to local Maven repo successfully.")))))
+
+(defn uberjar-checksum-matches?
+  "After installing/building Metabase we save a MD5 hex checksum of Metabase backend source files (including
+  `project.clj`). The next time we run `build-metabase!`, if the checksums have changed we know we need to
+  rebuild/reinstall."
+  []
+  (u/step "Determine whether Metabase source files checksum has changed since last build of uberjar"
+    (let [existing-checksum (checksum/checksum-from-file uberjar-checksum-path)
+          current-checksum  (checksum/metabase-source-checksum)
+          same?             (= existing-checksum current-checksum)]
+      (u/announce (if same?
+                    "Checksum is the same. Do not need to rebuild Metabase uberjar."
+                    "Checksum is different. Need to rebuild Metabase uberjar."))
+      same?)))
+
+(defn- delete-metabase-uberjar! []
+  (u/step "Delete exist metabase uberjar"
+    (u/delete-file! (c/filename c/project-root-directory "target"))))
+
+(defn- build-metabase-uberjar! []
+  (u/step "Build Metabase uberjar if needed"
+    (if (uberjar-checksum-matches?)
+      (u/announce "Update-to-date uberjar already built")
+      (do
+        (delete-metabase-uberjar!)
+        (u/sh {:dir c/project-root-directory} "lein" "clean")
+        (u/sh {:dir c/project-root-directory} "lein" "uberjar")
+        (u/step "Save checksum for Metabase uberar"
+          (spit uberjar-checksum-path (checksum/metabase-source-checksum)))
+        (u/announce "Metabase uberjar built successfully")))))
+
+(defn clean-metabase!
+  "Delete local Maven repository installation of the `metabase-core` library and delete the built Metabase uberjar."
+  []
+  (u/step "Clean local Metabase deps"
+    (delete-metabase-core-install!)
+    (delete-metabase-uberjar!)))
+
+(defn build-metabase!
+  "Install `metabase-core` as a library in the local Maven repo, and build the Metabase uberjar IF NEEDED. We need to do
+  both because `metabase-core` is used as a dependency for drivers, and the Metabase uberjar is checked to make sure
+  we don't ship duplicate classes in the driver JAR (as part of the `strip-and-compress` stage.)"
+  []
+  (u/step "Build metabase-core and install locally"
+    (install-metabase-core!)
+    (build-metabase-uberjar!)))
diff --git a/bin/build-drivers/build_drivers/plugin_manifest.clj b/bin/build-drivers/build_drivers/plugin_manifest.clj
new file mode 100644
index 00000000000..163c6096475
--- /dev/null
+++ b/bin/build-drivers/build_drivers/plugin_manifest.clj
@@ -0,0 +1,55 @@
+(ns build-drivers.plugin-manifest
+  "Code for reading the YAML plugin manifest for a driver. "
+  (:require [build-drivers.common :as c]
+            [metabuild-common.core :as u]
+            [yaml.core :as yaml]))
+
+(defn- plugin-manifest
+  "Read `driver` plugin manifest and return a map."
+  [driver]
+  {:post [(map? %)]}
+  (yaml/from-file (u/assert-file-exists (c/driver-plugin-manifest-filename driver))))
+
+(defn- driver-declarations [manifest]
+  ;; driver plugin manifest can have a single `:driver`, or multiple drivers, e.g. Spark SQL which also has the
+  ;; `:hive-like` abstract driver
+  (let [{driver-declaration :driver} manifest]
+    (if (map? driver-declaration)
+      [driver-declaration]
+      driver-declaration)))
+
+(defn- declared-drivers
+  "Sequence of all drivers declared in a plugin `manifest`. Usually only one driver, except for Spark SQL which declares
+  both `:hive-like` and `:sparksql`."
+  [manifest]
+  (map (comp keyword :name) (driver-declarations manifest)))
+
+(def ^:private metabase-core-drivers
+  "Drivers that ship as part of the core Metabase project (as opposed to a plugin) and thus do not need to be built."
+  #{:sql
+    :sql-jdbc
+    :mysql
+    :h2
+    :postgres})
+
+(defn parent-drivers
+  "Get the parent drivers of a driver for purposes of building a driver. Excludes drivers that ship as part of
+  `metabase-core`, since we don't need to worry about building those.
+
+  e.g.
+
+    (parent-drivers :googleanalytics) ;-> (:google)"
+  [driver]
+  (let [manifest (plugin-manifest driver)
+        declared (declared-drivers manifest)]
+    (or (not-empty
+         (for [{parent-declaration :parent} (driver-declarations manifest)
+               :let                         [parents (if (string? parent-declaration)
+                                                       [parent-declaration]
+                                                       parent-declaration)]
+               parent                       parents
+               :let                         [parent (keyword parent)]
+               :when                        (and (not (contains? (set declared) parent))
+                                                 (not (contains? metabase-core-drivers parent)))]
+           parent))
+        (u/announce "%s does not have any parents" driver))))
diff --git a/bin/build-drivers/build_drivers/verify.clj b/bin/build-drivers/build_drivers/verify.clj
new file mode 100644
index 00000000000..16919cfbde8
--- /dev/null
+++ b/bin/build-drivers/build_drivers/verify.clj
@@ -0,0 +1,39 @@
+(ns build-drivers.verify
+  (:require [build-drivers.common :as c]
+            [colorize.core :as colorize]
+            [metabuild-common.core :as u]))
+
+(defn- verify-exists [driver]
+  (let [filename (c/driver-jar-destination-path driver)]
+    (u/step (format "Check %s exists" filename)
+      (if (u/file-exists? filename)
+        (u/announce "File exists.")
+        (throw (ex-info (format "Driver verification failed: %s does not exist" filename) {}))))))
+
+(defn- verify-has-init-class [driver]
+  (let [filename          (c/driver-jar-destination-path driver)
+        driver-init-class (format "metabase/driver/%s__init.class" (munge (name driver)))]
+    (u/step (format "Check %s contains init class file %s" filename driver-init-class)
+      (if (some (partial = driver-init-class)
+                (u/sh {:quiet? true} "jar" "-tf" filename))
+        (u/announce "Driver init class file found.")
+        (throw (ex-info (format "Driver verification failed: init class file %s not found" driver-init-class) {}))))))
+
+(defn- verify-has-plugin-manifest [driver]
+  (let [filename          (c/driver-jar-destination-path driver)
+        driver-init-class (format "metabase/driver/%s__init.class" (munge (name driver)))]
+    (u/step (format "Check %s contains metabase-plugin.yaml" filename)
+      (if (some (partial = "metabase-plugin.yaml")
+                (u/sh {:quiet? true} "jar" "-tf" filename))
+        (u/announce "Plugin manifest found.")
+        (throw (ex-info (format "Driver verification failed: plugin manifest missing" driver-init-class) {}))))))
+
+(defn verify-driver
+  "Run a series of checks to make sure `driver` was built correctly. Throws exception if any checks fail."
+  [driver]
+  {:pre [(keyword? driver)]}
+  (u/step (str (colorize/green "Verify ") (colorize/yellow driver) (colorize/green " driver"))
+    (verify-exists driver)
+    (verify-has-init-class driver)
+    (verify-has-plugin-manifest driver)
+    (u/announce (format "%s driver verification successful." driver))))
diff --git a/bin/build-drivers/deps.edn b/bin/build-drivers/deps.edn
new file mode 100644
index 00000000000..0e9cf9483eb
--- /dev/null
+++ b/bin/build-drivers/deps.edn
@@ -0,0 +1,13 @@
+{:paths ["./" "../"]
+
+ :deps
+ {cheshire/cheshire           {:mvn/version "5.8.1"}
+  commons-codec/commons-codec {:mvn/version "1.14"}
+  commons-io/commons-io       {:mvn/version "2.6"}
+  colorize/colorize           {:mvn/version "0.1.1"}
+  environ/environ             {:mvn/version "1.1.0"}
+  hiccup/hiccup               {:mvn/version "1.0.5"}
+  io.forward/yaml             {:mvn/version "1.0.9"}
+  org.flatland/ordered        {:mvn/version "1.5.7"}
+  potemkin/potemkin           {:mvn/version "0.4.5"}
+  stencil/stencil             {:mvn/version "0.5.0"}}}
diff --git a/bin/build-drivers/verify_driver.clj b/bin/build-drivers/verify_driver.clj
new file mode 100644
index 00000000000..5964ba99c0e
--- /dev/null
+++ b/bin/build-drivers/verify_driver.clj
@@ -0,0 +1,10 @@
+(ns verify-driver
+  "Entrypoint for `bin/verify-driver`. Verify that a driver JAR looks correct."
+  (:require [build-drivers.verify :as verify]
+            [metabuild-common.core :as u]))
+
+(defn -main [& [driver]]
+  (u/exit-when-finished-nonzero-on-exception
+    (when-not (seq driver)
+      (throw (ex-info "Usage: clojure -m verify-driver <driver>" {})))
+    (verify/verify-driver (keyword driver))))
diff --git a/bin/check-clojure-cli.sh b/bin/check-clojure-cli.sh
new file mode 100755
index 00000000000..0fdb8cd5df4
--- /dev/null
+++ b/bin/check-clojure-cli.sh
@@ -0,0 +1,33 @@
+#! /usr/bin/env bash
+
+set -eou pipefail
+
+you_need_to_upgrade() {
+    echo "Clojure CLI must be at least version 1.10.1.708. Your version is $version."
+    echo "See https://www.clojure.org/guides/getting_started for upgrade instructions."
+    exit -3
+}
+
+check_clojure_cli() {
+    if [ ! `which clojure` ]; then
+        echo "Please install the Clojure command line tools. See https://www.clojure.org/guides/getting_started for instructions."
+        exit -2
+    fi
+
+    version=`clojure --help | grep Version`
+    minor_version=`echo "$version" | cut -d '.' -f 2`
+    patch_version=`echo "$version" | cut -d '.' -f 3`
+    build_version=`echo "$version" | cut -d '.' -f 4`
+
+    if [ "$minor_version" -lt "10" ]; then
+        you_need_to_upgrade
+    elif [ "$minor_version" -eq "10" ]; then
+        if [ "$patch_version" -lt "1" ]; then
+            you_need_to_upgrade
+        elif [ "$patch_version" -eq "1" ]; then
+            if [ "$build_version" -lt "708" ]; then
+                you_need_to_upgrade
+            fi
+        fi
+    fi
+}
diff --git a/bin/metabuild_common/core.clj b/bin/metabuild_common/core.clj
new file mode 100644
index 00000000000..023b96c7e16
--- /dev/null
+++ b/bin/metabuild_common/core.clj
@@ -0,0 +1,38 @@
+(ns metabuild-common.core
+  (:require [metabuild-common
+             [entrypoint :as entrypoint]
+             [files :as files]
+             [output :as output]
+             [shell :as shell]
+             [steps :as steps]]
+            [potemkin :as p]))
+
+(comment entrypoint/keep-me
+         files/keep-me
+         output/keep-me
+         shell/keep-me
+         steps/keep-me)
+
+(p/import-vars
+ [entrypoint
+  exit-when-finished-nonzero-on-exception]
+
+ [files
+  assert-file-exists
+  copy-file!
+  create-directory-unless-exists!
+  delete-file!
+  file-exists?
+  find-files]
+
+ [output
+  announce
+  error
+  safe-println]
+
+ [shell
+  sh
+  sh*]
+
+ [steps
+  step])
diff --git a/bin/metabuild_common/entrypoint.clj b/bin/metabuild_common/entrypoint.clj
new file mode 100644
index 00000000000..c7767ad3264
--- /dev/null
+++ b/bin/metabuild_common/entrypoint.clj
@@ -0,0 +1,20 @@
+(ns metabuild-common.entrypoint
+  (:require [clojure.pprint :as pprint]
+            [colorize.core :as colorize]))
+
+(defn do-exit-when-finished-nonzero-on-exception [thunk]
+  (try
+    (thunk)
+    (System/exit 0)
+    (catch Throwable e
+      (let [e-map (Throwable->map e)]
+        (println (colorize/red (str "Command failed: " (:cause e-map))))
+        (binding [pprint/*print-right-margin* 120]
+          (pprint/pprint e-map))))))
+
+(defmacro exit-when-finished-nonzero-on-exception
+  "Execute `body` and catch exceptions. If an Exception is thrown, exit with status code 0; if an exception was
+  thrown, print it and exit with a non-zero status code. Intended for use in `-main` functions."
+  {:style/indent 0}
+  [& body]
+  `(do-exit-when-finished-nonzero-on-exception (fn [] ~@body)))
diff --git a/bin/metabuild_common/files.clj b/bin/metabuild_common/files.clj
new file mode 100644
index 00000000000..efda641bfa4
--- /dev/null
+++ b/bin/metabuild_common/files.clj
@@ -0,0 +1,79 @@
+(ns metabuild-common.files
+  (:require [metabuild-common
+             [output :as out]
+             [shell :as sh]
+             [steps :as steps]])
+  (:import java.io.File
+           [java.nio.file Files FileVisitOption Path Paths]
+           java.util.function.BiPredicate
+           org.apache.commons.io.FileUtils))
+
+(defn file-exists?
+  "Does a file or directory with `filename` exist?"
+  [^String filename]
+  (when filename
+    (.exists (File. filename))))
+
+(defn assert-file-exists
+  "If file with `filename` exists, return `filename` as is; otherwise, throw Exception."
+  ^String [filename & [message]]
+  (when-not (file-exists? filename)
+    (throw (ex-info (format "File %s does not exist. %s" (pr-str filename) (or message "")) {:filename filename})))
+  (str filename))
+
+(defn create-directory-unless-exists! [^String dir]
+  (when-not (file-exists? dir)
+    (steps/step (format "Creating directory %s..." dir)
+      (.mkdirs (File. dir))))
+  dir)
+
+(defn delete-file!
+  "Delete a file or directory (recursively) if it exists."
+  ([^String filename]
+   (steps/step (format "Deleting %s..." filename)
+     (if (file-exists? filename)
+       (let [file (File. filename)]
+         (if (.isDirectory file)
+           (FileUtils/deleteDirectory file)
+           (.delete file))
+         (out/safe-println (format "Deleted %s." filename)))
+       (out/safe-println (format "Don't need to delete %s, file does not exist." filename)))
+     (assert (not (file-exists? filename)))))
+
+  ([file & more]
+   (dorun (map delete-file! (cons file more)))))
+
+(defn copy-file!
+  "Copy a `source` file (or directory, recursively) to `dest`."
+  [^String source ^String dest]
+  (let [source-file (File. (assert-file-exists source))
+        dest-file   (File. dest)]
+    ;; Use native `cp` rather than FileUtils or the like because codesigning is broken when you use those because they
+    ;; don't preserve symlinks or something like that.
+    (if (.isDirectory source-file)
+      (steps/step (format "Copying directory %s -> %s" source dest)
+        (sh/sh "cp" "-R" source dest))
+      (steps/step (format "Copying file %s -> %s" source dest)
+        (sh/sh "cp" source dest))))
+  (assert-file-exists dest))
+
+(defn- ->URI ^java.net.URI [filename]
+  (java.net.URI. (str "file://" filename)))
+
+(defn- ->Path ^Path [filename]
+  (Paths/get (->URI filename)))
+
+(defn find-files
+  "Pure Java version of `find`. Recursively find files in `dir-path` that satisfy `pred`, which has the signature
+
+    (pred filename-string) -> Boolean"
+  [^String dir-path pred]
+  (->> (Files/find (->Path dir-path)
+                   Integer/MAX_VALUE
+                   (reify BiPredicate
+                     (test [_ path _]
+                       (boolean (pred (str path)))))
+                   ^FileVisitOption (make-array FileVisitOption 0))
+       .toArray
+       (map str)
+       sort))
diff --git a/bin/metabuild_common/output.clj b/bin/metabuild_common/output.clj
new file mode 100644
index 00000000000..2f004a2774b
--- /dev/null
+++ b/bin/metabuild_common/output.clj
@@ -0,0 +1,34 @@
+(ns metabuild-common.output
+  (:require [clojure.string :as str]
+            [colorize.core :as colorize]))
+
+(def ^:dynamic *steps* [])
+
+(def ^:private step-indent (str/join (repeat 2 \space)))
+
+(defn- steps-indent []
+  (str/join (repeat (count *steps*) step-indent)))
+
+(defn safe-println
+  "Thread-safe version of `println` that also indents output based on the current step build step."
+  [& args]
+  (locking println
+    (print (steps-indent))
+    (apply println args)))
+
+(defn announce
+  "Like `safe-println` + `format`, but outputs text in magenta. Use this for printing messages such as when starting
+  build steps."
+  ([s]
+   (safe-println (colorize/magenta s)))
+
+  ([format-string & args]
+   (announce (apply format (str format-string) args))))
+
+(defn error
+  "Life `safe-println` + `format`, but outputs text in red. Use this for printing error messages or Exceptions."
+  ([s]
+   (safe-println (colorize/red s)))
+
+  ([format-string & args]
+   (error (apply format format-string args))))
diff --git a/bin/metabuild_common/shell.clj b/bin/metabuild_common/shell.clj
new file mode 100644
index 00000000000..da3951c54d8
--- /dev/null
+++ b/bin/metabuild_common/shell.clj
@@ -0,0 +1,76 @@
+(ns metabuild-common.shell
+  (:require [clojure.string :as str]
+            [colorize.core :as colorize]
+            [metabuild-common
+             [output :as out]
+             [steps :as steps]])
+  (:import [java.io BufferedReader File InputStreamReader]))
+
+(defn- read-lines [^java.io.BufferedReader reader {:keys [quiet? err?]}]
+  (loop [lines []]
+    (if-let [line (.readLine reader)]
+      (do
+        (when-not quiet?
+          (out/safe-println (if err? (colorize/red line) line)))
+        (recur (conj lines line)))
+      lines)))
+
+(defn- deref-with-timeout [dereffable timeout-ms]
+  (let [result (deref dereffable timeout-ms ::timed-out)]
+    (when (= result ::timed-out)
+      (throw (ex-info (format "Timed out after %d ms." timeout-ms) {})))
+    result))
+
+(def ^:private command-timeout-ms (* 15 60 1000)) ; 15 minutes
+
+(defn sh*
+  "Run a shell command. Like `clojure.java.shell/sh`, but prints output to stdout/stderr and returns results as a vector
+  of lines.
+
+  Options:
+
+  * `env` -- environment variables (as a map) to use when running `cmd`. If `:env` is `nil`, the default parent
+    environment (i.e., the environment in which this Clojure code itself is ran) will be used; if `:env` IS passed, it
+    completely replaces the parent environment in which this script is ran -- make sure you pass anything that might be
+    needed such as `JAVA_HOME` if you do this
+
+  * `dir` -- current directory to use when running the shell command. If not specified, command is run in the same
+    current directory as the Clojure scripts, `bin/build-drivers`
+
+  * `quiet?` -- whether to suppress output from this shell command."
+  {:arglists '([cmd & args] [{:keys [env dir quiet?]} cmd & args])}
+  [& args]
+  (steps/step (colorize/blue (str "$ " (str/join " " (map (comp pr-str str) (if (map? (first args))
+                                                                              (rest args)
+                                                                              args)))))
+    (let [[opts & args]     (if (map? (first args))
+                              args
+                              (cons nil args))
+          {:keys [env dir]} opts
+          cmd-array         (into-array (map str args))
+          env-array         (when env
+                              (assert (map? env))
+                              (into-array String (for [[k v] env]
+                                                   (format "%s=%s" (name k) (str v)))))
+          proc              (.exec (Runtime/getRuntime)
+                                   ^"[Ljava.lang.String;" cmd-array
+                                   ^"[Ljava.lang.String;" env-array
+                                   ^File (when dir (File. ^String dir)))]
+      (with-open [out-reader (BufferedReader. (InputStreamReader. (.getInputStream proc)))
+                  err-reader (BufferedReader. (InputStreamReader. (.getErrorStream proc)))]
+        (let [exit-code (future (.waitFor proc))
+              out       (future (read-lines out-reader opts))
+              err       (future (read-lines err-reader (assoc opts :err? true)))]
+          {:exit (deref-with-timeout exit-code command-timeout-ms)
+           :out  (deref-with-timeout out command-timeout-ms)
+           :err  (deref-with-timeout err command-timeout-ms)})))))
+
+(defn sh
+  "Like `sh*`, but throws an Exception if the command exits with a non-zero status. Options are the same as `sh*` -- see
+  its documentation for more information."
+  {:arglists '([cmd & args] [{:keys [env dir quiet?]} cmd & args])}
+  [& args]
+  (let [{:keys [exit out err], :as response} (apply sh* args)]
+    (if (zero? exit)
+      (concat out err)
+      (throw (ex-info (str/join "\n" (concat out err)) response)))))
diff --git a/bin/metabuild_common/steps.clj b/bin/metabuild_common/steps.clj
new file mode 100644
index 00000000000..62b6a81a1eb
--- /dev/null
+++ b/bin/metabuild_common/steps.clj
@@ -0,0 +1,30 @@
+(ns metabuild-common.steps
+  (:require [colorize.core :as colorize]
+            [metabuild-common.output :as out]))
+
+(defn do-step
+  "Impl for `step` macro."
+  [step thunk]
+  (out/safe-println (colorize/green (str step)))
+  (binding [out/*steps* (conj (vec out/*steps*) step)]
+    (try
+      (thunk)
+      (catch Throwable e
+        (throw (ex-info (str step) {} e))))))
+
+(defmacro step
+  "Start a new build step, which:
+
+  1. Logs the `step` message
+  2. Indents all output inside `body` by one level
+  3. Catches any exceptions inside `body` and rethrows with additional context including `step` message
+
+  These are meant to be nested, e.g.
+
+    (step \"Build driver\"
+      (step \"Build dependencies\")
+      (step \"Build driver JAR\")
+      (step \"Verify driver\"))"
+  {:style/indent 1}
+  [step & body]
+  `(do-step ~step (fn [] ~@body)))
diff --git a/bin/metabuild_common/util.clj b/bin/metabuild_common/util.clj
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/bin/verify-driver b/bin/verify-driver
index 77a080f202b..4e463736085 100755
--- a/bin/verify-driver
+++ b/bin/verify-driver
@@ -1,50 +1,16 @@
 #! /usr/bin/env bash
 
-set -euo pipefail
+set -eo pipefail
 
 driver="$1"
 
 if [ ! "$driver" ]; then
-    echo 'Usage: ./bin/verify-driver [driver]'
+    echo "Usage: ./bin/verify-driver [driver]"
     exit -1
 fi
 
-echo "Verifying $driver driver..."
+source "./bin/check-clojure-cli.sh"
+check_clojure_cli
 
-driver_file="resources/modules/$driver.metabase-driver.jar"
-
-echo "Checking whether $driver_file exists...."
-if [ ! -f "$driver_file" ]; then
-    echo 'File does not exist. Driver verification failed.'
-    exit -2
-fi
-
-echo 'File exists.'
-
-# This assumes that the driver's main namespace is {driver}.clj, which is not necessarily required. Namespace is
-# determined by `load-namespace` in the plugin manifest
-#
-# TODO - this won't work for drivers that have slashes in the name, because of namespace munging
-munged_driver=`echo "$driver" | sed 's/-/_/g'`
-driver_main_class="metabase/driver/${munged_driver}__init.class"
-
-echo "Checking whether driver contains main class file $driver_main_class..."
-
-if [ `jar -tf "$driver_file" | grep "$driver_main_class"` ]; then
-    echo 'Main class file found.'
-else
-    echo 'Main class file missing. Driver verification failed.'
-    exit -3
-fi
-
-echo "Checking whether driver contains plugin manifest..."
-
-if [ `jar -tf "$driver_file" | grep 'metabase-plugin.yaml'` ]; then
-    echo 'Plugin manifest found.'
-else
-    echo 'Plugin manifest missing. Driver verification failed.'
-    exit -4
-fi
-
-echo 'Driver verification successful.'
-exit 0
+cd bin/build-drivers
+clojure -M -m verify-driver "$driver"
diff --git a/modules/drivers/bigquery/parents b/modules/drivers/bigquery/parents
deleted file mode 100644
index cb429113e0f..00000000000
--- a/modules/drivers/bigquery/parents
+++ /dev/null
@@ -1 +0,0 @@
-google
diff --git a/modules/drivers/googleanalytics/parents b/modules/drivers/googleanalytics/parents
deleted file mode 100644
index cb429113e0f..00000000000
--- a/modules/drivers/googleanalytics/parents
+++ /dev/null
@@ -1 +0,0 @@
-google
-- 
GitLab