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 ( |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 8 | "debug/elf" |
| 9 | "flag" |
| 10 | "io/fs" |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 11 | "log" |
| 12 | "os" |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 13 | "path" |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 14 | "path/filepath" |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 15 | "regexp" |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 16 | "sort" |
| 17 | "strings" |
| 18 | |
| 19 | "google.golang.org/protobuf/encoding/prototext" |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 20 | "google.golang.org/protobuf/proto" |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 21 | |
| 22 | "source.monogon.dev/metropolis/node/build/fsspec" |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 23 | "source.monogon.dev/metropolis/pkg/kmod" |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 24 | ) |
| 25 | |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 26 | // 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. |
| 29 | var linkRegexp = regexp.MustCompile(`(?m:^Link:\s*([^\s]+)\s+->\s+([^\s+]+)\s*$)`) |
| 30 | |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 31 | var ( |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 32 | 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 Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 38 | ) |
| 39 | |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 40 | func main() { |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 41 | flag.Parse() |
| 42 | if *modinfoPath == "" || *modulesPath == "" || *firmwareListPath == "" || |
| 43 | *whenceFilePath == "" || *outMetaPath == "" || *outFSSpecPath == "" { |
| 44 | log.Fatal("all flags are required and need to be provided") |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 45 | } |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 46 | |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 47 | allFirmwareData, err := os.ReadFile(*firmwareListPath) |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 48 | 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 Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 56 | // 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 Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 60 | suffixLUT := make(map[string]string) |
| 61 | for _, firmwarePath := range allFirmwarePaths { |
| 62 | pathParts := strings.Split(firmwarePath, string(os.PathSeparator)) |
| 63 | for i := range pathParts { |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 64 | suffixLUT[path.Join(pathParts[i:]...)] = firmwarePath |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 65 | } |
| 66 | } |
| 67 | |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 68 | // 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 Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 71 | linkMap := make(map[string]string) |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 72 | metadata, err := os.ReadFile(*whenceFilePath) |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 73 | 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 Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 83 | // 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 Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 90 | if err != nil { |
| 91 | log.Fatalf("While reading modinfo: %v", err) |
| 92 | } |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 93 | modMeta, err := kmod.GetBuiltinModulesInfo(mi) |
| 94 | if err != nil { |
| 95 | log.Fatalf("Failed to read modules modinfo data: %v", err) |
| 96 | } |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 97 | |
Lorenz Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 98 | 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 Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 167 | |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 168 | // 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 Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 191 | 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 Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 196 | return |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 197 | } |
| 198 | files = append(files, &fsspec.File{ |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 199 | Path: path.Join("/lib/firmware", p), |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 200 | Mode: 0444, |
| 201 | SourcePath: sourcePath, |
| 202 | }) |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 203 | populatedPaths[path.Join("/lib/firmware", p)] = true |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 204 | } |
Lorenz Brun | d3ce0ac | 2022-03-03 12:51:21 +0100 | [diff] [blame] | 205 | |
| 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 Brun | 6c45434 | 2023-06-01 12:23:38 +0200 | [diff] [blame^] | 212 | if err := os.WriteFile(*outFSSpecPath, fsspecRaw, 0644); err != nil { |
Lorenz Brun | 17c4c8b | 2022-02-01 12:59:47 +0100 | [diff] [blame] | 213 | log.Fatalf("failed writing output: %v", err) |
| 214 | } |
| 215 | } |