blob: 1c9b39f4bcb500031e87166287ac1193c40c9c93 [file] [log] [blame]
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +01001package main
2
3import (
4 "flag"
5 "io"
6 "log"
7 "os"
8 "path"
9 "sort"
10 "strings"
11
12 "github.com/cavaliergopher/cpio"
13 "github.com/pierrec/lz4/v4"
14 "golang.org/x/sys/unix"
15
16 "source.monogon.dev/metropolis/node/build/fsspec"
17)
18
19var (
20 outPath = flag.String("out", "", "Output file path")
21)
22
23type placeEnum int
24
25const (
26 // placeNone implies that currently nothing is placed at that path.
27 // Can be overridden by everything.
28 placeNone placeEnum = 0
29 // placeDirImplicit means that there is currently a implied directory
30 // at the given path. It can be overridden by (and only by) an explicit
31 // directory.
32 placeDirImplicit placeEnum = 1
33 // placeDirExplicit means that there is an explicit (i.e. specified by
34 // the FSSpec) directory at the given path. Nothing else can override
35 // this.
36 placeDirExplicit placeEnum = 2
37 // placeNonDir means that there is a file-type resource (i.e a file, symlink
38 // or special_file) at the given path. Nothing else can override this.
39 placeNonDir placeEnum = 3
40)
41
42// place represents the state a given canonical path is in during metadata
43// construction. Its zero value is { State: placeNone, Inode: nil }.
44type place struct {
45 State placeEnum
46 // Inode contains one of the types inside an FSSpec (e.g. *fsspec.File)
47 Inode interface{}
48}
49
50// Usage: -out <out-path.cpio.lz4> fsspec-path...
51func main() {
52 flag.Parse()
53 outFile, err := os.Create(*outPath)
54 if err != nil {
55 log.Fatalf("Failed to open CPIO output file: %v", err)
56 }
57 defer outFile.Close()
58 compressedOut := lz4.NewWriter(outFile)
59 compressedOut.Apply(lz4.LegacyOption(true))
60 defer compressedOut.Close()
61 cpioWriter := cpio.NewWriter(compressedOut)
62 defer cpioWriter.Close()
63
64 spec, err := fsspec.ReadMergeSpecs(flag.Args())
65 if err != nil {
66 log.Fatalf("failed to load specs: %v", err)
67 }
68
69 // Map of paths to metadata for validation & implicit directory injection
70 places := make(map[string]place)
71
72 // The idea behind this machinery is that we try to place all files and
73 // directories into a map while creating the required parent directories
74 // on-the-fly as implicit directories. Overriding an implicit directory
75 // with an explicit one is allowed thus the actual order in which this
76 // structure is created does not matter. All non-directories cannot be
77 // overridden anyways so their insertion order does not matter.
78 // This also has the job of validating the FSSpec structure, ensuring that
79 // there are no duplicate paths and that there is nothing placed below a
80 // non-directory.
81 var placeInode func(p string, isDir bool, inode interface{})
82 placeInode = func(p string, isDir bool, inode interface{}) {
83 cleanPath := path.Clean(p)
84 if !isDir {
85 if places[cleanPath].State != placeNone {
86 log.Fatalf("Invalid FSSpec: Duplicate Inode at %q", cleanPath)
87 }
88 places[cleanPath] = place{
89 State: placeNonDir,
90 Inode: inode,
91 }
92 } else {
93 switch places[cleanPath].State {
94 case placeNone:
95 if inode != nil {
96 places[cleanPath] = place{
97 State: placeDirExplicit,
98 Inode: inode,
99 }
100 } else {
101 places[cleanPath] = place{
102 State: placeDirImplicit,
103 Inode: &fsspec.Directory{Path: cleanPath, Mode: 0555},
104 }
105 }
106 case placeDirImplicit:
107 if inode != nil {
108 places[cleanPath] = place{
109 State: placeDirExplicit,
110 Inode: inode,
111 }
112 }
113 case placeDirExplicit:
114 if inode != nil {
115 log.Fatalf("Invalid FSSpec: Conflicting explicit directories at %v", cleanPath)
116 }
117 case placeNonDir:
118 log.Fatalf("Invalid FSSpec: Trying to place inode below non-directory at #{cleanPath}")
119 default:
120 panic("unhandled placeEnum value")
121 }
122 }
123 parentPath, _ := path.Split(p)
124 parentPath = path.Clean(parentPath)
125 if parentPath == "/" || parentPath == p {
126 return
127 }
128 placeInode(parentPath, true, nil)
129 }
130 for _, d := range spec.Directory {
131 placeInode(d.Path, true, d)
132 }
133 for _, f := range spec.File {
134 placeInode(f.Path, false, f)
135 }
136 for _, s := range spec.SymbolicLink {
137 placeInode(s.Path, false, s)
138 }
139 for _, s := range spec.SpecialFile {
140 placeInode(s.Path, false, s)
141 }
142
143 var writeOrder []string
144 for path := range places {
145 writeOrder = append(writeOrder, path)
146 }
147 // Sorting a list of normalized paths representing a tree gives us Depth-
148 // first search (DFS) order which is the correct order for writing archives.
149 // This also makes the output reproducible.
150 sort.Strings(writeOrder)
151
152 for _, path := range writeOrder {
153 place := places[path]
154 switch i := place.Inode.(type) {
155 case *fsspec.File:
156 inF, err := os.Open(i.SourcePath)
157 if err != nil {
158 log.Fatalf("Failed to open source path for file %q: %v", i.Path, err)
159 }
160 inFStat, err := inF.Stat()
161 if err != nil {
162 log.Fatalf("Failed to stat source path for file %q: %v", i.Path, err)
163 }
164 if err := cpioWriter.WriteHeader(&cpio.Header{
165 Mode: cpio.FileMode(i.Mode),
166 Name: strings.TrimPrefix(i.Path, "/"),
167 Size: inFStat.Size(),
168 }); err != nil {
169 log.Fatalf("Failed to write cpio header for file %q: %v", i.Path, err)
170 }
171 if _, err := io.Copy(cpioWriter, inF); err != nil {
172 log.Fatalf("Failed to copy file %q into cpio: %v", i.SourcePath, err)
173 }
174 inF.Close()
175 case *fsspec.Directory:
176 if err := cpioWriter.WriteHeader(&cpio.Header{
Lorenz Brund13c1c62022-03-30 19:58:58 +0200177 Mode: cpio.FileMode(i.Mode) | cpio.TypeDir,
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100178 Name: strings.TrimPrefix(i.Path, "/"),
179 }); err != nil {
180 log.Fatalf("Failed to write cpio header for directory %q: %v", i.Path, err)
181 }
182 case *fsspec.SymbolicLink:
183 if err := cpioWriter.WriteHeader(&cpio.Header{
184 // Symlinks are 0777 by definition (from man 7 symlink on Linux)
Lorenz Brun51350b02023-04-18 00:34:53 +0200185 Mode: 0777 | cpio.TypeSymlink,
186 Name: strings.TrimPrefix(i.Path, "/"),
187 Size: int64(len(i.TargetPath)),
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100188 }); err != nil {
189 log.Fatalf("Failed to write cpio header for symlink %q: %v", i.Path, err)
190 }
Lorenz Brun51350b02023-04-18 00:34:53 +0200191 if _, err := cpioWriter.Write([]byte(i.TargetPath)); err != nil {
192 log.Fatalf("Failed to write cpio symlink %q: %v", i.Path, err)
193 }
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100194 case *fsspec.SpecialFile:
195 mode := cpio.FileMode(i.Mode)
196 switch i.Type {
197 case fsspec.SpecialFile_CHARACTER_DEV:
Lorenz Brund13c1c62022-03-30 19:58:58 +0200198 mode |= cpio.TypeChar
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100199 case fsspec.SpecialFile_BLOCK_DEV:
Lorenz Brund13c1c62022-03-30 19:58:58 +0200200 mode |= cpio.TypeBlock
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100201 case fsspec.SpecialFile_FIFO:
Lorenz Brund13c1c62022-03-30 19:58:58 +0200202 mode |= cpio.TypeFifo
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100203 }
204
205 if err := cpioWriter.WriteHeader(&cpio.Header{
206 Mode: mode,
207 Name: strings.TrimPrefix(i.Path, "/"),
208 DeviceID: int(unix.Mkdev(i.Major, i.Minor)),
209 }); err != nil {
210 log.Fatalf("Failed to write CPIO header for special file %q: %v", i.Path, err)
211 }
212 default:
213 panic("inode type not handled")
214 }
215 }
216}