m/n/build: implement new fsspec infrastructure
This makes the node_initramfs and erofs_image use the new common fsspec
infrastructure. It also adds the fsspecs attribute to both which can
later be used to add arbitrary fsspecs.
Change-Id: I384e04712c0a70f82c5c975911cbb1d0d5e6cabc
Reviewed-on: https://review.monogon.dev/c/monogon/+/530
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/build/mkcpio/main.go b/metropolis/node/build/mkcpio/main.go
new file mode 100644
index 0000000..10deb5a
--- /dev/null
+++ b/metropolis/node/build/mkcpio/main.go
@@ -0,0 +1,213 @@
+package main
+
+import (
+ "flag"
+ "io"
+ "log"
+ "os"
+ "path"
+ "sort"
+ "strings"
+
+ "github.com/cavaliergopher/cpio"
+ "github.com/pierrec/lz4/v4"
+ "golang.org/x/sys/unix"
+
+ "source.monogon.dev/metropolis/node/build/fsspec"
+)
+
+var (
+ outPath = flag.String("out", "", "Output file path")
+)
+
+type placeEnum int
+
+const (
+ // placeNone implies that currently nothing is placed at that path.
+ // Can be overridden by everything.
+ placeNone placeEnum = 0
+ // placeDirImplicit means that there is currently a implied directory
+ // at the given path. It can be overridden by (and only by) an explicit
+ // directory.
+ placeDirImplicit placeEnum = 1
+ // placeDirExplicit means that there is an explicit (i.e. specified by
+ // the FSSpec) directory at the given path. Nothing else can override
+ // this.
+ placeDirExplicit placeEnum = 2
+ // placeNonDir means that there is a file-type resource (i.e a file, symlink
+ // or special_file) at the given path. Nothing else can override this.
+ placeNonDir placeEnum = 3
+)
+
+// place represents the state a given canonical path is in during metadata
+// construction. Its zero value is { State: placeNone, Inode: nil }.
+type place struct {
+ State placeEnum
+ // Inode contains one of the types inside an FSSpec (e.g. *fsspec.File)
+ Inode interface{}
+}
+
+// Usage: -out <out-path.cpio.lz4> fsspec-path...
+func main() {
+ flag.Parse()
+ outFile, err := os.Create(*outPath)
+ if err != nil {
+ log.Fatalf("Failed to open CPIO output file: %v", err)
+ }
+ defer outFile.Close()
+ compressedOut := lz4.NewWriter(outFile)
+ compressedOut.Apply(lz4.LegacyOption(true))
+ defer compressedOut.Close()
+ cpioWriter := cpio.NewWriter(compressedOut)
+ defer cpioWriter.Close()
+
+ spec, err := fsspec.ReadMergeSpecs(flag.Args())
+ if err != nil {
+ log.Fatalf("failed to load specs: %v", err)
+ }
+
+ // Map of paths to metadata for validation & implicit directory injection
+ places := make(map[string]place)
+
+ // The idea behind this machinery is that we try to place all files and
+ // directories into a map while creating the required parent directories
+ // on-the-fly as implicit directories. Overriding an implicit directory
+ // with an explicit one is allowed thus the actual order in which this
+ // structure is created does not matter. All non-directories cannot be
+ // overridden anyways so their insertion order does not matter.
+ // This also has the job of validating the FSSpec structure, ensuring that
+ // there are no duplicate paths and that there is nothing placed below a
+ // non-directory.
+ var placeInode func(p string, isDir bool, inode interface{})
+ placeInode = func(p string, isDir bool, inode interface{}) {
+ cleanPath := path.Clean(p)
+ if !isDir {
+ if places[cleanPath].State != placeNone {
+ log.Fatalf("Invalid FSSpec: Duplicate Inode at %q", cleanPath)
+ }
+ places[cleanPath] = place{
+ State: placeNonDir,
+ Inode: inode,
+ }
+ } else {
+ switch places[cleanPath].State {
+ case placeNone:
+ if inode != nil {
+ places[cleanPath] = place{
+ State: placeDirExplicit,
+ Inode: inode,
+ }
+ } else {
+ places[cleanPath] = place{
+ State: placeDirImplicit,
+ Inode: &fsspec.Directory{Path: cleanPath, Mode: 0555},
+ }
+ }
+ case placeDirImplicit:
+ if inode != nil {
+ places[cleanPath] = place{
+ State: placeDirExplicit,
+ Inode: inode,
+ }
+ }
+ case placeDirExplicit:
+ if inode != nil {
+ log.Fatalf("Invalid FSSpec: Conflicting explicit directories at %v", cleanPath)
+ }
+ case placeNonDir:
+ log.Fatalf("Invalid FSSpec: Trying to place inode below non-directory at #{cleanPath}")
+ default:
+ panic("unhandled placeEnum value")
+ }
+ }
+ parentPath, _ := path.Split(p)
+ parentPath = path.Clean(parentPath)
+ if parentPath == "/" || parentPath == p {
+ return
+ }
+ placeInode(parentPath, true, nil)
+ }
+ for _, d := range spec.Directory {
+ placeInode(d.Path, true, d)
+ }
+ for _, f := range spec.File {
+ placeInode(f.Path, false, f)
+ }
+ for _, s := range spec.SymbolicLink {
+ placeInode(s.Path, false, s)
+ }
+ for _, s := range spec.SpecialFile {
+ placeInode(s.Path, false, s)
+ }
+
+ var writeOrder []string
+ for path := range places {
+ writeOrder = append(writeOrder, path)
+ }
+ // Sorting a list of normalized paths representing a tree gives us Depth-
+ // first search (DFS) order which is the correct order for writing archives.
+ // This also makes the output reproducible.
+ sort.Strings(writeOrder)
+
+ for _, path := range writeOrder {
+ place := places[path]
+ switch i := place.Inode.(type) {
+ case *fsspec.File:
+ inF, err := os.Open(i.SourcePath)
+ if err != nil {
+ log.Fatalf("Failed to open source path for file %q: %v", i.Path, err)
+ }
+ inFStat, err := inF.Stat()
+ if err != nil {
+ log.Fatalf("Failed to stat source path for file %q: %v", i.Path, err)
+ }
+ if err := cpioWriter.WriteHeader(&cpio.Header{
+ Mode: cpio.FileMode(i.Mode),
+ Name: strings.TrimPrefix(i.Path, "/"),
+ Size: inFStat.Size(),
+ }); err != nil {
+ log.Fatalf("Failed to write cpio header for file %q: %v", i.Path, err)
+ }
+ if _, err := io.Copy(cpioWriter, inF); err != nil {
+ log.Fatalf("Failed to copy file %q into cpio: %v", i.SourcePath, err)
+ }
+ inF.Close()
+ case *fsspec.Directory:
+ if err := cpioWriter.WriteHeader(&cpio.Header{
+ Mode: cpio.FileMode(i.Mode) | cpio.ModeDir,
+ Name: strings.TrimPrefix(i.Path, "/"),
+ }); err != nil {
+ log.Fatalf("Failed to write cpio header for directory %q: %v", i.Path, err)
+ }
+ case *fsspec.SymbolicLink:
+ if err := cpioWriter.WriteHeader(&cpio.Header{
+ // Symlinks are 0777 by definition (from man 7 symlink on Linux)
+ Mode: 0777 | cpio.ModeSymlink,
+ Name: strings.TrimPrefix(i.Path, "/"),
+ Linkname: i.TargetPath,
+ }); err != nil {
+ log.Fatalf("Failed to write cpio header for symlink %q: %v", i.Path, err)
+ }
+ case *fsspec.SpecialFile:
+ mode := cpio.FileMode(i.Mode)
+ switch i.Type {
+ case fsspec.SpecialFile_CHARACTER_DEV:
+ mode |= cpio.ModeCharDevice
+ case fsspec.SpecialFile_BLOCK_DEV:
+ mode |= cpio.ModeDevice
+ case fsspec.SpecialFile_FIFO:
+ mode |= cpio.ModeNamedPipe
+ }
+
+ if err := cpioWriter.WriteHeader(&cpio.Header{
+ Mode: mode,
+ Name: strings.TrimPrefix(i.Path, "/"),
+ DeviceID: int(unix.Mkdev(i.Major, i.Minor)),
+ }); err != nil {
+ log.Fatalf("Failed to write CPIO header for special file %q: %v", i.Path, err)
+ }
+ default:
+ panic("inode type not handled")
+ }
+ }
+}