*: 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/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 "$@"