*: do not require env.sh

This removes the need to source env.{sh,fish} when working with hscloud.

This is done by:

 1. Implementing a Go library to reliably detect the location of the
    active hscloud checkout. That in turn is enabled by
    BUILD_WORKSPACE_DIRECTORY being now a thing in Bazel.
 2. Creating a tool `hscloud`, with a command `hscloud workspace` that
    returns the workspace path.
 3. Wrapping this tool to be accessible from Python and Bash.
 4. Bumping all users of hscloud_root to use either the Go library or
    one of the two implemented wrappers.

We also drive-by replace tools/install.sh to be a proper sh_binary, and
make it yell at people if it isn't being ran as `bazel run
//tools:install`.

Finally, we also drive-by delete cluster/tools/nixops.sh which was never used.

Change-Id: I7873714319bfc38bbb930b05baa605c5aa36470a
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1169
Reviewed-by: informatic <informatic@hackerspace.pl>
diff --git a/cluster/clustercfg/BUILD b/cluster/clustercfg/BUILD
index 433f79c..e08a2e3 100644
--- a/cluster/clustercfg/BUILD
+++ b/cluster/clustercfg/BUILD
@@ -15,5 +15,6 @@
         requirement("idna"),
         requirement("six"),
         "//tools:secretstore_lib",
+        "//tools/hscloud:python",
     ],
 )
diff --git a/cluster/clustercfg/clustercfg.py b/cluster/clustercfg/clustercfg.py
index d852d6a..ea15df8 100644
--- a/cluster/clustercfg/clustercfg.py
+++ b/cluster/clustercfg/clustercfg.py
@@ -16,13 +16,12 @@
 import fabric
 
 from tools import secretstore
+from tools.hscloud import lib as hscloud
 
 import ca
 
 
-local_root = os.getenv('hscloud_root')
-if local_root is None:
-    raise Exception("Please source env.sh")
+local_root = hscloud.workspace_location()
 
 
 cluster = 'k0.hswaw.net'
diff --git a/cluster/doc/user.md b/cluster/doc/user.md
index f04e7db..11ea959 100644
--- a/cluster/doc/user.md
+++ b/cluster/doc/user.md
@@ -19,7 +19,10 @@
     Enter SSO/LDAP password for q3k@hackerspace.pl: 
     Good evening professor. I see you have driven here in your Ferrari.
 
-If `prodaccess` is not on your $PATH, ensure you have sourced `env.sh` from the root of hscloud and ran `tools/install.sh`.
+If `prodaccess` is not on your $PATH:
+
+    $ bazel run //tools:install
+    $ . env.sh
 
 By default, `prodaccess` will use your local user name to authenticate as `<user>@hackerspce.pl`. If your Hackerspace SSO name is different, specify it using the `-u` flag to prodaccess, eg. `prodaccess -u informatic`.
 
diff --git a/cluster/prodaccess/BUILD.bazel b/cluster/prodaccess/BUILD.bazel
index 6c72082..4db48dd 100644
--- a/cluster/prodaccess/BUILD.bazel
+++ b/cluster/prodaccess/BUILD.bazel
@@ -13,6 +13,7 @@
         "//cluster/certs:go_default_library",
         "//cluster/prodvider/proto:go_default_library",
         "//go/pki:go_default_library",
+        "//go/workspace:go_default_library",
         "@com_github_golang_glog//:go_default_library",
         "@org_golang_google_grpc//:go_default_library",
         "@org_golang_google_grpc//credentials:go_default_library",
diff --git a/cluster/prodaccess/kubernetes.go b/cluster/prodaccess/kubernetes.go
index 7226423..c50cd07 100644
--- a/cluster/prodaccess/kubernetes.go
+++ b/cluster/prodaccess/kubernetes.go
@@ -14,17 +14,18 @@
 	"github.com/golang/glog"
 
 	pb "code.hackerspace.pl/hscloud/cluster/prodvider/proto"
+	"code.hackerspace.pl/hscloud/go/workspace"
 )
 
 func kubernetesPaths() (string, string, string) {
-	localRoot := os.Getenv("hscloud_root")
-	if localRoot == "" {
-		glog.Exitf("Please source env.sh")
+	ws, err := workspace.Get()
+	if err != nil {
+		glog.Exitf("%v", err)
 	}
 
-	localKey := path.Join(localRoot, ".kubectl", fmt.Sprintf("%s.key", flagUsername))
-	localCert := path.Join(localRoot, ".kubectl", fmt.Sprintf("%s.crt", flagUsername))
-	localCA := path.Join(localRoot, ".kubectl", fmt.Sprintf("ca.crt"))
+	localKey := path.Join(ws, ".kubectl", fmt.Sprintf("%s.key", flagUsername))
+	localCert := path.Join(ws, ".kubectl", fmt.Sprintf("%s.crt", flagUsername))
+	localCA := path.Join(ws, ".kubectl", fmt.Sprintf("ca.crt"))
 
 	return localKey, localCert, localCA
 }
diff --git a/cluster/tools/BUILD b/cluster/tools/BUILD
index 2526fd7..a63245e 100644
--- a/cluster/tools/BUILD
+++ b/cluster/tools/BUILD
@@ -22,7 +22,14 @@
 sh_binary(
     name = "calicoctl",
     srcs = ["calicoctl.sh"],
-    data = [":calicoctl.bin", "//tools:secretstore"],
+    data = [
+        ":calicoctl.bin",
+        "//tools:secretstore",
+        "//tools/hscloud",
+    ],
+    deps = [
+        "//tools/hscloud:shell",
+    ],
 )
 
 copy_go_binary(
diff --git a/cluster/tools/calicoctl.sh b/cluster/tools/calicoctl.sh
index 30fe652..86e1097 100755
--- a/cluster/tools/calicoctl.sh
+++ b/cluster/tools/calicoctl.sh
@@ -1,23 +1,22 @@
 #!/usr/bin/env bash
 
-# A wrapper around the real calicoctl to configure etcd access...
+# A wrapper around the real calicoctl to configure etcd access.
 
-if [ -z "$hscloud_root" ]; then
-   echo 2>&1 "Please source env.sh"
-   exit 1
-fi
+source tools/hscloud/lib.sh || exit 1
 
-ETCD_ENDPOINTS="https://bc01n01.hswaw.net:2379,https://bc01n01.hswaw.net:2379,https://bc01n01.hswaw.net:2379"
-ETCD_KEY_FILE="$hscloud_root/cluster/secrets/plain/etcd-calico.key"
-ETCD_CERT_FILE="$hscloud_root/cluster/certs/etcd-calico.cert"
-ETCD_CA_CERT_FILE="$hscloud_root/cluster/certs/ca-etcd.crt"
+function main() {
+    local ws=$(hscloud::workspace_location)
 
-if [ ! -f "$ETCD_KEY_FILE" ] ; then
-        secretstore decrypt "$hscloud_root/cluster/secrets/cipher/etcd-calico.key" > "$ETCD_KEY_FILE"
-fi
+    export ETCD_ENDPOINTS="https://bc01n01.hswaw.net:2379,https://bc01n01.hswaw.net:2379,https://bc01n01.hswaw.net:2379"
+    export ETCD_KEY_FILE="$ws/cluster/secrets/plain/etcd-calico.key"
+    export ETCD_CERT_FILE="$ws/cluster/certs/etcd-calico.cert"
+    export ETCD_CA_CERT_FILE="$ws/cluster/certs/ca-etcd.crt"
 
-export ETCD_ENDPOINTS
-export ETCD_KEY_FILE
-export ETCD_CERT_FILE
-export ETCD_CA_CERT_FILE
-calicoctl.bin "$@"
+    if [ ! -f "$ETCD_KEY_FILE" ] ; then
+        $(hscloud::must_rlocation hscloud/tools/secretstore) decrypt "$ws/cluster/secrets/cipher/etcd-calico.key" "$ETCD_KEY_FILE"
+    fi
+
+    "$(hscloud::must_rlocation hscloud/cluster/tools/calicoctl.bin)" "$@"
+}
+
+main "$@"
diff --git a/cluster/tools/install.sh b/cluster/tools/install.sh
deleted file mode 100755
index 6f32fbb..0000000
--- a/cluster/tools/install.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-
-set -e -o pipefail
-
-if [ -z "$hscloud_root" ]; then
-    echo 2>&1 "Please first source env.sh"
-    exit 1
-fi
-
-cd "${hscloud_root}"
-
-bazel build \
-        //cluster/tools:kubectl \
-        //cluster/tools:kubecfg \
-        //cluster/tools:calicoctl \
-        //cluster/tools:cfssl
-
diff --git a/cluster/tools/nixops.sh b/cluster/tools/nixops.sh
deleted file mode 100755
index e810972..0000000
--- a/cluster/tools/nixops.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-
-# A wrapper around real nixops to decrypt GCP secret.
-
-if [ -z "$hscloud_root" ]; then
-   echo 2>&1 "Please source env.sh"
-   exit 1
-fi
-
-for f in sa.json sa.pem; do
-    plain="$hscloud_root/gcp/secrets/plain/$f"
-    cipher="$hscloud_root/gcp/secrets/cipher/$f"
-    if [ ! -f "$plain" ]; then
-        secretstore decrypt "$cipher" > "$plain"
-    fi
-done
-
-export GCE_PROJECT="hscloud"
-export GCE_SERVICE_ACCOUNT="nixops@hscloud.iam.gserviceaccount.com"
-export ACCESS_KEYPATH="$hscloud_root/gcp/secrets/plain/sa.pem"
-
-./external/nixops/bin/nixops "$@"
diff --git a/env.fish b/env.fish
index 428c5df..02cfa77 100644
--- a/env.fish
+++ b/env.fish
@@ -11,6 +11,8 @@
     set -x PATH $hscloud_path $PATH
 end
 
+# Leftover junk. This should be removed, as env.fish is now optional.
+# Do _not_ add more aliases!
 function gpg-unlock
     echo "test" | gpg2 --sign --batch --no-tty -o /dev/null
 end
diff --git a/env.sh b/env.sh
index e9cfd22..0723030 100644
--- a/env.sh
+++ b/env.sh
@@ -1,23 +1,24 @@
-# source me to have all the nice things
+# Source this file to have hscloud tools available in your PATH after running
+# `bazel run //tools:install`.
 
 if [ "$0" == "$BASH_SOURCE" ]; then
     echo "You should be sourcing this."
     exit 1
 fi
 
-export hscloud_root="$( cd "$(dirname "$BASH_SOURCE")"; pwd -P )"
-
+hscloud_root="$( cd "$(dirname "$BASH_SOURCE")"; pwd -P )"
 if [ ! -f "$hscloud_root/WORKSPACE" ]; then
     echo "Could not find WORKSPACE"
     exit 1
 fi
-
 hscloud_path="$hscloud_root/bazel-bin/tools:$hscloud_root/bazel-bin/cluster/tools"
-
 [[ ":$PATH:" != *":$hscloud_path:"* ]] && PATH="$hscloud_path:${PATH}"
+unset -f hscloud_root
+unset -f hscloud_path
 
+# Leftover junk. This should be removed, as env.sh is now optional.
+# Do _not_ add more aliases!
 alias bajzel=bazel
-
 gpg-unlock() {
     echo "test" | gpg2 --sign --batch --no-tty -o /dev/null
 }
diff --git a/go/workspace/BUILD.bazel b/go/workspace/BUILD.bazel
new file mode 100644
index 0000000..34f8acc
--- /dev/null
+++ b/go/workspace/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["workspace.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/workspace",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/workspace/workspace.go b/go/workspace/workspace.go
new file mode 100644
index 0000000..f8f2ef9
--- /dev/null
+++ b/go/workspace/workspace.go
@@ -0,0 +1,101 @@
+package workspace
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"sync"
+	"syscall"
+)
+
+// isWorkspace returns whether a given string is a valid path pointing to a
+// Bazel workspace directory.
+func isWorkspace(dir string) bool {
+	w := path.Join(dir, "WORKSPACE")
+	if _, err := os.Stat(w); err == nil {
+		return true
+	}
+	return false
+}
+
+// getPathFSID returns an opaque filesystem identifier for a given path.
+func getPathFSID(dir string) (uint64, error) {
+	st, err := os.Stat(dir)
+	if err != nil {
+		// No need to wrap err, as stat errors are already quite explicit
+		// (they also include the stat'd path).
+		return 0, err
+	}
+	switch x := st.Sys().(type) {
+	case *syscall.Stat_t:
+		return x.Dev, nil
+	default:
+		return 0, fmt.Errorf("unsupported operating system (got stat type %+v)", st.Sys())
+	}
+}
+
+// lookForWorkspace recurses up a directory until it finds a WORKSPACE, the
+// root, or crosses a filesystem boundary.
+func lookForWorkspace(dir string) (string, error) {
+	fsid, err := getPathFSID(dir)
+	if err != nil {
+		return "", fmt.Errorf("could not get initial FSID: %w", err)
+	}
+	for {
+		if dir == "." || dir == "/" || dir == "" {
+			return "", fmt.Errorf("got up to root before finding workspace")
+		}
+
+		fsid2, err := getPathFSID(dir)
+		if err != nil {
+			return "", fmt.Errorf("could not get parent FWID: %w", err)
+		}
+		if fsid2 != fsid {
+			return "", fmt.Errorf("crossed filesystem boundaries before finding workspace")
+		}
+
+		if isWorkspace(dir) {
+			return dir, nil
+		}
+		dir = path.Dir(dir)
+	}
+}
+
+// Get returns the workspace directory from which a given
+// command line tool is running. This handles the following
+// cases:
+//
+// 1. The command line tool was invoked via `bazel run`.
+// 2. The command line tool was started in the workspace directory or a
+//    subdirectory.
+//
+// If the workspace directory path cannot be inferred based on the above
+// assumptions, an error is returned.
+func Get() (string, error) {
+	workspaceOnce.Do(func() {
+		workspace, workspaceErr = get()
+	})
+	return workspace, workspaceErr
+}
+
+var (
+	workspace     string
+	workspaceErr  error
+	workspaceOnce sync.Once
+)
+
+func get() (string, error) {
+	if p := os.Getenv("BUILD_WORKSPACE_DIRECTORY"); p != "" && isWorkspace(p) {
+		return p, nil
+	}
+
+	wd, err := os.Getwd()
+	if err != nil {
+		return "", err
+	}
+	p, err := lookForWorkspace(wd)
+	if err != nil {
+		return "", fmt.Errorf("not invoked from `bazel run` and could not find workspace root by traversing upwards: %w", err)
+	}
+	return p, nil
+}
diff --git a/shell.nix b/shell.nix
index 080aca0..e295a36 100644
--- a/shell.nix
+++ b/shell.nix
@@ -10,7 +10,7 @@
   ''
     source /etc/profile
     source ${toString ./.}/env.sh
-    ${toString ./.}/tools/install.sh
+    bazel run //tools:install
 
     # Fancy colorful PS1 to make people notice easily they're in hscloud.
     PS1='\[\033]0;\u/hscloud:\w\007\]'
diff --git a/tools/BUILD b/tools/BUILD
index 64faf53..daf6c12 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -1,6 +1,17 @@
 load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar", "pkg_deb")
 load("//bzl:rules.bzl", "copy_go_binary")
 
+sh_binary(
+    name = "install",
+    srcs = [
+        "install.sh",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//tools/hscloud:shell",
+    ],
+)
+
 py_library(
     name = "secretstore_lib",
     srcs = ["secretstore.py"],
diff --git a/tools/hscloud/BUILD.bazel b/tools/hscloud/BUILD.bazel
new file mode 100644
index 0000000..ac455b3
--- /dev/null
+++ b/tools/hscloud/BUILD.bazel
@@ -0,0 +1,46 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "hscloud_lib",
+    srcs = ["main.go"],
+    importpath = "code.hackerspace.pl/hscloud/tools/hscloud",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//go/workspace:go_default_library",
+        "@com_github_spf13_cobra//:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "hscloud",
+    embed = [":hscloud_lib"],
+    visibility = ["//visibility:public"],
+)
+
+sh_library(
+    name = "shell",
+    srcs = [
+        "lib.sh",
+    ],
+    deps = [
+        "@bazel_tools//tools/bash/runfiles",
+    ],
+    data = [
+        ":hscloud",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+py_library(
+    name = "python",
+    srcs = [
+        "lib.py",
+    ],
+    deps = [
+        "@rules_python//python/runfiles",
+    ],
+    data = [
+        ":hscloud",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/tools/hscloud/lib.py b/tools/hscloud/lib.py
new file mode 100644
index 0000000..1ef2b33
--- /dev/null
+++ b/tools/hscloud/lib.py
@@ -0,0 +1,45 @@
+# Library to interact with the active hscloud checkout. This supersedes the
+# hscloud_root environment variable once used in hscloud.
+#
+# Some of this could be implemented in Python instead of shelling out to a Go
+# binary - but that way we have a single source of truth, even if it's janky.
+#
+# To use:
+#
+#    from tools.hscloud import lib as hscloud
+#
+# And specify deps = [ "//tools/hscloud:python" ] in py_binary.
+
+import subprocess
+
+from rules_python.python.runfiles import runfiles
+
+
+r = runfiles.Create()
+
+
+def tool_location():
+    """
+    Return an absolute path to a built //tools/hscloud binary, ready to run.
+    """
+    rloc = r.Rlocation("hscloud/tools/hscloud/hscloud_/hscloud")
+    if rloc is None:
+        raise Exception("Could not find location of hscloud - are you in a valid checkout?")
+    return rloc
+
+
+def workspace_location():
+    """Return an absolute path to the hscloud checkout."""
+    return subprocess.check_output([tool_location(), "workspace"]).decode()
+
+
+def must_rlocation(runfile):
+    """Return an absolute path to a runfile, eg. a data depndency in sh_binary."""
+    rloc = r.Rlocation(runfile)
+    if rloc is None:
+        msg = f"Could not find runfile {runfile}"
+        manifest = os.environ.get("RUNFILES_MANIFEST_FILE", "")
+        if manifest != "":
+            msg += f"; manifest file: {manifest}"
+        raise Exception(msg)
+    return rloc
diff --git a/tools/hscloud/lib.sh b/tools/hscloud/lib.sh
new file mode 100644
index 0000000..c45b050
--- /dev/null
+++ b/tools/hscloud/lib.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+
+# Top-level 'universal' shell library for hscloud. All sh_binary targets should
+# depend on this and import it as follows:
+#
+#  #!/usr/bin/env bash
+#  source tools/hscloud/lib.sh || exit 1
+#
+# And by specifying deps = [ "//tools/hscloud:shell" ] in sh_binary.
+
+set -e -u -o pipefail
+
+function hscloud::_prepare_runfiles() {
+    if [[ $(type -t rlocation) == function ]]; then
+        return
+    fi
+    # --- begin runfiles.bash initialization v2 ---
+    # Mostly copy-pasted from the Bazel Bash runfiles library v2.
+    local f=bazel_tools/tools/bash/runfiles/runfiles.bash
+    source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
+      source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
+      source "$0.runfiles/$f" 2>/dev/null || \
+      source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+      source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+      { echo>&2 "ERROR: cannot find $f - are you in a valid checkout and running the script via bazel?"; exit 1; }
+    # --- end runfiles.bash initialization v2 ---
+}
+
+# Return an absolute path to a built //tools/hscloud binary, ready to run.
+#
+# This will fail if we're not in a hscloud checkout.
+function hscloud::tool_location() {
+    rloc="$(hscloud::rlocation "hscloud/tools/hscloud/hscloud_/hscloud")"
+    if [ -z "$rloc" ]; then
+        echo "Could not find location of hscloud - are you in a valid checkout and running the script via bazel?" >&2
+        exit 1
+    fi
+    echo "$rloc"
+}
+
+# Return an absolute path to the hscloud checkout.
+function hscloud::workspace_location() {
+    $(hscloud::tool_location) workspace
+}
+
+# Return an absolute path to a runfile, eg. a data dependency in sh_binary.
+function hscloud::rlocation() {
+    hscloud::_prepare_runfiles
+    echo "$(rlocation "$1")"
+}
+
+# Return an absolute path to a runfile, eg. a data dependency in sh_binary.
+#
+# This will fail if the runfile is not found.
+function hscloud::must_rlocation() {
+    rloc="$(hscloud::rlocation $1)"
+    if [ -z "$rloc" ]; then
+        echo "Could not find runfile $1" >&2
+        if [ ! -z "${RUNFILES_MANIFEST_FILE:-}" ]; then
+            echo "Manifest file: $RUNFILES_MANIFEST_FILE" >&2
+        fi
+        exit 1
+    fi
+    echo "$rloc"
+}
diff --git a/tools/hscloud/main.go b/tools/hscloud/main.go
new file mode 100644
index 0000000..35f00f3
--- /dev/null
+++ b/tools/hscloud/main.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"code.hackerspace.pl/hscloud/go/workspace"
+	"github.com/spf13/cobra"
+)
+
+var rootCmd = &cobra.Command{
+	Use:   "hscloud",
+	Short: "hscloud kitchesink tool",
+	Long:  `A single entrypoint tool to interact with a hscloud git checkout.`,
+}
+
+var workspaceCmd = &cobra.Command{
+	Use:   "workspace",
+	Short: "Print root path of hscloud checkuot",
+	Long:  `This returns the directory path containing WORKSPACE. It works both from 'bazel run', when invoked as a tool in bazel or when called manually. Feel free to use this in your sh_binary scripts.`,
+	Run: func(cmd *cobra.Command, args []string) {
+		wd, err := workspace.Get()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "%v", err)
+			os.Exit(1)
+		}
+		fmt.Println(wd)
+	},
+}
+
+func main() {
+	rootCmd.AddCommand(workspaceCmd)
+	if err := rootCmd.Execute(); err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+}
diff --git a/tools/install.sh b/tools/install.sh
index 6151a38..04b4f79 100755
--- a/tools/install.sh
+++ b/tools/install.sh
@@ -1,14 +1,52 @@
 #!/usr/bin/env bash
+source tools/hscloud/lib.sh || exit 1
 
-set -e -o pipefail
+function main() {
+    # If we're not running from `bazel run/buld`, complain and re-execute
+    # ourselves.
+    #
+    # We do the check fairly low level, as //tools/hscloud:lib.sh will just
+    # fail in this case. We want to be nice.
+    #
+    # This is all mostly copied from the runfiles.bash snippet in
+    # tools/hscloud/lib.sh.
+    f=bazel_tools/tools/bash/runfiles/runfiles.bash
+    if [ ! -e "${RUNFILES_DIR:-/dev/null}/$f" ] && \
+       [ ! -e "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" ] && \
+       [ ! -e "$0.runfiles/$f" ] && \
+       [ ! -e "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" ] && \
+       [ ! -e "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" ]; then
+        echo "Uh-oh, looks like you didn't run this as 'bazel run //tools:install'.">&2
+        echo "Let me fix that for you in a few seconds - but work on your muscle memory, as we'll stop supporting this by some point.">&2
+        sleep 2
+        bazel run //tools:install -- "$@"
+        exit $?
+    fi
 
-if [ -z "$hscloud_root" ]; then
-    echo 2>&1 "Please first source env.sh"
-    exit 1
-fi
+    cd $(hscloud::workspace_location)
+    echo "Building hscloud tools and cluster tools..."
+    bazel build //tools/... //cluster/tools/...
 
-cd "${hscloud_root}"
+    local path_missing=""
+    local path="$(hscloud::workspace_location)/bazel-bin/tools"
+    if [[ ":$PATH:" == *":$path:"* ]]; then
+        path_missing="$path"
+    fi
+    local path="$(hscloud::workspace_location)/bazel-bin/cluster/tools"
+    if [[ ":$PATH:" == *":$path:"* ]]; then
+        if [ -z "$path_missing" ]; then
+            path_missing="$path"
+        else
+            path_missing="$path_missing:$path"
+        fi
+    fi
+    if [ -z "$path_missing" ]; then
+        echo "Tools built correctly, but your PATH should be updated to access them:">&2
+        echo '   PATH="$PATH:'$path_missing'"'
+        echo 'Add the above line to your shell profile, or source env.sh from the root of hscloud.'
+    else
+        echo "Tools built correctly and in PATH. Happy hsclouding!"
+    fi
+}
 
-bazel build //tools/...
-
-cluster/tools/install.sh
+main "$@"