metropolis/handbook: init

This is the beginning of the Metropolis Handbook, the main end-user
documentation of Metropolis.

It is built using mdbook, and currently contains only the default
content from `mdbook init`.

Future work: start writing the handbook, build in CI, publish in CI.

Change-Id: I81753350215b2f7aabc17925eadfd20706e1fdb5
Reviewed-on: https://review.monogon.dev/c/monogon/+/202
Reviewed-by: Leopold Schabel <leo@nexantic.com>
diff --git a/metropolis/handbook/.gitignore b/metropolis/handbook/.gitignore
new file mode 100644
index 0000000..7585238
--- /dev/null
+++ b/metropolis/handbook/.gitignore
@@ -0,0 +1 @@
+book
diff --git a/metropolis/handbook/BUILD.bazel b/metropolis/handbook/BUILD.bazel
new file mode 100644
index 0000000..72c3490
--- /dev/null
+++ b/metropolis/handbook/BUILD.bazel
@@ -0,0 +1,7 @@
+load("//metropolis/handbook:defs.bzl", "mdbook_html")
+
+mdbook_html(
+    name = "handbook",
+    srcs = glob(["src/**/*.md"]),
+    title = "Metropolis Handbook",
+)
diff --git a/metropolis/handbook/README.md b/metropolis/handbook/README.md
new file mode 100644
index 0000000..0864428
--- /dev/null
+++ b/metropolis/handbook/README.md
@@ -0,0 +1,38 @@
+Metropolis Handbook
+===
+
+This directory contains the sources of the Metropolis Handbook end-user documentation.
+
+Layout
+---
+
+Everything within `//monogon/handbook/src` will be used to generate documentation with [mdbook](https://rust-lang.github.io/mdBook/format/index.html).
+
+Compared to upstream mdbook we do not have a static `book.toml` file, one is instead generated as part of the build process. See the definition of the `//metropolis/handbook` target to change some of the options.
+
+Building
+---
+
+    bazel build //metropolis/handbook
+
+Then, you can visit the following file in your browser:
+
+    bazel-bin/metropolis/handbook/handbook/index.html
+
+To view the built documentation.
+
+Interactive editing
+---
+
+For faster edit/check loops of the handbook, you can use `ibazel`:
+
+    ibazel build //metropolis/handbook
+
+This will automatically rebuild the handbook any time some source changes.
+
+You will still need to manually refresh your browser to see any changes. This could be made better, if needed, by injecting some [ibazel-compatible live reload javascript](https://github.com/bazelbuild/bazel-watcher/blob/84cab6f15f64850fb972ea88701e634c8b611301/example_client/example_client.go#L24) to automatically reload the page on changes, or by adding a target which launches `mdbook serve`.
+
+Publishing
+---
+
+We currently do not build the handbook automatically and/or publish it anywhere.
diff --git a/metropolis/handbook/defs.bzl b/metropolis/handbook/defs.bzl
new file mode 100644
index 0000000..c875980
--- /dev/null
+++ b/metropolis/handbook/defs.bzl
@@ -0,0 +1,111 @@
+def _mdbook_html_impl(ctx):
+    # We will be generating our own book.toml based on this rule's configuration.
+    #
+    # We do this because:
+    #  - The book.toml must contain a reference to the source files of the
+    #    generated book. This only works as long as the source files are not
+    #    generated by Bazel.
+    #  - The book.toml file effectively describes a build, so it makes sense to
+    #    port that over to be fully managed by the Bazel rule for mdbook. This
+    #    makes things more consistent with the rest of our Bazel usage, at the
+    #    expense of slightly deviating from how upstream does things.
+    #
+    # We emit the toml into `book.toml` because that's what mdbook needs.
+    # However, instead of emitting it in a subdirectory named after this
+    # target, we do that in `${target}_`. This is so that we can use the target
+    # name as a directory containing the actual generated HTML files, making
+    # the life of developers using this rule a bit easier.
+    out_book_toml = ctx.actions.declare_file(ctx.attr.name + "_/book.toml")
+
+    # Find root of given handbook from srcs - there must be exactly one
+    # SUMMARY.md and the parent directory of that is the root.
+    summary = None
+    for f in ctx.files.srcs:
+        if not f.path.endswith('/SUMMARY.md'):
+            continue
+        if summary != None:
+            fail("More then one SUMMARY.md provided.")
+        summary = f
+    if summary == None:
+        fail("No SUMMARY.md provided in srcs.")
+
+    # We now have the SUMMARY.md file from which we can figure out the source
+    # directory of the book. However, mdbook takes a source root path relative
+    # to the book.toml file, not one relative to the current working
+    # directory... Thus, we need to prepend a list of '../' elements that bring
+    # mdbook down from the book.toml location back into the workspace execution
+    # root, which is where our SUMMARY.md path is itself rooted.
+    #
+    # For example, if book.toml lives in:
+    #   execroot/dev_source_monogon/bazel-out/k8-fastbuild/bin/metropolis/handbook/handbook_/book.toml
+    # Then we will need to prepend:
+    #   ../../../../../../../
+    # To get back to execroot/.
+    prepend = len(out_book_toml.path.split('/')) - 1
+    src_dir_path = ('../' * prepend) + summary.dirname
+
+    # Generate book.toml.
+    # Bazel does not have a toml library. We abuse JSON encoding to get
+    # serialized list/string data as an acceptable substitute to building a
+    # fully self-standing toml serializer for Bazel.
+    book_toml_contents = [
+        "[book]",
+        "title = {}".format(json.encode(ctx.attr.title)),
+        "authors = {}".format(json.encode(ctx.attr.authors)),
+        "language = {}".format(json.encode(ctx.attr.language)),
+        "multilingual = false",
+        "src = {}".format(json.encode(src_dir_path)),
+    ]
+    ctx.actions.write(
+        output = out_book_toml,
+        content = "\n".join(book_toml_contents)
+    )
+
+    out_dir = ctx.actions.declare_directory(ctx.attr.name)
+    # We also have to prepend the out dir path, for the same reasons for which
+    # we prepend src_dir_path above.
+    out_dir_path = ('../' * prepend) + out_dir.path
+    ctx.actions.run(
+        executable = ctx.executable._mdbook,
+        arguments = [
+            "build",
+            "-d", out_dir_path,
+            out_book_toml.dirname,
+        ],
+        inputs = ctx.files.srcs + [ out_book_toml ],
+        outputs = [ out_dir ],
+    )
+    return [
+        DefaultInfo(
+            files = depset([out_dir]),
+        )
+    ]
+
+mdbook_html = rule(
+    doc = "Build an mdbook source root into HTML files.",
+    implementation = _mdbook_html_impl,
+    attrs = {
+        "title": attr.string(
+            doc = "The title of the generated book.",
+        ),
+        "authors": attr.string_list(
+            default = ["Monogon Project Authors"],
+            doc = "The authors of the generated book.",
+        ),
+        "language": attr.string(
+            default = "en",
+            doc = "The language of the generated book.",
+        ),
+        "srcs": attr.label_list(
+            allow_files = True,
+            doc = "The sources of the generated book. Exaclty one file must be named SUMMARY.md, and that file's location will be used to determine the root of the book sources.",
+        ),
+
+        "_mdbook": attr.label(
+            doc = "The mdbook tool.",
+            executable = True,
+            cfg = "host",
+            default = "@dev_source_monogon//third_party/rust:cargo_bin_mdbook",
+        ),
+    },
+)
diff --git a/metropolis/handbook/src/SUMMARY.md b/metropolis/handbook/src/SUMMARY.md
new file mode 100644
index 0000000..7390c82
--- /dev/null
+++ b/metropolis/handbook/src/SUMMARY.md
@@ -0,0 +1,3 @@
+# Summary
+
+- [Chapter 1](./chapter_1.md)
diff --git a/metropolis/handbook/src/chapter_1.md b/metropolis/handbook/src/chapter_1.md
new file mode 100644
index 0000000..6786da2
--- /dev/null
+++ b/metropolis/handbook/src/chapter_1.md
@@ -0,0 +1,2 @@
+# Chapter 1
+