treewide: move build helper to more fitting places

Change-Id: I3d0cfe9283222d403ae369ec9db09201ad511e15
Reviewed-on: https://review.monogon.dev/c/monogon/+/3327
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/osbase/build/fsspec/BUILD.bazel b/osbase/build/fsspec/BUILD.bazel
new file mode 100644
index 0000000..b719522
--- /dev/null
+++ b/osbase/build/fsspec/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+    name = "spec_proto",
+    srcs = ["spec.proto"],
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "fsspec",
+    srcs = ["utils.go"],
+    embed = [":fsspec_go_proto"],
+    importpath = "source.monogon.dev/osbase/build/fsspec",
+    visibility = ["//visibility:public"],
+    deps = ["@org_golang_google_protobuf//encoding/prototext"],
+)
+
+go_proto_library(
+    name = "fsspec_go_proto",
+    importpath = "source.monogon.dev/osbase/build/fsspec",
+    proto = ":spec_proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/osbase/build/fsspec/spec.proto b/osbase/build/fsspec/spec.proto
new file mode 100644
index 0000000..487eebf
--- /dev/null
+++ b/osbase/build/fsspec/spec.proto
@@ -0,0 +1,98 @@
+// 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.
+
+syntax = "proto3";
+
+package osbase.node.build.fsspec;
+option go_package = "source.monogon.dev/osbase/build/fsspec";
+
+// FSSpec is the spec from which a filesystem is generated. It consists of files, directories and symbolic
+// links. Directories are also automatically inferred when required for the placement of files or symbolic
+// links. Inferred directories always have uid 0, gid 0 and permissions 0555. This can be overridden by
+// explicitly specifying a directory at a given path.
+message FSSpec {
+  repeated File file = 1;
+  repeated Directory directory = 2;
+  repeated SymbolicLink symbolic_link = 3;
+  repeated SpecialFile special_file = 4;
+}
+
+// For internal use only. Represents all supported inodes in a oneof.
+message Inode {
+  oneof type {
+    File file = 1;
+    Directory directory = 2;
+    SymbolicLink symbolic_link = 3;
+    SpecialFile special_file = 4;
+  }
+}
+
+message File {
+  // The path where the file ends up in the filesystem.
+  string path = 1;
+  // The path on the host filesystem where the file contents should be taken from.
+  string source_path = 2;
+  // Unix permission bits
+  uint32 mode = 3;
+  // Owner uid
+  uint32 uid = 4;
+  // Owner gid
+  uint32 gid = 5;
+}
+
+message Directory {
+  // The path where the directory ends up in the filesystem.
+  string path = 1;
+  // Unix permission bits
+  uint32 mode = 2;
+  // Owner uid
+  uint32 uid = 3;
+  // Owner gid
+  uint32 gid = 4;
+}
+
+message SymbolicLink {
+  // The path where the symbolic link ends up in the filesystem.
+  string path = 1;
+  // The path to which the symbolic link resolves to.
+  string target_path = 2;
+}
+
+message SpecialFile {
+  // The path where the special file ends up in the filesystem.
+  string path = 1;
+
+  enum Type {
+    CHARACTER_DEV = 0;
+    BLOCK_DEV = 1;
+    FIFO = 2;
+  }
+
+  // Type of special file.
+  Type type = 2;
+
+  // The major device number of the special file.
+  uint32 major = 3;
+  // The minor number of the special file. Ignored for FIFO-type special files.
+  uint32 minor = 4;
+
+  // Unix permission bits
+  uint32 mode = 5;
+  // Owner uid
+  uint32 uid = 6;
+  // Owner gid
+  uint32 gid = 7;
+}
\ No newline at end of file
diff --git a/osbase/build/fsspec/utils.go b/osbase/build/fsspec/utils.go
new file mode 100644
index 0000000..f5a45e3
--- /dev/null
+++ b/osbase/build/fsspec/utils.go
@@ -0,0 +1,30 @@
+package fsspec
+
+import (
+	"fmt"
+	"os"
+
+	"google.golang.org/protobuf/encoding/prototext"
+)
+
+// ReadMergeSpecs reads FSSpecs from all files in paths and merges them into
+// a single FSSpec.
+func ReadMergeSpecs(paths []string) (*FSSpec, error) {
+	var mergedSpec FSSpec
+	for _, p := range paths {
+		specRaw, err := os.ReadFile(p)
+		if err != nil {
+			return nil, fmt.Errorf("failed to open spec: %w", err)
+		}
+
+		var spec FSSpec
+		if err := prototext.Unmarshal(specRaw, &spec); err != nil {
+			return nil, fmt.Errorf("failed to parse spec %q: %w", p, err)
+		}
+		mergedSpec.File = append(mergedSpec.File, spec.File...)
+		mergedSpec.Directory = append(mergedSpec.Directory, spec.Directory...)
+		mergedSpec.SymbolicLink = append(mergedSpec.SymbolicLink, spec.SymbolicLink...)
+		mergedSpec.SpecialFile = append(mergedSpec.SpecialFile, spec.SpecialFile...)
+	}
+	return &mergedSpec, nil
+}