build/toolbase/gotoolchain: init

Another piece of the toolbase puzzle, this one is a library which
provides information about the Go SDK picked by Bazel/rules_go, and
allows to build tools that call the `go` tool.

This is effectively the logic from //build/fietsje:def.bzl, but
rewritten to be reusable. In a later CL, we will make Fietsje use this
logic instead of its existing starlark/shell magic.

Change-Id: I2be723089410c81843b54df77bcd665a4e050cbb
Reviewed-on: https://review.monogon.dev/c/monogon/+/329
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
diff --git a/build/toolbase/gotoolchain/BUILD.bazel b/build/toolbase/gotoolchain/BUILD.bazel
new file mode 100644
index 0000000..9579e2c
--- /dev/null
+++ b/build/toolbase/gotoolchain/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+load(":def.bzl", "toolchain_library")
+
+toolchain_library(
+    name = "toolchain_library",
+    importpath = "source.monogon.dev/build/toolbase/gotoolchain",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@io_bazel_rules_go//go/tools/bazel:go_default_library",
+    ],
+)
+
+# keep
+go_library(
+    name = "go_default_library",
+    embed = [":toolchain_library"],
+    importpath = "source.monogon.dev/build/toolbase/gotoolchain",
+    visibility = ["//visibility:public"],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["toolchain_test.go"],
+    embed = [":go_default_library"],  # keep
+)
diff --git a/build/toolbase/gotoolchain/def.bzl b/build/toolbase/gotoolchain/def.bzl
new file mode 100644
index 0000000..1722cc4
--- /dev/null
+++ b/build/toolbase/gotoolchain/def.bzl
@@ -0,0 +1,78 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_context", "GoSource")
+
+# This implements the toolchain_library rule, which is used to generate a
+# rules_go compatible go_library-style target which contains toolchain.go.in
+# augmented with information about the Go SDK's toolchain used by Bazel.
+#
+# The library can then be used to build tools that call the `go` tool, for
+# example to perform static analysis or dependency management.
+
+def _toolchain_library_impl(ctx):
+    go = go_context(ctx)
+
+    importpath = ctx.attr.importpath
+
+    out = go.declare_file(go, ext = ".go")
+    ctx.actions.expand_template(
+        template = ctx.file._template,
+        output = out,
+        substitutions = {
+            'GOROOT': go.root,
+            'GOTOOL': go.go.path,
+        },
+    )
+
+    library = go.new_library(go)
+    source = go.library_to_source(go, struct(
+        srcs = [struct(files = [out])],
+        deps = ctx.attr.deps,
+    ), library, ctx.coverage_instrumented())
+
+    # Hack: we want to inject runfiles into the generated GoSource, because
+    # there's no other way to make rules_go pick up runfiles otherwise.
+    runfiles = ctx.runfiles(files = [
+        go.go,
+        go.sdk_root,
+    ] + go.sdk_files)
+    source = {
+        key: getattr(source, key)
+        for key in dir(source)
+        if key not in ['to_json', 'to_proto']
+    }
+    source['runfiles'] = runfiles
+    source = GoSource(**source)
+    archive = go.archive(go, source)
+
+
+    return [
+        library,
+        source,
+        archive,
+        DefaultInfo(
+            files = depset([archive.data.file]),
+            runfiles = runfiles,
+        ),
+        OutputGroupInfo(
+            cgo_exports = archive.cgo_exports,
+            compilation_outputs = [archive.data.file],
+        ),
+    ]
+
+
+toolchain_library = rule(
+    implementation = _toolchain_library_impl,
+    attrs = {
+        "importpath": attr.string(
+            mandatory = True,
+        ),
+        "deps": attr.label_list(),
+        "_template": attr.label(
+            allow_single_file = True,
+            default = ":toolchain.go.in",
+        ),
+        "_go_context_data": attr.label(
+            default = "@io_bazel_rules_go//:go_context_data",
+        ),
+    },
+    toolchains = ["@io_bazel_rules_go//go:toolchain"],
+)
diff --git a/build/toolbase/gotoolchain/toolchain.go.in b/build/toolbase/gotoolchain/toolchain.go.in
new file mode 100644
index 0000000..ca3db65
--- /dev/null
+++ b/build/toolbase/gotoolchain/toolchain.go.in
@@ -0,0 +1,24 @@
+// gotoolchain provides information about the Go toolchain used on the host by
+// rules_go.
+package gotoolchain
+
+import (
+	"fmt"
+
+	"github.com/bazelbuild/rules_go/go/tools/bazel"
+)
+
+func mustRunfile(s string) string {
+	res, err := bazel.Runfile(s)
+	if err != nil {
+		panic(fmt.Sprintf("runfile %q not found: %v", s, err))
+	}
+	return res
+}
+
+var (
+	// Go is a path to the `go` executable.
+	Go = mustRunfile(`GOTOOL`)
+	// Root is the GOROOT path.
+	Root = mustRunfile(`GOROOT`)
+)
diff --git a/build/toolbase/gotoolchain/toolchain_test.go b/build/toolbase/gotoolchain/toolchain_test.go
new file mode 100644
index 0000000..7b93d6d
--- /dev/null
+++ b/build/toolbase/gotoolchain/toolchain_test.go
@@ -0,0 +1,22 @@
+package gotoolchain
+
+import (
+	"os"
+	"os/exec"
+	"path"
+	"testing"
+)
+
+func TestGoToolRuns(t *testing.T) {
+	cmd := exec.Command(Go, "version")
+	if out, err := cmd.CombinedOutput(); err != nil {
+		t.Fatalf("Failed to run `go version`: %q, %v", string(out), err)
+	}
+}
+
+func TestGorootContainsRoot(t *testing.T) {
+	rootfile := path.Join(Root, "ROOT")
+	if _, err := os.Stat(rootfile); err != nil {
+		t.Fatalf("ROOT not found in %s", Root)
+	}
+}