blob: c8c8e9c56728ba454db55ac7e0b011639a313c4c [file] [log] [blame]
Lorenz Brun17c4c8b2022-02-01 12:59:47 +01001// fwprune is a buildsystem utility that filters linux-firmware repository
2// contents to include only files required by the built-in kernel modules,
3// that are specified in modules.builtin.modinfo.
4// (see: https://www.kernel.org/doc/Documentation/kbuild/kbuild.txt)
5package main
6
7import (
Lorenz Brun6c454342023-06-01 12:23:38 +02008 "debug/elf"
9 "flag"
10 "io/fs"
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010011 "log"
12 "os"
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010013 "path"
Lorenz Brun6c454342023-06-01 12:23:38 +020014 "path/filepath"
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010015 "regexp"
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010016 "sort"
17 "strings"
18
19 "google.golang.org/protobuf/encoding/prototext"
Lorenz Brun6c454342023-06-01 12:23:38 +020020 "google.golang.org/protobuf/proto"
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010021
22 "source.monogon.dev/metropolis/node/build/fsspec"
Lorenz Brun6c454342023-06-01 12:23:38 +020023 "source.monogon.dev/metropolis/pkg/kmod"
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010024)
25
Lorenz Brun6c454342023-06-01 12:23:38 +020026// linkRegexp parses the Link: lines in the WHENCE file. This does not have
27// an official grammar, the regexp has been written in an approximation of
28// the original parsing algorithm at @linux-firmware//:copy_firmware.sh.
29var linkRegexp = regexp.MustCompile(`(?m:^Link:\s*([^\s]+)\s+->\s+([^\s+]+)\s*$)`)
30
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010031var (
Lorenz Brun6c454342023-06-01 12:23:38 +020032 modinfoPath = flag.String("modinfo", "", "Path to the modules.builtin.modinfo file built with the kernel")
33 modulesPath = flag.String("modules", "", "Path to the directory containing the dynamically loaded kernel modules (.ko files)")
34 firmwareListPath = flag.String("firmware-file-list", "", "Path to a file containing a newline-separated list of paths to firmware files")
35 whenceFilePath = flag.String("firmware-whence", "", "Path to the linux-firmware WHENCE file containing aliases for firmware files")
36 outMetaPath = flag.String("out-meta", "", "Path where the resulting module metadata protobuf file should be created")
37 outFSSpecPath = flag.String("out-fsspec", "", "Path where the resulting fsspec should be created")
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010038)
39
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010040func main() {
Lorenz Brun6c454342023-06-01 12:23:38 +020041 flag.Parse()
42 if *modinfoPath == "" || *modulesPath == "" || *firmwareListPath == "" ||
43 *whenceFilePath == "" || *outMetaPath == "" || *outFSSpecPath == "" {
44 log.Fatal("all flags are required and need to be provided")
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010045 }
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010046
Lorenz Brun6c454342023-06-01 12:23:38 +020047 allFirmwareData, err := os.ReadFile(*firmwareListPath)
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010048 if err != nil {
49 log.Fatalf("Failed to read firmware source list: %v", err)
50 }
51 allFirmwarePaths := strings.Split(string(allFirmwareData), "\n")
52
53 // Create a look-up table of all possible suffixes to their full paths as
54 // this is much faster at O(n) than calling strings.HasSuffix for every
55 // possible combination which is O(n^2).
Lorenz Brun6c454342023-06-01 12:23:38 +020056 // For example a build output at out/a/b/c.bin will be entered into
57 // the suffix LUT as build as out/a/b/c.bin, a/b/c.bin, b/c.bin and c.bin.
58 // If the firmware then requests b/c.bin, the output path is contained in
59 // the suffix LUT.
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010060 suffixLUT := make(map[string]string)
61 for _, firmwarePath := range allFirmwarePaths {
62 pathParts := strings.Split(firmwarePath, string(os.PathSeparator))
63 for i := range pathParts {
Lorenz Brun6c454342023-06-01 12:23:38 +020064 suffixLUT[path.Join(pathParts[i:]...)] = firmwarePath
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010065 }
66 }
67
Lorenz Brun6c454342023-06-01 12:23:38 +020068 // The linux-firmware repo contains a WHENCE file which contains (among
69 // other information) aliases for firmware which should be symlinked.
70 // Open this file and create a map of aliases in it.
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010071 linkMap := make(map[string]string)
Lorenz Brun6c454342023-06-01 12:23:38 +020072 metadata, err := os.ReadFile(*whenceFilePath)
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010073 if err != nil {
74 log.Fatalf("Failed to read metadata file: %v", err)
75 }
76 linksRaw := linkRegexp.FindAllStringSubmatch(string(metadata), -1)
77 for _, link := range linksRaw {
78 // For links we know the exact path referenced by kernel drives so
79 // a suffix LUT is unnecessary.
80 linkMap[link[1]] = link[2]
81 }
82
Lorenz Brun6c454342023-06-01 12:23:38 +020083 // Collect module metadata (modinfo) from both built-in modules via the
84 // kbuild-generated metadata file as well as from the loadable modules by
85 // walking them.
86 var files []*fsspec.File
87 var symlinks []*fsspec.SymbolicLink
88
89 mi, err := os.Open(*modinfoPath)
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010090 if err != nil {
91 log.Fatalf("While reading modinfo: %v", err)
92 }
Lorenz Brun6c454342023-06-01 12:23:38 +020093 modMeta, err := kmod.GetBuiltinModulesInfo(mi)
94 if err != nil {
95 log.Fatalf("Failed to read modules modinfo data: %v", err)
96 }
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010097
Lorenz Brun6c454342023-06-01 12:23:38 +020098 err = filepath.WalkDir(*modulesPath, func(p string, d fs.DirEntry, err error) error {
99 if err != nil {
100 log.Fatal(err)
101 }
102 if d.IsDir() {
103 return nil
104 }
105 mod, err := elf.Open(p)
106 if err != nil {
107 log.Fatal(err)
108 }
109 defer mod.Close()
110 out, err := kmod.GetModuleInfo(mod)
111 if err != nil {
112 log.Fatal(err)
113 }
114 relPath, err := filepath.Rel(*modulesPath, p)
115 if err != nil {
116 return err
117 }
118 // Add path information for MakeMetaFromModuleInfo.
119 out["path"] = []string{relPath}
120 modMeta = append(modMeta, out)
121 files = append(files, &fsspec.File{
122 Path: path.Join("/lib/modules", relPath),
123 SourcePath: filepath.Join(*modulesPath, relPath),
124 Mode: 0555,
125 })
126 return nil
127 })
128 if err != nil {
129 log.Fatalf("Error walking modules: %v", err)
130 }
131
132 // Generate loading metadata from all known modules.
133 meta, err := kmod.MakeMetaFromModuleInfo(modMeta)
134 if err != nil {
135 log.Fatal(err)
136 }
137 metaRaw, err := proto.Marshal(meta)
138 if err != nil {
139 log.Fatal(err)
140 }
141 if err := os.WriteFile(*outMetaPath, metaRaw, 0640); err != nil {
142 log.Fatal(err)
143 }
144 files = append(files, &fsspec.File{
145 Path: "/lib/modules/meta.pb",
146 SourcePath: *outMetaPath,
147 Mode: 0444,
148 })
149
150 // Create set of all firmware paths required by modules
151 fwset := make(map[string]bool)
152 for _, m := range modMeta {
153 if len(m["path"]) == 0 && len(m.Firmware()) > 0 {
154 log.Fatalf("Module %v is built-in, but requires firmware. Linux does not support this in all configurations.", m.Name())
155 }
156 for _, fw := range m.Firmware() {
157 fwset[fw] = true
158 }
159 }
160
161 // Convert set to list and sort for determinism
162 fwp := make([]string, 0, len(fwset))
163 for p := range fwset {
164 fwp = append(fwp, p)
165 }
166 sort.Strings(fwp)
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100167
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100168 // This function is called for every requested firmware file and adds and
169 // resolves symlinks until it finds the target file and adds that too.
170 populatedPaths := make(map[string]bool)
171 var chaseReference func(string)
172 chaseReference = func(p string) {
173 if populatedPaths[p] {
174 // Bail if path is already populated. Because of the DAG-like
175 // property of links in filesystems everything transitively pointed
176 // to by anything at this path has already been included.
177 return
178 }
179 placedPath := path.Join("/lib/firmware", p)
180 if linkTarget := linkMap[p]; linkTarget != "" {
181 symlinks = append(symlinks, &fsspec.SymbolicLink{
182 Path: placedPath,
183 TargetPath: linkTarget,
184 })
185 populatedPaths[placedPath] = true
186 // Symlinks are relative to their place, resolve them to be relative
187 // to the firmware root directory.
188 chaseReference(path.Join(path.Dir(p), linkTarget))
189 return
190 }
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100191 sourcePath := suffixLUT[p]
192 if sourcePath == "" {
193 // This should not be fatal as sometimes linux-firmware cannot
194 // ship all firmware usable by the kernel for mostly legal reasons.
195 log.Printf("WARNING: Requested firmware %q not found", p)
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100196 return
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100197 }
198 files = append(files, &fsspec.File{
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100199 Path: path.Join("/lib/firmware", p),
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100200 Mode: 0444,
201 SourcePath: sourcePath,
202 })
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100203 populatedPaths[path.Join("/lib/firmware", p)] = true
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100204 }
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100205
206 for _, p := range fwp {
207 chaseReference(p)
208 }
209 // Format output in a both human- and machine-readable form
210 marshalOpts := prototext.MarshalOptions{Multiline: true, Indent: " "}
211 fsspecRaw, err := marshalOpts.Marshal(&fsspec.FSSpec{File: files, SymbolicLink: symlinks})
Lorenz Brun6c454342023-06-01 12:23:38 +0200212 if err := os.WriteFile(*outFSSpecPath, fsspecRaw, 0644); err != nil {
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100213 log.Fatalf("failed writing output: %v", err)
214 }
215}