blob: 791c7941e034495386f08179d7bf6ce0538a69a0 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brun17c4c8b2022-02-01 12:59:47 +01004// fwprune is a buildsystem utility that filters linux-firmware repository
5// contents to include only files required by the built-in kernel modules,
6// that are specified in modules.builtin.modinfo.
7// (see: https://www.kernel.org/doc/Documentation/kbuild/kbuild.txt)
8package main
9
10import (
Lorenz Brun6c454342023-06-01 12:23:38 +020011 "debug/elf"
12 "flag"
13 "io/fs"
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010014 "log"
15 "os"
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010016 "path"
Lorenz Brun6c454342023-06-01 12:23:38 +020017 "path/filepath"
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010018 "regexp"
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010019 "sort"
20 "strings"
21
22 "google.golang.org/protobuf/encoding/prototext"
Lorenz Brun6c454342023-06-01 12:23:38 +020023 "google.golang.org/protobuf/proto"
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010024
Tim Windelschmidtc2290c22024-08-15 19:56:00 +020025 "source.monogon.dev/osbase/build/fsspec"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020026 "source.monogon.dev/osbase/kmod"
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010027)
28
Lorenz Brun6c454342023-06-01 12:23:38 +020029// linkRegexp parses the Link: lines in the WHENCE file. This does not have
30// an official grammar, the regexp has been written in an approximation of
31// the original parsing algorithm at @linux-firmware//:copy_firmware.sh.
Jan Schär4912c5a2024-04-08 18:01:09 +020032var linkRegexp = regexp.MustCompile(`(?m:^Link:\s*([^\s]+)\s+->\s+([^\s]+)\s*$)`)
Lorenz Brun6c454342023-06-01 12:23:38 +020033
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010034var (
Lorenz Brun6c454342023-06-01 12:23:38 +020035 modinfoPath = flag.String("modinfo", "", "Path to the modules.builtin.modinfo file built with the kernel")
36 modulesPath = flag.String("modules", "", "Path to the directory containing the dynamically loaded kernel modules (.ko files)")
37 firmwareListPath = flag.String("firmware-file-list", "", "Path to a file containing a newline-separated list of paths to firmware files")
38 whenceFilePath = flag.String("firmware-whence", "", "Path to the linux-firmware WHENCE file containing aliases for firmware files")
39 outMetaPath = flag.String("out-meta", "", "Path where the resulting module metadata protobuf file should be created")
40 outFSSpecPath = flag.String("out-fsspec", "", "Path where the resulting fsspec should be created")
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010041)
42
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010043func main() {
Lorenz Brun6c454342023-06-01 12:23:38 +020044 flag.Parse()
45 if *modinfoPath == "" || *modulesPath == "" || *firmwareListPath == "" ||
46 *whenceFilePath == "" || *outMetaPath == "" || *outFSSpecPath == "" {
47 log.Fatal("all flags are required and need to be provided")
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010048 }
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010049
Lorenz Brun6c454342023-06-01 12:23:38 +020050 allFirmwareData, err := os.ReadFile(*firmwareListPath)
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010051 if err != nil {
52 log.Fatalf("Failed to read firmware source list: %v", err)
53 }
54 allFirmwarePaths := strings.Split(string(allFirmwareData), "\n")
55
56 // Create a look-up table of all possible suffixes to their full paths as
57 // this is much faster at O(n) than calling strings.HasSuffix for every
58 // possible combination which is O(n^2).
Lorenz Brun6c454342023-06-01 12:23:38 +020059 // For example a build output at out/a/b/c.bin will be entered into
60 // the suffix LUT as build as out/a/b/c.bin, a/b/c.bin, b/c.bin and c.bin.
61 // If the firmware then requests b/c.bin, the output path is contained in
62 // the suffix LUT.
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010063 suffixLUT := make(map[string]string)
64 for _, firmwarePath := range allFirmwarePaths {
65 pathParts := strings.Split(firmwarePath, string(os.PathSeparator))
66 for i := range pathParts {
Lorenz Brun6c454342023-06-01 12:23:38 +020067 suffixLUT[path.Join(pathParts[i:]...)] = firmwarePath
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010068 }
69 }
70
Lorenz Brun6c454342023-06-01 12:23:38 +020071 // The linux-firmware repo contains a WHENCE file which contains (among
72 // other information) aliases for firmware which should be symlinked.
73 // Open this file and create a map of aliases in it.
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010074 linkMap := make(map[string]string)
Lorenz Brun6c454342023-06-01 12:23:38 +020075 metadata, err := os.ReadFile(*whenceFilePath)
Lorenz Brund3ce0ac2022-03-03 12:51:21 +010076 if err != nil {
77 log.Fatalf("Failed to read metadata file: %v", err)
78 }
79 linksRaw := linkRegexp.FindAllStringSubmatch(string(metadata), -1)
80 for _, link := range linksRaw {
81 // For links we know the exact path referenced by kernel drives so
82 // a suffix LUT is unnecessary.
83 linkMap[link[1]] = link[2]
84 }
85
Lorenz Brun6c454342023-06-01 12:23:38 +020086 // Collect module metadata (modinfo) from both built-in modules via the
87 // kbuild-generated metadata file as well as from the loadable modules by
88 // walking them.
89 var files []*fsspec.File
90 var symlinks []*fsspec.SymbolicLink
91
92 mi, err := os.Open(*modinfoPath)
Lorenz Brun17c4c8b2022-02-01 12:59:47 +010093 if err != nil {
94 log.Fatalf("While reading modinfo: %v", err)
95 }
Lorenz Brun6c454342023-06-01 12:23:38 +020096 modMeta, err := kmod.GetBuiltinModulesInfo(mi)
97 if err != nil {
98 log.Fatalf("Failed to read modules modinfo data: %v", err)
99 }
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100100
Lorenz Brun6c454342023-06-01 12:23:38 +0200101 err = filepath.WalkDir(*modulesPath, func(p string, d fs.DirEntry, err error) error {
102 if err != nil {
103 log.Fatal(err)
104 }
105 if d.IsDir() {
106 return nil
107 }
108 mod, err := elf.Open(p)
109 if err != nil {
110 log.Fatal(err)
111 }
112 defer mod.Close()
113 out, err := kmod.GetModuleInfo(mod)
114 if err != nil {
115 log.Fatal(err)
116 }
117 relPath, err := filepath.Rel(*modulesPath, p)
118 if err != nil {
119 return err
120 }
121 // Add path information for MakeMetaFromModuleInfo.
122 out["path"] = []string{relPath}
123 modMeta = append(modMeta, out)
124 files = append(files, &fsspec.File{
125 Path: path.Join("/lib/modules", relPath),
126 SourcePath: filepath.Join(*modulesPath, relPath),
127 Mode: 0555,
128 })
129 return nil
130 })
131 if err != nil {
132 log.Fatalf("Error walking modules: %v", err)
133 }
134
135 // Generate loading metadata from all known modules.
136 meta, err := kmod.MakeMetaFromModuleInfo(modMeta)
137 if err != nil {
138 log.Fatal(err)
139 }
140 metaRaw, err := proto.Marshal(meta)
141 if err != nil {
142 log.Fatal(err)
143 }
144 if err := os.WriteFile(*outMetaPath, metaRaw, 0640); err != nil {
145 log.Fatal(err)
146 }
147 files = append(files, &fsspec.File{
148 Path: "/lib/modules/meta.pb",
149 SourcePath: *outMetaPath,
150 Mode: 0444,
151 })
152
153 // Create set of all firmware paths required by modules
154 fwset := make(map[string]bool)
155 for _, m := range modMeta {
156 if len(m["path"]) == 0 && len(m.Firmware()) > 0 {
157 log.Fatalf("Module %v is built-in, but requires firmware. Linux does not support this in all configurations.", m.Name())
158 }
159 for _, fw := range m.Firmware() {
160 fwset[fw] = true
161 }
162 }
163
164 // Convert set to list and sort for determinism
165 fwp := make([]string, 0, len(fwset))
166 for p := range fwset {
167 fwp = append(fwp, p)
168 }
169 sort.Strings(fwp)
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100170
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100171 // This function is called for every requested firmware file and adds and
172 // resolves symlinks until it finds the target file and adds that too.
173 populatedPaths := make(map[string]bool)
174 var chaseReference func(string)
175 chaseReference = func(p string) {
176 if populatedPaths[p] {
177 // Bail if path is already populated. Because of the DAG-like
178 // property of links in filesystems everything transitively pointed
179 // to by anything at this path has already been included.
180 return
181 }
182 placedPath := path.Join("/lib/firmware", p)
183 if linkTarget := linkMap[p]; linkTarget != "" {
184 symlinks = append(symlinks, &fsspec.SymbolicLink{
185 Path: placedPath,
186 TargetPath: linkTarget,
187 })
Lorenz Brunda342a42023-10-24 15:47:54 +0200188 populatedPaths[p] = true
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100189 // Symlinks are relative to their place, resolve them to be relative
190 // to the firmware root directory.
191 chaseReference(path.Join(path.Dir(p), linkTarget))
192 return
193 }
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100194 sourcePath := suffixLUT[p]
195 if sourcePath == "" {
196 // This should not be fatal as sometimes linux-firmware cannot
197 // ship all firmware usable by the kernel for mostly legal reasons.
198 log.Printf("WARNING: Requested firmware %q not found", p)
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100199 return
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100200 }
201 files = append(files, &fsspec.File{
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100202 Path: path.Join("/lib/firmware", p),
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100203 Mode: 0444,
204 SourcePath: sourcePath,
205 })
Lorenz Brunda342a42023-10-24 15:47:54 +0200206 populatedPaths[p] = true
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100207 }
Lorenz Brund3ce0ac2022-03-03 12:51:21 +0100208
209 for _, p := range fwp {
210 chaseReference(p)
211 }
212 // Format output in a both human- and machine-readable form
213 marshalOpts := prototext.MarshalOptions{Multiline: true, Indent: " "}
214 fsspecRaw, err := marshalOpts.Marshal(&fsspec.FSSpec{File: files, SymbolicLink: symlinks})
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200215 if err != nil {
216 log.Fatalf("failed to marshal fsspec: %v", err)
217 }
Lorenz Brun6c454342023-06-01 12:23:38 +0200218 if err := os.WriteFile(*outFSSpecPath, fsspecRaw, 0644); err != nil {
Lorenz Brun17c4c8b2022-02-01 12:59:47 +0100219 log.Fatalf("failed writing output: %v", err)
220 }
221}