Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 1 | // 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) |
| 5 | package main |
| 6 | |
| 7 | import ( |
| 8 | "bytes" |
| 9 | "log" |
| 10 | "os" |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 11 | "path" |
| 12 | "regexp" |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 13 | "sort" |
| 14 | "strings" |
| 15 | |
| 16 | "google.golang.org/protobuf/encoding/prototext" |
| 17 | |
| 18 | "source.monogon.dev/metropolis/node/build/fsspec" |
| 19 | ) |
| 20 | |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 21 | var ( |
| 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 Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 28 | // 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. |
| 31 | func 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 Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 70 | // It also parses links from the linux-firmware metadata file and uses them |
| 71 | // for matching requested firmware. |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 72 | // Finally it generates an fsspec placing each file under its requested path |
| 73 | // under /lib/firmware. |
| 74 | func main() { |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 75 | if len(os.Args) != 5 { |
| 76 | log.Fatal("Usage: fwprune modules.builtin.modinfo firmwareListPath metadataFilePath outSpec") |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 77 | } |
| 78 | modinfo := os.Args[1] |
| 79 | firmwareListPath := os.Args[2] |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 80 | metadataFilePath := os.Args[3] |
| 81 | outSpec := os.Args[4] |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 82 | |
| 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 Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 96 | suffixLUT[path.Join(pathParts[i:len(pathParts)]...)] = firmwarePath |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 97 | } |
| 98 | } |
| 99 | |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 100 | 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 Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 112 | // 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 Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 120 | var symlinks []*fsspec.SymbolicLink |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 121 | |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 122 | // 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 Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 145 | 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 Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 150 | return |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 151 | } |
| 152 | files = append(files, &fsspec.File{ |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 153 | Path: path.Join("/lib/firmware", p), |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 154 | Mode: 0444, |
| 155 | SourcePath: sourcePath, |
| 156 | }) |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 157 | populatedPaths[path.Join("/lib/firmware", p)] = true |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 158 | } |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame^] | 159 | |
| 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 Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 166 | if err := os.WriteFile(outSpec, fsspecRaw, 0644); err != nil { |
| 167 | log.Fatalf("failed writing output: %v", err) |
| 168 | } |
| 169 | } |