core -> metropolis
Smalltown is now called Metropolis!
This is the first commit in a series of cleanup commits that prepare us
for an open source release. This one just some Bazel packages around to
follow a stricter directory layout.
All of Metropolis now lives in `//metropolis`.
All of Metropolis Node code now lives in `//metropolis/node`.
All of the main /init now lives in `//m/n/core`.
All of the Kubernetes functionality/glue now lives in `//m/n/kubernetes`.
Next steps:
- hunt down all references to Smalltown and replace them appropriately
- narrow down visibility rules
- document new code organization
- move `//build/toolchain` to `//monogon/build/toolchain`
- do another cleanup pass between `//golibs` and
`//monogon/node/{core,common}`.
- remove `//delta` and `//anubis`
Fixes T799.
Test Plan: Just a very large refactor. CI should help us out here.
Bug: T799
X-Origin-Diff: phab/D667
GitOrigin-RevId: 6029b8d4edc42325d50042596b639e8b122d0ded
diff --git a/metropolis/node/build/BUILD b/metropolis/node/build/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/metropolis/node/build/BUILD
diff --git a/metropolis/node/build/def.bzl b/metropolis/node/build/def.bzl
new file mode 100644
index 0000000..e2885e5
--- /dev/null
+++ b/metropolis/node/build/def.bzl
@@ -0,0 +1,257 @@
+# Copyright 2020 The Monogon Project Authors.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def _build_pure_transition_impl(settings, attr):
+ """
+ Transition that enables pure, static build of Go binaries.
+ """
+ return {
+ "@io_bazel_rules_go//go/config:pure": True,
+ "@io_bazel_rules_go//go/config:static": True,
+ }
+
+build_pure_transition = transition(
+ implementation = _build_pure_transition_impl,
+ inputs = [],
+ outputs = [
+ "@io_bazel_rules_go//go/config:pure",
+ "@io_bazel_rules_go//go/config:static",
+ ],
+)
+
+def _build_static_transition_impl(settings, attr):
+ """
+ Transition that enables static builds with CGo and musl for Go binaries.
+ """
+ return {
+ "@io_bazel_rules_go//go/config:static": True,
+ "//command_line_option:crosstool_top": "//build/toolchain/musl-host-gcc:musl_host_cc_suite",
+ }
+
+build_static_transition = transition(
+ implementation = _build_static_transition_impl,
+ inputs = [],
+ outputs = [
+ "@io_bazel_rules_go//go/config:static",
+ "//command_line_option:crosstool_top",
+ ],
+)
+
+def _smalltown_initramfs_impl(ctx):
+ """
+ Generate an lz4-compressed initramfs based on a label/file list.
+ """
+
+ # Generate config file for gen_init_cpio that describes the initramfs to build.
+ cpio_list_name = ctx.label.name + ".cpio_list"
+ cpio_list = ctx.actions.declare_file(cpio_list_name)
+
+ # Start out with some standard initramfs device files.
+ cpio_list_content = [
+ "dir /dev 0755 0 0",
+ "nod /dev/console 0600 0 0 c 5 1",
+ "nod /dev/null 0644 0 0 c 1 3",
+ "nod /dev/kmsg 0644 0 0 c 1 11",
+ "nod /dev/ptmx 0644 0 0 c 5 2",
+ ]
+
+ # Find all directories that need to be created.
+ directories_needed = []
+ for _, p in ctx.attr.files.items():
+ if not p.startswith("/"):
+ fail("file {} invalid: must begin with /".format(p))
+
+ # Get all intermediate directories on path to file
+ parts = p.split("/")[1:-1]
+ directories_needed.append(parts)
+
+ for _, p in ctx.attr.files_cc.items():
+ if not p.startswith("/"):
+ fail("file {} invalid: must begin with /".format(p))
+
+ # Get all intermediate directories on path to file
+ parts = p.split("/")[1:-1]
+ directories_needed.append(parts)
+
+ # Extend with extra directories defined by user.
+ for p in ctx.attr.extra_dirs:
+ if not p.startswith("/"):
+ fail("directory {} invalid: must begin with /".format(p))
+
+ parts = p.split("/")[1:]
+ directories_needed.append(parts)
+
+ directories = []
+ for parts in directories_needed:
+ # Turn directory parts [usr, local, bin] into successive subpaths [/usr, /usr/local, /usr/local/bin].
+ last = ""
+ for part in parts:
+ last += "/" + part
+
+ # TODO(q3k): this is slow - this should be a set instead, but starlark doesn't implement them.
+ # For the amount of files we're dealing with this doesn't matter, but all stars are pointing towards this
+ # becoming accidentally quadratic at some point in the future.
+ if last not in directories:
+ directories.append(last)
+
+ # Append instructions to create directories.
+ # Serendipitously, the directories should already be in the right order due to us not using a set to create the
+ # list. They might not be in an elegant order (ie, if files [/foo/one/one, /bar, /foo/two/two] are request, the
+ # order will be [/foo, /foo/one, /bar, /foo/two]), but that's fine.
+ for d in directories:
+ cpio_list_content.append("dir {} 0755 0 0".format(d))
+
+ # Append instructions to add files.
+ inputs = []
+ for label, p in ctx.attr.files.items():
+ # Figure out if this is an executable.
+ is_executable = True
+
+ di = label[DefaultInfo]
+ if di.files_to_run.executable == None:
+ # Generated non-executable files will have DefaultInfo.files_to_run.executable == None
+ is_executable = False
+ elif di.files_to_run.executable.is_source:
+ # Source files will have executable.is_source == True
+ is_executable = False
+
+ # Ensure only single output is declared.
+ # If you hit this error, figure out a better logic to find what file you need, maybe looking at providers other
+ # than DefaultInfo.
+ files = di.files.to_list()
+ if len(files) > 1:
+ fail("file {} has more than one output: {}", p, files)
+ src = files[0]
+ inputs.append(src)
+
+ mode = "0755" if is_executable else "0444"
+
+ cpio_list_content.append("file {} {} {} 0 0".format(p, src.path, mode))
+
+ for label, p in ctx.attr.files_cc.items():
+ # Figure out if this is an executable.
+ is_executable = True
+
+ di = label[DefaultInfo]
+ if di.files_to_run.executable == None:
+ # Generated non-executable files will have DefaultInfo.files_to_run.executable == None
+ is_executable = False
+ elif di.files_to_run.executable.is_source:
+ # Source files will have executable.is_source == True
+ is_executable = False
+
+ # Ensure only single output is declared.
+ # If you hit this error, figure out a better logic to find what file you need, maybe looking at providers other
+ # than DefaultInfo.
+ files = di.files.to_list()
+ if len(files) > 1:
+ fail("file {} has more than one output: {}", p, files)
+ src = files[0]
+ inputs.append(src)
+
+ mode = "0755" if is_executable else "0444"
+
+ cpio_list_content.append("file {} {} {} 0 0".format(p, src.path, mode))
+
+ # Write cpio_list.
+ ctx.actions.write(cpio_list, "\n".join(cpio_list_content))
+
+ gen_init_cpio = ctx.executable._gen_init_cpio
+ savestdout = ctx.executable._savestdout
+ lz4 = ctx.executable._lz4
+
+ # Generate 'raw' (uncompressed) initramfs
+ initramfs_raw_name = ctx.label.name
+ initramfs_raw = ctx.actions.declare_file(initramfs_raw_name)
+ ctx.actions.run(
+ outputs = [initramfs_raw],
+ inputs = [cpio_list] + inputs,
+ tools = [savestdout, gen_init_cpio],
+ executable = savestdout,
+ arguments = [initramfs_raw.path, gen_init_cpio.path, cpio_list.path],
+ )
+
+ # Compress raw initramfs using lz4c.
+ initramfs_name = ctx.label.name + ".lz4"
+ initramfs = ctx.actions.declare_file(initramfs_name)
+ ctx.actions.run(
+ outputs = [initramfs],
+ inputs = [initramfs_raw],
+ tools = [savestdout, lz4],
+ executable = lz4.path,
+ arguments = ["-l", initramfs_raw.path, initramfs.path],
+ )
+
+ return [DefaultInfo(files = depset([initramfs]))]
+
+smalltown_initramfs = rule(
+ implementation = _smalltown_initramfs_impl,
+ doc = """
+ Build a Smalltown initramfs. The initramfs will contain a basic /dev directory and all the files specified by the
+ `files` attribute. Executable files will have their permissions set to 0755, non-executable files will have
+ their permissions set to 0444. All parent directories will be created with 0755 permissions.
+ """,
+ attrs = {
+ "files": attr.label_keyed_string_dict(
+ mandatory = True,
+ allow_files = True,
+ doc = """
+ Dictionary of Labels to String, placing a given Label's output file in the initramfs at the location
+ specified by the String value. The specified labels must only have a single output.
+ """,
+ # Attach pure transition to ensure all binaries added to the initramfs are pure/static binaries.
+ cfg = build_pure_transition,
+ ),
+ "files_cc": attr.label_keyed_string_dict(
+ allow_files = True,
+ doc = """
+ Special case of 'files' for compilation targets that need to be built with the musl toolchain like
+ go_binary targets which need cgo or cc_binary targets.
+ """,
+ # Attach static transition to all files_cc inputs to ensure they are built with musl and static.
+ cfg = build_static_transition,
+ ),
+ "extra_dirs": attr.string_list(
+ default = [],
+ doc = """
+ Extra directories to create. These will be created in addition to all the directories required to
+ contain the files specified in the `files` attribute.
+ """,
+ ),
+
+ # Tools, implicit dependencies.
+ "_gen_init_cpio": attr.label(
+ default = Label("@linux//:gen_init_cpio"),
+ executable = True,
+ cfg = "host",
+ ),
+ "_lz4": attr.label(
+ default = Label("@com_github_lz4_lz4//programs:lz4"),
+ executable = True,
+ cfg = "host",
+ ),
+ "_savestdout": attr.label(
+ default = Label("//build/savestdout"),
+ executable = True,
+ cfg = "host",
+ ),
+
+ # Allow for transitions to be attached to this rule.
+ "_whitelist_function_transition": attr.label(
+ default = "@bazel_tools//tools/whitelists/function_transition_whitelist",
+ ),
+ },
+)
diff --git a/metropolis/node/build/genosrelease/BUILD.bazel b/metropolis/node/build/genosrelease/BUILD.bazel
new file mode 100644
index 0000000..9403d72
--- /dev/null
+++ b/metropolis/node/build/genosrelease/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["main.go"],
+ importpath = "git.monogon.dev/source/nexantic.git/metropolis/node/build/genosrelease",
+ visibility = ["//visibility:private"],
+ deps = ["@com_github_joho_godotenv//:go_default_library"],
+)
+
+go_binary(
+ name = "genosrelease",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
diff --git a/metropolis/node/build/genosrelease/defs.bzl b/metropolis/node/build/genosrelease/defs.bzl
new file mode 100644
index 0000000..61ce9e4
--- /dev/null
+++ b/metropolis/node/build/genosrelease/defs.bzl
@@ -0,0 +1,54 @@
+# Copyright 2020 The Monogon Project Authors.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def _os_release_impl(ctx):
+ ctx.actions.run(
+ mnemonic = "GenOSRelease",
+ progress_message = "Generating os-release",
+ inputs = [ctx.info_file],
+ outputs = [ctx.outputs.out],
+ executable = ctx.executable._genosrelease,
+ arguments = [
+ "-status_file",
+ ctx.info_file.path,
+ "-out_file",
+ ctx.outputs.out.path,
+ "-stamp_var",
+ ctx.attr.stamp_var,
+ "-name",
+ ctx.attr.os_name,
+ "-id",
+ ctx.attr.os_id,
+ ],
+ )
+
+os_release = rule(
+ implementation = _os_release_impl,
+ attrs = {
+ "os_name": attr.string(mandatory = True),
+ "os_id": attr.string(mandatory = True),
+ "stamp_var": attr.string(mandatory = True),
+ "_genosrelease": attr.label(
+ default = Label("//metropolis/node/build/genosrelease"),
+ cfg = "host",
+ executable = True,
+ allow_files = True,
+ ),
+ },
+ outputs = {
+ "out": "os-release",
+ },
+)
diff --git a/metropolis/node/build/genosrelease/main.go b/metropolis/node/build/genosrelease/main.go
new file mode 100644
index 0000000..2344f19
--- /dev/null
+++ b/metropolis/node/build/genosrelease/main.go
@@ -0,0 +1,78 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// genosrelease provides rudimentary support to generate os-release files following the freedesktop spec
+// (https://www.freedesktop.org/software/systemd/man/os-release.html) from arguments and stamping
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+
+ "github.com/joho/godotenv"
+)
+
+var (
+ flagStatusFile = flag.String("status_file", "", "path to bazel workspace status file")
+ flagOutFile = flag.String("out_file", "os-release", "path to os-release output file")
+ flagStampVar = flag.String("stamp_var", "", "variable to use as version from the workspace status file")
+ flagName = flag.String("name", "", "name parameter (see freedesktop spec)")
+ flagID = flag.String("id", "", "id parameter (see freedesktop spec)")
+)
+
+func main() {
+ flag.Parse()
+ statusFileContent, err := ioutil.ReadFile(*flagStatusFile)
+ if err != nil {
+ fmt.Printf("Failed to open bazel workspace status file: %v\n", err)
+ os.Exit(1)
+ }
+ statusVars := make(map[string]string)
+ for _, line := range strings.Split(string(statusFileContent), "\n") {
+ line = strings.TrimSpace(line)
+ parts := strings.Fields(line)
+ if len(parts) != 2 {
+ continue
+ }
+ statusVars[parts[0]] = parts[1]
+ }
+
+ smalltownVersion, ok := statusVars[*flagStampVar]
+ if !ok {
+ fmt.Printf("%v key not set in bazel workspace status file\n", *flagStampVar)
+ os.Exit(1)
+ }
+ // As specified by https://www.freedesktop.org/software/systemd/man/os-release.html
+ osReleaseVars := map[string]string{
+ "NAME": *flagName,
+ "ID": *flagID,
+ "VERSION": smalltownVersion,
+ "VERSION_ID": smalltownVersion,
+ "PRETTY_NAME": *flagName + " " + smalltownVersion,
+ }
+ osReleaseContent, err := godotenv.Marshal(osReleaseVars)
+ if err != nil {
+ fmt.Printf("Failed to encode os-release file: %v\n", err)
+ os.Exit(1)
+ }
+ if err := ioutil.WriteFile(*flagOutFile, []byte(osReleaseContent), 0644); err != nil {
+ fmt.Printf("Failed to write os-release file: %v\n", err)
+ os.Exit(1)
+ }
+}
diff --git a/metropolis/node/build/kconfig-patcher/BUILD.bazel b/metropolis/node/build/kconfig-patcher/BUILD.bazel
new file mode 100644
index 0000000..55b2b52
--- /dev/null
+++ b/metropolis/node/build/kconfig-patcher/BUILD.bazel
@@ -0,0 +1,20 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["main.go"],
+ importpath = "git.monogon.dev/source/nexantic.git/metropolis/node/build/kconfig-patcher",
+ visibility = ["//visibility:private"],
+)
+
+go_binary(
+ name = "kconfig-patcher",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
+
+go_test(
+ name = "go_default_test",
+ srcs = ["main_test.go"],
+ embed = [":go_default_library"],
+)
diff --git a/metropolis/node/build/kconfig-patcher/kconfig-patcher.bzl b/metropolis/node/build/kconfig-patcher/kconfig-patcher.bzl
new file mode 100644
index 0000000..337642e
--- /dev/null
+++ b/metropolis/node/build/kconfig-patcher/kconfig-patcher.bzl
@@ -0,0 +1,33 @@
+# Copyright 2020 The Monogon Project Authors.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Override configs in a Linux kernel Kconfig
+"""
+
+def kconfig_patch(name, src, out, override_configs, **kwargs):
+ native.genrule(
+ name = name,
+ srcs = [src],
+ outs = [out],
+ tools = [
+ "//metropolis/node/build/kconfig-patcher",
+ ],
+ cmd = """
+ $(location //metropolis/node/build/kconfig-patcher) \
+ -in $< -out $@ '%s'
+ """ % struct(overrides = override_configs).to_json(),
+ **kwargs
+ )
diff --git a/metropolis/node/build/kconfig-patcher/main.go b/metropolis/node/build/kconfig-patcher/main.go
new file mode 100644
index 0000000..27c33e9
--- /dev/null
+++ b/metropolis/node/build/kconfig-patcher/main.go
@@ -0,0 +1,95 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "bufio"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+)
+
+var (
+ inPath = flag.String("in", "", "Path to input Kconfig")
+ outPath = flag.String("out", "", "Path to output Kconfig")
+)
+
+func main() {
+ flag.Parse()
+ if *inPath == "" || *outPath == "" {
+ flag.PrintDefaults()
+ os.Exit(2)
+ }
+ inFile, err := os.Open(*inPath)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to open input Kconfig: %v\n", err)
+ os.Exit(1)
+ }
+ outFile, err := os.Create(*outPath)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to create output Kconfig: %v\n", err)
+ os.Exit(1)
+ }
+ var config struct {
+ Overrides map[string]string `json:"overrides"`
+ }
+ if err := json.Unmarshal([]byte(flag.Arg(0)), &config); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to parse overrides: %v\n", err)
+ os.Exit(1)
+ }
+ err = patchKconfig(inFile, outFile, config.Overrides)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to patch: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func patchKconfig(inFile io.Reader, outFile io.Writer, overrides map[string]string) error {
+ scanner := bufio.NewScanner(inFile)
+ for scanner.Scan() {
+ line := scanner.Text()
+ cleanLine := strings.TrimSpace(line)
+ if strings.HasPrefix(cleanLine, "#") || cleanLine == "" {
+ // Pass through comments and empty lines
+ fmt.Fprintln(outFile, line)
+ } else {
+ // Line contains a configuration option
+ parts := strings.SplitN(line, "=", 2)
+ keyName := parts[0]
+ if overrideVal, ok := overrides[strings.TrimSpace(keyName)]; ok {
+ // Override it
+ if overrideVal == "" {
+ fmt.Fprintf(outFile, "# %v is not set\n", keyName)
+ } else {
+ fmt.Fprintf(outFile, "%v=%v\n", keyName, overrideVal)
+ }
+ delete(overrides, keyName)
+ } else {
+ // Pass through unchanged
+ fmt.Fprintln(outFile, line)
+ }
+ }
+ }
+ // Process left over overrides
+ for key, val := range overrides {
+ fmt.Fprintf(outFile, "%v=%v\n", key, val)
+ }
+ return nil
+}
diff --git a/metropolis/node/build/kconfig-patcher/main_test.go b/metropolis/node/build/kconfig-patcher/main_test.go
new file mode 100644
index 0000000..11c7d84
--- /dev/null
+++ b/metropolis/node/build/kconfig-patcher/main_test.go
@@ -0,0 +1,61 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+)
+
+func Test_patchKconfig(t *testing.T) {
+ type args struct {
+ inFile string
+ overrides map[string]string
+ }
+ tests := []struct {
+ name string
+ args args
+ wantOutFile string
+ wantErr bool
+ }{
+ {
+ "passthroughExtend",
+ args{inFile: "# TEST=y\n\n", overrides: map[string]string{"TEST": "n"}},
+ "# TEST=y\n\nTEST=n\n",
+ false,
+ },
+ {
+ "patch",
+ args{inFile: "TEST=y\nTEST_NO=n\n", overrides: map[string]string{"TEST": "n"}},
+ "TEST=n\nTEST_NO=n\n",
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ outFile := &bytes.Buffer{}
+ if err := patchKconfig(strings.NewReader(tt.args.inFile), outFile, tt.args.overrides); (err != nil) != tt.wantErr {
+ t.Errorf("patchKconfig() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if gotOutFile := outFile.String(); gotOutFile != tt.wantOutFile {
+ t.Errorf("patchKconfig() = %v, want %v", gotOutFile, tt.wantOutFile)
+ }
+ })
+ }
+}
diff --git a/metropolis/node/build/mkimage/BUILD.bazel b/metropolis/node/build/mkimage/BUILD.bazel
new file mode 100644
index 0000000..b489002
--- /dev/null
+++ b/metropolis/node/build/mkimage/BUILD.bazel
@@ -0,0 +1,20 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["main.go"],
+ importpath = "git.monogon.dev/source/nexantic.git/metropolis/node/build/mkimage",
+ visibility = ["//visibility:private"],
+ deps = [
+ "@com_github_diskfs_go_diskfs//:go_default_library",
+ "@com_github_diskfs_go_diskfs//disk:go_default_library",
+ "@com_github_diskfs_go_diskfs//filesystem:go_default_library",
+ "@com_github_diskfs_go_diskfs//partition/gpt:go_default_library",
+ ],
+)
+
+go_binary(
+ name = "mkimage",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
diff --git a/metropolis/node/build/mkimage/main.go b/metropolis/node/build/mkimage/main.go
new file mode 100644
index 0000000..9f49f0a
--- /dev/null
+++ b/metropolis/node/build/mkimage/main.go
@@ -0,0 +1,141 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+// mkimage is a tool to generate a Smalltown disk image containing the given EFI payload, and optionally, a given external
+// initramfs image and enrolment credentials.
+
+import (
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+
+ diskfs "github.com/diskfs/go-diskfs"
+ "github.com/diskfs/go-diskfs/disk"
+ "github.com/diskfs/go-diskfs/filesystem"
+ "github.com/diskfs/go-diskfs/partition/gpt"
+)
+
+var SmalltownDataPartition gpt.Type = gpt.Type("9eeec464-6885-414a-b278-4305c51f7966")
+
+var (
+ flagEFI string
+ flagOut string
+ flagInitramfs string
+ flagEnrolmentCredentials string
+ flagDataPartitionSize uint64
+ flagESPPartitionSize uint64
+)
+
+func mibToSectors(size uint64) uint64 {
+ return (size * 1024 * 1024) / 512
+}
+
+func main() {
+ flag.StringVar(&flagEFI, "efi", "", "UEFI payload")
+ flag.StringVar(&flagOut, "out", "", "Output disk image")
+ flag.StringVar(&flagInitramfs, "initramfs", "", "External initramfs [optional]")
+ flag.StringVar(&flagEnrolmentCredentials, "enrolment_credentials", "", "Enrolment credentials [optional]")
+ flag.Uint64Var(&flagDataPartitionSize, "data_partition_size", 2048, "Override the data partition size (default 2048 MiB)")
+ flag.Uint64Var(&flagESPPartitionSize, "esp_partition_size", 512, "Override the ESP partition size (default: 512MiB)")
+ flag.Parse()
+
+ if flagEFI == "" || flagOut == "" {
+ log.Fatalf("efi and initramfs must be set")
+ }
+
+ _ = os.Remove(flagOut)
+ diskImg, err := diskfs.Create(flagOut, 3*1024*1024*1024, diskfs.Raw)
+ if err != nil {
+ log.Fatalf("diskfs.Create(%q): %v", flagOut, err)
+ }
+
+ table := &gpt.Table{
+ // This is appropriate at least for virtio disks. Might need to be adjusted for real ones.
+ LogicalSectorSize: 512,
+ PhysicalSectorSize: 512,
+ ProtectiveMBR: true,
+ Partitions: []*gpt.Partition{
+ {
+ Type: gpt.EFISystemPartition,
+ Name: "ESP",
+ Start: mibToSectors(1),
+ End: mibToSectors(flagESPPartitionSize) - 1,
+ },
+ {
+ Type: SmalltownDataPartition,
+ Name: "SIGNOS-DATA",
+ Start: mibToSectors(flagESPPartitionSize),
+ End: mibToSectors(flagESPPartitionSize+flagDataPartitionSize) - 1,
+ },
+ },
+ }
+ if err := diskImg.Partition(table); err != nil {
+ log.Fatalf("Failed to apply partition table: %v", err)
+ }
+
+ fs, err := diskImg.CreateFilesystem(disk.FilesystemSpec{Partition: 1, FSType: filesystem.TypeFat32, VolumeLabel: "ESP"})
+ if err != nil {
+ log.Fatalf("Failed to create filesystem: %v", err)
+ }
+
+ // Create EFI partition structure.
+ for _, dir := range []string{"/EFI", "/EFI/BOOT", "/EFI/smalltown"} {
+ if err := fs.Mkdir(dir); err != nil {
+ log.Fatalf("Mkdir(%q): %v", dir, err)
+ }
+ }
+
+ put(fs, flagEFI, "/EFI/BOOT/BOOTX64.EFI")
+
+ if flagInitramfs != "" {
+ put(fs, flagInitramfs, "/EFI/smalltown/initramfs.cpio.lz4")
+ }
+
+ if flagEnrolmentCredentials != "" {
+ put(fs, flagEnrolmentCredentials, "/EFI/smalltown/enrolment.pb")
+ }
+
+ if err := diskImg.File.Close(); err != nil {
+ log.Fatalf("Failed to finalize image: %v", err)
+ }
+ log.Printf("Success! You can now boot %v", flagOut)
+}
+
+// put copies a file from the host filesystem into the target image.
+func put(fs filesystem.FileSystem, src, dst string) {
+ target, err := fs.OpenFile(dst, os.O_CREATE|os.O_RDWR)
+ if err != nil {
+ log.Fatalf("fs.OpenFile(%q): %v", dst, err)
+ }
+ source, err := os.Open(src)
+ if err != nil {
+ log.Fatalf("os.Open(%q): %v", src, err)
+ }
+ defer source.Close()
+ // If this is streamed (e.g. using io.Copy) it exposes a bug in diskfs, so do it in one go.
+ data, err := ioutil.ReadAll(source)
+ if err != nil {
+ log.Fatalf("Reading %q: %v", src, err)
+ }
+ if _, err := target.Write(data); err != nil {
+ fmt.Printf("writing file %q: %v", dst, err)
+ os.Exit(1)
+ }
+}