m/n/b/mkpayload: init

mkpayload is an objcopy wrapper meant to perform actions that neither
the buildsystem or objcopy could perform by themselves. This is needed
by the upcoming dm-verity rootfs integration.

Change-Id: I8ad097a1ad26bec0fb2db4f8b14e75a1b038f8fb
Reviewed-on: https://review.monogon.dev/c/monogon/+/524
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/build/mkpayload/BUILD.bazel b/metropolis/node/build/mkpayload/BUILD.bazel
new file mode 100644
index 0000000..7716e60
--- /dev/null
+++ b/metropolis/node/build/mkpayload/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_binary(
+    name = "mkpayload",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = ["mkpayload.go"],
+    importpath = "source.monogon.dev/metropolis/node/build/mkpayload",
+    visibility = ["//visibility:private"],
+)
diff --git a/metropolis/node/build/mkpayload/mkpayload.go b/metropolis/node/build/mkpayload/mkpayload.go
new file mode 100644
index 0000000..42ec062
--- /dev/null
+++ b/metropolis/node/build/mkpayload/mkpayload.go
@@ -0,0 +1,142 @@
+// 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.
+
+// mkpayload is an objcopy wrapper that builds EFI unified kernel images. It
+// performs actions that can't be realized by either objcopy or the
+// buildsystem.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"strings"
+)
+
+var (
+	// sections contains VMAs and source files of the payload PE sections. The
+	// file path pointers will be filled in when the flags are parsed. It's used
+	// to generate objcopy command line arguments. Entries that are "required"
+	// will cause the program to stop and print usage information if not provided
+	// as command line parameters.
+	sections = map[string]struct {
+		descr    string
+		vma      string
+		required bool
+		file     *string
+	}{
+		"linux":   {"Linux kernel image", "0x2000000", true, nil},
+		"initrd":  {"initramfs", "0x5000000", false, nil},
+		"osrel":   {"OS release file in text format", "0x20000", false, nil},
+		"cmdline": {"a file containting additional kernel command line parameters", "0x30000", false, nil},
+		"splash":  {"a splash screen image in BMP format", "0x40000", false, nil},
+	}
+	objcopy         = flag.String("objcopy", "", "objcopy executable")
+	stub            = flag.String("stub", "", "the EFI stub executable")
+	output          = flag.String("output", "", "objcopy output")
+	rootfs_dm_table = flag.String("rootfs_dm_table", "", "a text file containing the DeviceMapper rootfs target table")
+)
+
+func main() {
+	// Register parameters related to the EFI payload sections, then parse the flags.
+	for k, v := range sections {
+		v.file = flag.String(k, "", v.descr)
+		sections[k] = v
+	}
+	flag.Parse()
+
+	// Ensure all the required parameters are filled in.
+	for n, s := range sections {
+		if s.required && *s.file == "" {
+			log.Fatalf("-%s parameter is missing.", n)
+		}
+	}
+	if *objcopy == "" {
+		log.Fatalf("-objcopy parameter is missing.")
+	}
+	if *stub == "" {
+		log.Fatalf("-stub parameter is missing.")
+	}
+	if *output == "" {
+		log.Fatalf("-output parameter is missing.")
+	}
+
+	// If a DeviceMapper table was passed, configure the kernel to boot from the
+	// device described by it, while keeping any other kernel command line
+	// parameters that might have been passed through "-cmdline".
+	if *rootfs_dm_table != "" {
+		var cmdline string
+		p := *sections["cmdline"].file
+		if p != "" {
+			c, err := os.ReadFile(p)
+			if err != nil {
+				log.Fatalf("%v", err)
+			}
+			cmdline = string(c[:])
+
+			if strings.Contains(cmdline, "root=") {
+				log.Fatalf("A DeviceMapper table was passed, however the kernel command line already contains a \"root=\" statement.")
+			}
+		}
+
+		vt, err := os.ReadFile(*rootfs_dm_table)
+		if err != nil {
+			log.Fatalf("%v", err)
+		}
+		cmdline += fmt.Sprintf(" dm-mod.create=\"rootfs,,,ro,%s\" root=/dev/dm-0", vt)
+
+		out, err := os.CreateTemp(".", "cmdline")
+		if err != nil {
+			log.Fatalf("%v", err)
+		}
+		defer os.Remove(out.Name())
+		if _, err = out.Write([]byte(cmdline[:])); err != nil {
+			log.Fatalf("%v", err)
+		}
+		out.Close()
+
+		*sections["cmdline"].file = out.Name()
+	}
+
+	// Execute objcopy
+	var args []string
+	for name, c := range sections {
+		if *c.file != "" {
+			args = append(args, []string{
+				"--add-section", fmt.Sprintf(".%s=%s", name, *c.file),
+				"--change-section-vma", fmt.Sprintf(".%s=%s", name, c.vma),
+			}...)
+		}
+	}
+	args = append(args, []string{
+		*stub,
+		*output,
+	}...)
+	cmd := exec.Command(*objcopy, args...)
+	cmd.Stderr = os.Stderr
+	cmd.Stdout = os.Stdout
+	err := cmd.Run()
+	if err == nil {
+		return
+	}
+	// Exit with objcopy's return code.
+	if e, ok := err.(*exec.ExitError); ok {
+		os.Exit(e.ExitCode())
+	}
+	log.Fatalf("Could not start command: %v", err)
+}