blob: 861a2d09c4649fc36c01a7693cc35a043465bb67 [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 (
8 "bytes"
9 "log"
10 "os"
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010011 "path"
12 "regexp"
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010013 "sort"
14 "strings"
15
16 "google.golang.org/protobuf/encoding/prototext"
17
18 "source.monogon.dev/metropolis/node/build/fsspec"
19)
20
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010021var (
22 // linkRegexp parses the Link: lines in the WHENCE file. This does not have
23 // an official grammar, the regexp has been written in an approximation of
24 // the original parsing algorithm at @linux-firmware//:copy_firmware.sh.
25 linkRegexp = regexp.MustCompile(`(?m:^Link:\s*([^\s]+)\s+->\s+([^\s+]+)\s*$)`)
26)
27
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010028// fwPaths returns a slice of filesystem paths relative to the root of the
29// linux-firmware repository, pointing at firmware files, according to contents
30// of the kernel build side effect: modules.builtin.modinfo.
31func fwPaths(mi []byte) []string {
32 // Use a map pset to deduplicate firmware paths.
33 pset := make(map[string]bool)
34 // Get a slice of entries of the form "unix.license=GPL" from mi. Then extract
35 // firmware information from it.
36 entries := bytes.Split(mi, []byte{0})
37 for _, entry := range entries {
38 // Skip empty entries.
39 if len(entry) == 0 {
40 continue
41 }
42 // Parse the entries. Split each entry into a key-value pair, separated
43 // by "=".
44 kv := strings.SplitN(string(entry), "=", 2)
45 key, value := kv[0], kv[1]
46 // Split the key into a module.attribute] pair, such as "unix.license".
47 ma := strings.SplitN(key, ".", 2)
48 // Skip, if it's not a firmware entry, according to the attribute.
49 if ma[1] != "firmware" {
50 continue
51 }
52 // If it is though, value holds a firmware path.
53 pset[value] = true
54 }
55 // Convert the deduplicated pset to a slice.
56 pslice := make([]string, 0, len(pset))
57 for p, _ := range pset {
58 pslice = append(pslice, p)
59 }
60 sort.Strings(pslice)
61 return pslice
62}
63
64// fwprune takes a modinfo file from the kernel and extracts a list of all
65// firmware files requested by all modules in that file. It then takes all
66// available firmware file paths (newline-separated in the firmwareList file)
67// and tries to match the requested file paths as a suffix of them.
68// For example if a module requests firmware foo/bar.bin and in the firmware list
69// there is a file at path build-out/x/y/foo/bar.bin it will use that file.
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010070// It also parses links from the linux-firmware metadata file and uses them
71// for matching requested firmware.
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010072// Finally it generates an fsspec placing each file under its requested path
73// under /lib/firmware.
74func main() {
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010075 if len(os.Args) != 5 {
76 log.Fatal("Usage: fwprune modules.builtin.modinfo firmwareListPath metadataFilePath outSpec")
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010077 }
78 modinfo := os.Args[1]
79 firmwareListPath := os.Args[2]
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010080 metadataFilePath := os.Args[3]
81 outSpec := os.Args[4]
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010082
83 allFirmwareData, err := os.ReadFile(firmwareListPath)
84 if err != nil {
85 log.Fatalf("Failed to read firmware source list: %v", err)
86 }
87 allFirmwarePaths := strings.Split(string(allFirmwareData), "\n")
88
89 // Create a look-up table of all possible suffixes to their full paths as
90 // this is much faster at O(n) than calling strings.HasSuffix for every
91 // possible combination which is O(n^2).
92 suffixLUT := make(map[string]string)
93 for _, firmwarePath := range allFirmwarePaths {
94 pathParts := strings.Split(firmwarePath, string(os.PathSeparator))
95 for i := range pathParts {
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010096 suffixLUT[path.Join(pathParts[i:len(pathParts)]...)] = firmwarePath
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010097 }
98 }
99
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100100 linkMap := make(map[string]string)
101 metadata, err := os.ReadFile(metadataFilePath)
102 if err != nil {
103 log.Fatalf("Failed to read metadata file: %v", err)
104 }
105 linksRaw := linkRegexp.FindAllStringSubmatch(string(metadata), -1)
106 for _, link := range linksRaw {
107 // For links we know the exact path referenced by kernel drives so
108 // a suffix LUT is unnecessary.
109 linkMap[link[1]] = link[2]
110 }
111
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100112 // Get the firmware file paths used by modules according to modinfo data
113 mi, err := os.ReadFile(modinfo)
114 if err != nil {
115 log.Fatalf("While reading modinfo: %v", err)
116 }
117 fwp := fwPaths(mi)
118
119 var files []*fsspec.File
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100120 var symlinks []*fsspec.SymbolicLink
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100121
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100122 // This function is called for every requested firmware file and adds and
123 // resolves symlinks until it finds the target file and adds that too.
124 populatedPaths := make(map[string]bool)
125 var chaseReference func(string)
126 chaseReference = func(p string) {
127 if populatedPaths[p] {
128 // Bail if path is already populated. Because of the DAG-like
129 // property of links in filesystems everything transitively pointed
130 // to by anything at this path has already been included.
131 return
132 }
133 placedPath := path.Join("/lib/firmware", p)
134 if linkTarget := linkMap[p]; linkTarget != "" {
135 symlinks = append(symlinks, &fsspec.SymbolicLink{
136 Path: placedPath,
137 TargetPath: linkTarget,
138 })
139 populatedPaths[placedPath] = true
140 // Symlinks are relative to their place, resolve them to be relative
141 // to the firmware root directory.
142 chaseReference(path.Join(path.Dir(p), linkTarget))
143 return
144 }
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100145 sourcePath := suffixLUT[p]
146 if sourcePath == "" {
147 // This should not be fatal as sometimes linux-firmware cannot
148 // ship all firmware usable by the kernel for mostly legal reasons.
149 log.Printf("WARNING: Requested firmware %q not found", p)
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100150 return
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100151 }
152 files = append(files, &fsspec.File{
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100153 Path: path.Join("/lib/firmware", p),
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100154 Mode: 0444,
155 SourcePath: sourcePath,
156 })
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100157 populatedPaths[path.Join("/lib/firmware", p)] = true
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100158 }
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100159
160 for _, p := range fwp {
161 chaseReference(p)
162 }
163 // Format output in a both human- and machine-readable form
164 marshalOpts := prototext.MarshalOptions{Multiline: true, Indent: " "}
165 fsspecRaw, err := marshalOpts.Marshal(&fsspec.FSSpec{File: files, SymbolicLink: symlinks})
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100166 if err := os.WriteFile(outSpec, fsspecRaw, 0644); err != nil {
167 log.Fatalf("failed writing output: %v", err)
168 }
169}