|  | // 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. | 
|  |  | 
|  | // mkerofs takes a specification in the form of a prototext file (see fsspec | 
|  | // next to this) and assembles an EROFS filesystem according to it. The output | 
|  | // is fully reproducible. | 
|  | package main | 
|  |  | 
|  | import ( | 
|  | "flag" | 
|  | "fmt" | 
|  | "io" | 
|  | "log" | 
|  | "os" | 
|  | "path" | 
|  | "sort" | 
|  | "strings" | 
|  |  | 
|  | "source.monogon.dev/metropolis/node/build/fsspec" | 
|  | "source.monogon.dev/metropolis/pkg/erofs" | 
|  | ) | 
|  |  | 
|  | func (spec *entrySpec) writeRecursive(w *erofs.Writer, pathname string) { | 
|  | switch inode := spec.data.Type.(type) { | 
|  | case *fsspec.Inode_Directory: | 
|  | // Sort children for reproducibility | 
|  | var sortedChildren []string | 
|  | for name := range spec.children { | 
|  | sortedChildren = append(sortedChildren, name) | 
|  | } | 
|  | sort.Strings(sortedChildren) | 
|  |  | 
|  | err := w.Create(pathname, &erofs.Directory{ | 
|  | Base: erofs.Base{ | 
|  | Permissions: uint16(inode.Directory.Mode), | 
|  | UID:         uint16(inode.Directory.Uid), | 
|  | GID:         uint16(inode.Directory.Gid), | 
|  | }, | 
|  | Children: sortedChildren, | 
|  | }) | 
|  | if err != nil { | 
|  | log.Fatalf("failed to write directory: %s", err) | 
|  | } | 
|  | for _, name := range sortedChildren { | 
|  | spec.children[name].writeRecursive(w, path.Join(pathname, name)) | 
|  | } | 
|  | case *fsspec.Inode_File: | 
|  | iw := w.CreateFile(pathname, &erofs.FileMeta{ | 
|  | Base: erofs.Base{ | 
|  | Permissions: uint16(inode.File.Mode), | 
|  | UID:         uint16(inode.File.Uid), | 
|  | GID:         uint16(inode.File.Gid), | 
|  | }, | 
|  | }) | 
|  |  | 
|  | sourceFile, err := os.Open(inode.File.SourcePath) | 
|  | if err != nil { | 
|  | log.Fatalf("failed to open source file %s: %s", inode.File.SourcePath, err) | 
|  | } | 
|  |  | 
|  | _, err = io.Copy(iw, sourceFile) | 
|  | if err != nil { | 
|  | log.Fatalf("failed to copy file into filesystem: %s", err) | 
|  | } | 
|  | sourceFile.Close() | 
|  | if err := iw.Close(); err != nil { | 
|  | log.Fatalf("failed to close target file: %s", err) | 
|  | } | 
|  | case *fsspec.Inode_SymbolicLink: | 
|  | err := w.Create(pathname, &erofs.SymbolicLink{ | 
|  | Base: erofs.Base{ | 
|  | Permissions: 0777, // Nominal, Linux forces that mode anyways, see symlink(7) | 
|  | }, | 
|  | Target: inode.SymbolicLink.TargetPath, | 
|  | }) | 
|  | if err != nil { | 
|  | log.Fatalf("failed to create symbolic link: %s", err) | 
|  | } | 
|  | case *fsspec.Inode_SpecialFile: | 
|  | err := fmt.Errorf("unimplemented special file type %s", inode.SpecialFile.Type) | 
|  | base := erofs.Base{ | 
|  | Permissions: uint16(inode.SpecialFile.Mode), | 
|  | UID:         uint16(inode.SpecialFile.Uid), | 
|  | GID:         uint16(inode.SpecialFile.Gid), | 
|  | } | 
|  | switch inode.SpecialFile.Type { | 
|  | case fsspec.SpecialFile_FIFO: | 
|  | err = w.Create(pathname, &erofs.FIFO{ | 
|  | Base: base, | 
|  | }) | 
|  | case fsspec.SpecialFile_CHARACTER_DEV: | 
|  | err = w.Create(pathname, &erofs.CharacterDevice{ | 
|  | Base:  base, | 
|  | Major: inode.SpecialFile.Major, | 
|  | Minor: inode.SpecialFile.Minor, | 
|  | }) | 
|  | case fsspec.SpecialFile_BLOCK_DEV: | 
|  | err = w.Create(pathname, &erofs.BlockDevice{ | 
|  | Base:  base, | 
|  | Major: inode.SpecialFile.Major, | 
|  | Minor: inode.SpecialFile.Minor, | 
|  | }) | 
|  | } | 
|  | if err != nil { | 
|  | log.Fatalf("failed to make special file: %v", err) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // entrySpec is a recursive structure representing the filesystem tree | 
|  | type entrySpec struct { | 
|  | data     fsspec.Inode | 
|  | children map[string]*entrySpec | 
|  | } | 
|  |  | 
|  | // pathRef gets the entrySpec at the leaf of the given path, inferring | 
|  | // directories if necessary | 
|  | func (s *entrySpec) pathRef(p string) *entrySpec { | 
|  | // This block gets a path array starting at the root of the filesystem. The | 
|  | // root folder is the zero-length array. | 
|  | pathParts := strings.Split(path.Clean("./"+p), "/") | 
|  | if pathParts[0] == "." { | 
|  | pathParts = pathParts[1:] | 
|  | } | 
|  |  | 
|  | entryRef := s | 
|  | for _, part := range pathParts { | 
|  | childRef, ok := entryRef.children[part] | 
|  | if !ok { | 
|  | childRef = &entrySpec{ | 
|  | data:     fsspec.Inode{Type: &fsspec.Inode_Directory{Directory: &fsspec.Directory{Mode: 0555}}}, | 
|  | children: make(map[string]*entrySpec), | 
|  | } | 
|  | entryRef.children[part] = childRef | 
|  | } | 
|  | entryRef = childRef | 
|  | } | 
|  | return entryRef | 
|  | } | 
|  |  | 
|  | var ( | 
|  | outPath = flag.String("out", "", "Output file path") | 
|  | ) | 
|  |  | 
|  | func main() { | 
|  | flag.Parse() | 
|  |  | 
|  | spec, err := fsspec.ReadMergeSpecs(flag.Args()) | 
|  | if err != nil { | 
|  | log.Fatalf("failed to load specs: %v", err) | 
|  | } | 
|  |  | 
|  | var fsRoot = &entrySpec{ | 
|  | data:     fsspec.Inode{Type: &fsspec.Inode_Directory{Directory: &fsspec.Directory{Mode: 0555}}}, | 
|  | children: make(map[string]*entrySpec), | 
|  | } | 
|  |  | 
|  | for _, dir := range spec.Directory { | 
|  | entryRef := fsRoot.pathRef(dir.Path) | 
|  | entryRef.data.Type = &fsspec.Inode_Directory{Directory: dir} | 
|  | } | 
|  |  | 
|  | for _, file := range spec.File { | 
|  | entryRef := fsRoot.pathRef(file.Path) | 
|  | entryRef.data.Type = &fsspec.Inode_File{File: file} | 
|  | } | 
|  |  | 
|  | for _, symlink := range spec.SymbolicLink { | 
|  | entryRef := fsRoot.pathRef(symlink.Path) | 
|  | entryRef.data.Type = &fsspec.Inode_SymbolicLink{SymbolicLink: symlink} | 
|  | } | 
|  |  | 
|  | for _, specialFile := range spec.SpecialFile { | 
|  | entryRef := fsRoot.pathRef(specialFile.Path) | 
|  | entryRef.data.Type = &fsspec.Inode_SpecialFile{SpecialFile: specialFile} | 
|  | } | 
|  |  | 
|  | fs, err := os.Create(*outPath) | 
|  | if err != nil { | 
|  | log.Fatalf("failed to open output file: %v", err) | 
|  | } | 
|  | writer, err := erofs.NewWriter(fs) | 
|  | if err != nil { | 
|  | log.Fatalf("failed to initialize EROFS writer: %v", err) | 
|  | } | 
|  |  | 
|  | fsRoot.writeRecursive(writer, ".") | 
|  |  | 
|  | if err := writer.Close(); err != nil { | 
|  | panic(err) | 
|  | } | 
|  | if err := fs.Close(); err != nil { | 
|  | panic(err) | 
|  | } | 
|  | } |