blob: b8f99b9e75c306a8537e6df6c049e679e140e73b [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"
Lorenz Brun62f1d362023-11-14 16:18:24 +010013 "github.com/klauspost/compress/zstd"
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +010014 "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
Lorenz Brun62f1d362023-11-14 16:18:24 +010050// Usage: -out <out-path.cpio.zst> fsspec-path...
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +010051func 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()
Lorenz Brun62f1d362023-11-14 16:18:24 +010058 compressedOut, err := zstd.NewWriter(outFile)
59 if err != nil {
60 log.Fatalf("While initializing zstd writer: %v", err)
61 }
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +010062 defer compressedOut.Close()
63 cpioWriter := cpio.NewWriter(compressedOut)
64 defer cpioWriter.Close()
65
66 spec, err := fsspec.ReadMergeSpecs(flag.Args())
67 if err != nil {
68 log.Fatalf("failed to load specs: %v", err)
69 }
70
71 // Map of paths to metadata for validation & implicit directory injection
72 places := make(map[string]place)
73
74 // The idea behind this machinery is that we try to place all files and
75 // directories into a map while creating the required parent directories
76 // on-the-fly as implicit directories. Overriding an implicit directory
77 // with an explicit one is allowed thus the actual order in which this
78 // structure is created does not matter. All non-directories cannot be
79 // overridden anyways so their insertion order does not matter.
80 // This also has the job of validating the FSSpec structure, ensuring that
81 // there are no duplicate paths and that there is nothing placed below a
82 // non-directory.
83 var placeInode func(p string, isDir bool, inode interface{})
84 placeInode = func(p string, isDir bool, inode interface{}) {
85 cleanPath := path.Clean(p)
86 if !isDir {
87 if places[cleanPath].State != placeNone {
88 log.Fatalf("Invalid FSSpec: Duplicate Inode at %q", cleanPath)
89 }
90 places[cleanPath] = place{
91 State: placeNonDir,
92 Inode: inode,
93 }
94 } else {
95 switch places[cleanPath].State {
96 case placeNone:
97 if inode != nil {
98 places[cleanPath] = place{
99 State: placeDirExplicit,
100 Inode: inode,
101 }
102 } else {
103 places[cleanPath] = place{
104 State: placeDirImplicit,
105 Inode: &fsspec.Directory{Path: cleanPath, Mode: 0555},
106 }
107 }
108 case placeDirImplicit:
109 if inode != nil {
110 places[cleanPath] = place{
111 State: placeDirExplicit,
112 Inode: inode,
113 }
114 }
115 case placeDirExplicit:
116 if inode != nil {
117 log.Fatalf("Invalid FSSpec: Conflicting explicit directories at %v", cleanPath)
118 }
119 case placeNonDir:
120 log.Fatalf("Invalid FSSpec: Trying to place inode below non-directory at #{cleanPath}")
121 default:
122 panic("unhandled placeEnum value")
123 }
124 }
125 parentPath, _ := path.Split(p)
126 parentPath = path.Clean(parentPath)
127 if parentPath == "/" || parentPath == p {
128 return
129 }
130 placeInode(parentPath, true, nil)
131 }
132 for _, d := range spec.Directory {
133 placeInode(d.Path, true, d)
134 }
135 for _, f := range spec.File {
136 placeInode(f.Path, false, f)
137 }
138 for _, s := range spec.SymbolicLink {
139 placeInode(s.Path, false, s)
140 }
141 for _, s := range spec.SpecialFile {
142 placeInode(s.Path, false, s)
143 }
144
145 var writeOrder []string
146 for path := range places {
147 writeOrder = append(writeOrder, path)
148 }
149 // Sorting a list of normalized paths representing a tree gives us Depth-
150 // first search (DFS) order which is the correct order for writing archives.
151 // This also makes the output reproducible.
152 sort.Strings(writeOrder)
153
154 for _, path := range writeOrder {
155 place := places[path]
156 switch i := place.Inode.(type) {
157 case *fsspec.File:
158 inF, err := os.Open(i.SourcePath)
159 if err != nil {
160 log.Fatalf("Failed to open source path for file %q: %v", i.Path, err)
161 }
162 inFStat, err := inF.Stat()
163 if err != nil {
164 log.Fatalf("Failed to stat source path for file %q: %v", i.Path, err)
165 }
166 if err := cpioWriter.WriteHeader(&cpio.Header{
167 Mode: cpio.FileMode(i.Mode),
168 Name: strings.TrimPrefix(i.Path, "/"),
169 Size: inFStat.Size(),
170 }); err != nil {
171 log.Fatalf("Failed to write cpio header for file %q: %v", i.Path, err)
172 }
Lorenz Brun62f1d362023-11-14 16:18:24 +0100173 if n, err := io.Copy(cpioWriter, inF); err != nil || n != inFStat.Size() {
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100174 log.Fatalf("Failed to copy file %q into cpio: %v", i.SourcePath, err)
175 }
176 inF.Close()
177 case *fsspec.Directory:
178 if err := cpioWriter.WriteHeader(&cpio.Header{
Lorenz Brund13c1c62022-03-30 19:58:58 +0200179 Mode: cpio.FileMode(i.Mode) | cpio.TypeDir,
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100180 Name: strings.TrimPrefix(i.Path, "/"),
181 }); err != nil {
182 log.Fatalf("Failed to write cpio header for directory %q: %v", i.Path, err)
183 }
184 case *fsspec.SymbolicLink:
185 if err := cpioWriter.WriteHeader(&cpio.Header{
186 // Symlinks are 0777 by definition (from man 7 symlink on Linux)
Lorenz Brun51350b02023-04-18 00:34:53 +0200187 Mode: 0777 | cpio.TypeSymlink,
188 Name: strings.TrimPrefix(i.Path, "/"),
189 Size: int64(len(i.TargetPath)),
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100190 }); err != nil {
191 log.Fatalf("Failed to write cpio header for symlink %q: %v", i.Path, err)
192 }
Lorenz Brun51350b02023-04-18 00:34:53 +0200193 if _, err := cpioWriter.Write([]byte(i.TargetPath)); err != nil {
194 log.Fatalf("Failed to write cpio symlink %q: %v", i.Path, err)
195 }
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100196 case *fsspec.SpecialFile:
197 mode := cpio.FileMode(i.Mode)
198 switch i.Type {
199 case fsspec.SpecialFile_CHARACTER_DEV:
Lorenz Brund13c1c62022-03-30 19:58:58 +0200200 mode |= cpio.TypeChar
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100201 case fsspec.SpecialFile_BLOCK_DEV:
Lorenz Brund13c1c62022-03-30 19:58:58 +0200202 mode |= cpio.TypeBlock
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100203 case fsspec.SpecialFile_FIFO:
Lorenz Brund13c1c62022-03-30 19:58:58 +0200204 mode |= cpio.TypeFifo
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100205 }
206
207 if err := cpioWriter.WriteHeader(&cpio.Header{
208 Mode: mode,
209 Name: strings.TrimPrefix(i.Path, "/"),
210 DeviceID: int(unix.Mkdev(i.Major, i.Minor)),
211 }); err != nil {
212 log.Fatalf("Failed to write CPIO header for special file %q: %v", i.Path, err)
213 }
214 default:
215 panic("inode type not handled")
216 }
217 }
218}