| // fwprune is a buildsystem utility that filters linux-firmware repository |
| // contents to include only files required by the built-in kernel modules, |
| // that are specified in modules.builtin.modinfo. |
| // (see: https://www.kernel.org/doc/Documentation/kbuild/kbuild.txt) |
| package main |
| |
| import ( |
| "bytes" |
| "log" |
| "os" |
| "path" |
| "regexp" |
| "sort" |
| "strings" |
| |
| "google.golang.org/protobuf/encoding/prototext" |
| |
| "source.monogon.dev/metropolis/node/build/fsspec" |
| ) |
| |
| var ( |
| // linkRegexp parses the Link: lines in the WHENCE file. This does not have |
| // an official grammar, the regexp has been written in an approximation of |
| // the original parsing algorithm at @linux-firmware//:copy_firmware.sh. |
| linkRegexp = regexp.MustCompile(`(?m:^Link:\s*([^\s]+)\s+->\s+([^\s+]+)\s*$)`) |
| ) |
| |
| // fwPaths returns a slice of filesystem paths relative to the root of the |
| // linux-firmware repository, pointing at firmware files, according to contents |
| // of the kernel build side effect: modules.builtin.modinfo. |
| func fwPaths(mi []byte) []string { |
| // Use a map pset to deduplicate firmware paths. |
| pset := make(map[string]bool) |
| // Get a slice of entries of the form "unix.license=GPL" from mi. Then extract |
| // firmware information from it. |
| entries := bytes.Split(mi, []byte{0}) |
| for _, entry := range entries { |
| // Skip empty entries. |
| if len(entry) == 0 { |
| continue |
| } |
| // Parse the entries. Split each entry into a key-value pair, separated |
| // by "=". |
| kv := strings.SplitN(string(entry), "=", 2) |
| key, value := kv[0], kv[1] |
| // Split the key into a module.attribute] pair, such as "unix.license". |
| ma := strings.SplitN(key, ".", 2) |
| // Skip, if it's not a firmware entry, according to the attribute. |
| if ma[1] != "firmware" { |
| continue |
| } |
| // If it is though, value holds a firmware path. |
| pset[value] = true |
| } |
| // Convert the deduplicated pset to a slice. |
| pslice := make([]string, 0, len(pset)) |
| for p, _ := range pset { |
| pslice = append(pslice, p) |
| } |
| sort.Strings(pslice) |
| return pslice |
| } |
| |
| // fwprune takes a modinfo file from the kernel and extracts a list of all |
| // firmware files requested by all modules in that file. It then takes all |
| // available firmware file paths (newline-separated in the firmwareList file) |
| // and tries to match the requested file paths as a suffix of them. |
| // For example if a module requests firmware foo/bar.bin and in the firmware list |
| // there is a file at path build-out/x/y/foo/bar.bin it will use that file. |
| // It also parses links from the linux-firmware metadata file and uses them |
| // for matching requested firmware. |
| // Finally it generates an fsspec placing each file under its requested path |
| // under /lib/firmware. |
| func main() { |
| if len(os.Args) != 5 { |
| log.Fatal("Usage: fwprune modules.builtin.modinfo firmwareListPath metadataFilePath outSpec") |
| } |
| modinfo := os.Args[1] |
| firmwareListPath := os.Args[2] |
| metadataFilePath := os.Args[3] |
| outSpec := os.Args[4] |
| |
| allFirmwareData, err := os.ReadFile(firmwareListPath) |
| if err != nil { |
| log.Fatalf("Failed to read firmware source list: %v", err) |
| } |
| allFirmwarePaths := strings.Split(string(allFirmwareData), "\n") |
| |
| // Create a look-up table of all possible suffixes to their full paths as |
| // this is much faster at O(n) than calling strings.HasSuffix for every |
| // possible combination which is O(n^2). |
| suffixLUT := make(map[string]string) |
| for _, firmwarePath := range allFirmwarePaths { |
| pathParts := strings.Split(firmwarePath, string(os.PathSeparator)) |
| for i := range pathParts { |
| suffixLUT[path.Join(pathParts[i:len(pathParts)]...)] = firmwarePath |
| } |
| } |
| |
| linkMap := make(map[string]string) |
| metadata, err := os.ReadFile(metadataFilePath) |
| if err != nil { |
| log.Fatalf("Failed to read metadata file: %v", err) |
| } |
| linksRaw := linkRegexp.FindAllStringSubmatch(string(metadata), -1) |
| for _, link := range linksRaw { |
| // For links we know the exact path referenced by kernel drives so |
| // a suffix LUT is unnecessary. |
| linkMap[link[1]] = link[2] |
| } |
| |
| // Get the firmware file paths used by modules according to modinfo data |
| mi, err := os.ReadFile(modinfo) |
| if err != nil { |
| log.Fatalf("While reading modinfo: %v", err) |
| } |
| fwp := fwPaths(mi) |
| |
| var files []*fsspec.File |
| var symlinks []*fsspec.SymbolicLink |
| |
| // This function is called for every requested firmware file and adds and |
| // resolves symlinks until it finds the target file and adds that too. |
| populatedPaths := make(map[string]bool) |
| var chaseReference func(string) |
| chaseReference = func(p string) { |
| if populatedPaths[p] { |
| // Bail if path is already populated. Because of the DAG-like |
| // property of links in filesystems everything transitively pointed |
| // to by anything at this path has already been included. |
| return |
| } |
| placedPath := path.Join("/lib/firmware", p) |
| if linkTarget := linkMap[p]; linkTarget != "" { |
| symlinks = append(symlinks, &fsspec.SymbolicLink{ |
| Path: placedPath, |
| TargetPath: linkTarget, |
| }) |
| populatedPaths[placedPath] = true |
| // Symlinks are relative to their place, resolve them to be relative |
| // to the firmware root directory. |
| chaseReference(path.Join(path.Dir(p), linkTarget)) |
| return |
| } |
| sourcePath := suffixLUT[p] |
| if sourcePath == "" { |
| // This should not be fatal as sometimes linux-firmware cannot |
| // ship all firmware usable by the kernel for mostly legal reasons. |
| log.Printf("WARNING: Requested firmware %q not found", p) |
| return |
| } |
| files = append(files, &fsspec.File{ |
| Path: path.Join("/lib/firmware", p), |
| Mode: 0444, |
| SourcePath: sourcePath, |
| }) |
| populatedPaths[path.Join("/lib/firmware", p)] = true |
| } |
| |
| for _, p := range fwp { |
| chaseReference(p) |
| } |
| // Format output in a both human- and machine-readable form |
| marshalOpts := prototext.MarshalOptions{Multiline: true, Indent: " "} |
| fsspecRaw, err := marshalOpts.Marshal(&fsspec.FSSpec{File: files, SymbolicLink: symlinks}) |
| if err := os.WriteFile(outSpec, fsspecRaw, 0644); err != nil { |
| log.Fatalf("failed writing output: %v", err) |
| } |
| } |