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" |
| 11 | "path/filepath" |
| 12 | "sort" |
| 13 | "strings" |
| 14 | |
| 15 | "google.golang.org/protobuf/encoding/prototext" |
| 16 | |
| 17 | "source.monogon.dev/metropolis/node/build/fsspec" |
| 18 | ) |
| 19 | |
| 20 | // fwPaths returns a slice of filesystem paths relative to the root of the |
| 21 | // linux-firmware repository, pointing at firmware files, according to contents |
| 22 | // of the kernel build side effect: modules.builtin.modinfo. |
| 23 | func fwPaths(mi []byte) []string { |
| 24 | // Use a map pset to deduplicate firmware paths. |
| 25 | pset := make(map[string]bool) |
| 26 | // Get a slice of entries of the form "unix.license=GPL" from mi. Then extract |
| 27 | // firmware information from it. |
| 28 | entries := bytes.Split(mi, []byte{0}) |
| 29 | for _, entry := range entries { |
| 30 | // Skip empty entries. |
| 31 | if len(entry) == 0 { |
| 32 | continue |
| 33 | } |
| 34 | // Parse the entries. Split each entry into a key-value pair, separated |
| 35 | // by "=". |
| 36 | kv := strings.SplitN(string(entry), "=", 2) |
| 37 | key, value := kv[0], kv[1] |
| 38 | // Split the key into a module.attribute] pair, such as "unix.license". |
| 39 | ma := strings.SplitN(key, ".", 2) |
| 40 | // Skip, if it's not a firmware entry, according to the attribute. |
| 41 | if ma[1] != "firmware" { |
| 42 | continue |
| 43 | } |
| 44 | // If it is though, value holds a firmware path. |
| 45 | pset[value] = true |
| 46 | } |
| 47 | // Convert the deduplicated pset to a slice. |
| 48 | pslice := make([]string, 0, len(pset)) |
| 49 | for p, _ := range pset { |
| 50 | pslice = append(pslice, p) |
| 51 | } |
| 52 | sort.Strings(pslice) |
| 53 | return pslice |
| 54 | } |
| 55 | |
| 56 | // fwprune takes a modinfo file from the kernel and extracts a list of all |
| 57 | // firmware files requested by all modules in that file. It then takes all |
| 58 | // available firmware file paths (newline-separated in the firmwareList file) |
| 59 | // and tries to match the requested file paths as a suffix of them. |
| 60 | // For example if a module requests firmware foo/bar.bin and in the firmware list |
| 61 | // there is a file at path build-out/x/y/foo/bar.bin it will use that file. |
| 62 | // Finally it generates an fsspec placing each file under its requested path |
| 63 | // under /lib/firmware. |
| 64 | func main() { |
| 65 | if len(os.Args) != 4 { |
| 66 | log.Fatal("Usage: fwprune modules.builtin.modinfo firmwareListPath outSpec") |
| 67 | } |
| 68 | modinfo := os.Args[1] |
| 69 | firmwareListPath := os.Args[2] |
| 70 | outSpec := os.Args[3] |
| 71 | |
| 72 | allFirmwareData, err := os.ReadFile(firmwareListPath) |
| 73 | if err != nil { |
| 74 | log.Fatalf("Failed to read firmware source list: %v", err) |
| 75 | } |
| 76 | allFirmwarePaths := strings.Split(string(allFirmwareData), "\n") |
| 77 | |
| 78 | // Create a look-up table of all possible suffixes to their full paths as |
| 79 | // this is much faster at O(n) than calling strings.HasSuffix for every |
| 80 | // possible combination which is O(n^2). |
| 81 | suffixLUT := make(map[string]string) |
| 82 | for _, firmwarePath := range allFirmwarePaths { |
| 83 | pathParts := strings.Split(firmwarePath, string(os.PathSeparator)) |
| 84 | for i := range pathParts { |
| 85 | suffixLUT[filepath.Join(pathParts[i:len(pathParts)]...)] = firmwarePath |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | // Get the firmware file paths used by modules according to modinfo data |
| 90 | mi, err := os.ReadFile(modinfo) |
| 91 | if err != nil { |
| 92 | log.Fatalf("While reading modinfo: %v", err) |
| 93 | } |
| 94 | fwp := fwPaths(mi) |
| 95 | |
| 96 | var files []*fsspec.File |
| 97 | |
| 98 | for _, p := range fwp { |
| 99 | sourcePath := suffixLUT[p] |
| 100 | if sourcePath == "" { |
| 101 | // This should not be fatal as sometimes linux-firmware cannot |
| 102 | // ship all firmware usable by the kernel for mostly legal reasons. |
| 103 | log.Printf("WARNING: Requested firmware %q not found", p) |
| 104 | continue |
| 105 | } |
| 106 | files = append(files, &fsspec.File{ |
| 107 | Path: filepath.Join("/lib/firmware", p), |
| 108 | Mode: 0444, |
| 109 | SourcePath: sourcePath, |
| 110 | }) |
| 111 | } |
| 112 | fsspecRaw, err := prototext.Marshal(&fsspec.FSSpec{File: files}) |
| 113 | if err := os.WriteFile(outSpec, fsspecRaw, 0644); err != nil { |
| 114 | log.Fatalf("failed writing output: %v", err) |
| 115 | } |
| 116 | } |