github.com/9elements/firmware-action/action@v0.0.0-20240514065043-044ed91c9ed8/recipes/stitching.go (about) 1 // SPDX-License-Identifier: MIT 2 3 // Package recipes / stitching 4 package recipes 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "log/slog" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strings" 15 16 "dagger.io/dagger" 17 "github.com/9elements/firmware-action/action/container" 18 "github.com/9elements/firmware-action/action/logging" 19 "github.com/dustin/go-humanize" 20 ) 21 22 var ( 23 errFailedToDetectRomSize = errors.New("failed to detect ROM size from IFD") 24 errBaseFileBiggerThanIfd = errors.New("base_file is bigger than size defined in IFD") 25 ) 26 27 const ifdtoolPath = "ifdtool" 28 29 // ANCHOR: IfdtoolEntry 30 31 // IfdtoolEntry is for injecting a file at `path` into region `TargetRegion` 32 type IfdtoolEntry struct { 33 // Gives the (relative) path to the binary blob 34 Path string `json:"path" validate:"required,filepath"` 35 36 // Region where to inject the file 37 // For supported options see `ifdtool --help` 38 TargetRegion string `json:"target_region" validate:"required"` 39 40 // Additional (optional) arguments and flags 41 // For example: 42 // `--platform adl` 43 // For supported options see `ifdtool --help` 44 OptionalArguments []string `json:"optional_arguments"` 45 } 46 47 // ANCHOR_END: IfdtoolEntry 48 49 // ANCHOR: FirmwareStitchingOpts 50 51 // FirmwareStitchingOpts is used to store all data needed to stitch firmware 52 type FirmwareStitchingOpts struct { 53 // List of IDs this instance depends on 54 Depends []string `json:"depends"` 55 56 // Common options like paths etc. 57 CommonOpts 58 59 // BaseFile into which inject files. 60 // !!! Must contain IFD !!! 61 // Examples: 62 // - coreboot.rom 63 // - ifd.bin 64 BaseFilePath string `json:"base_file_path" validate:"required,filepath"` 65 66 // Platform - passed to all `ifdtool` calls with `--platform` 67 Platform string `json:"platform"` 68 69 // List of instructions for ifdtool 70 IfdtoolEntries []IfdtoolEntry `json:"ifdtool_entries"` 71 72 // List of instructions for cbfstool 73 // TODO ??? 74 } 75 76 // ANCHOR_END: FirmwareStitchingOpts 77 78 // GetDepends is used to return list of dependencies 79 func (opts FirmwareStitchingOpts) GetDepends() []string { 80 return opts.Depends 81 } 82 83 // GetArtifacts returns list of wanted artifacts from container 84 func (opts FirmwareStitchingOpts) GetArtifacts() *[]container.Artifacts { 85 return opts.CommonOpts.GetArtifacts() 86 } 87 88 // ExtractSizeFromString uses regex to find size of ROM in MB 89 func ExtractSizeFromString(text string) ([]uint64, error) { 90 // Component 1 and 2 represent flash chips on motherboard 91 // 1st is a must, 2nd is optional 92 // Example: 93 // " Component 2 Density: 32MB" 94 // " Component 1 Density: 64MB" 95 // FindSubmatch: 96 // " Component 1 Density: 64MB" 97 // ^-----------------^:^---------------^^--^ 98 // %s : \s* (\w+) 99 items := []string{ 100 "Component 1 Density", 101 "Component 2 Density", 102 } 103 results := []uint64{} 104 for _, item := range items { 105 re := regexp.MustCompile(fmt.Sprintf("%s:\\s*(\\w+)", item)) 106 matches := re.FindSubmatch([]byte(text)) 107 if len(matches) >= 1 { 108 size, err := StringToSizeMB(string(matches[1])) 109 if err != nil { 110 return []uint64{}, err 111 } 112 results = append(results, size) 113 } else { 114 return []uint64{}, fmt.Errorf("could not find '%s' in ifdtool dump: %w", item, errFailedToDetectRomSize) 115 } 116 } 117 return results, nil 118 } 119 120 // StringToSizeMB parses string and returns size in MB 121 func StringToSizeMB(text string) (uint64, error) { 122 // Check for UNUSED 123 if strings.ToLower(text) == "unused" { 124 return 0, nil 125 } 126 127 // Cleanup string 128 re := regexp.MustCompile(`\s+`) 129 text = string(re.ReplaceAll([]byte(text), []byte(""))) 130 131 // Parse integer 132 reUnits := regexp.MustCompile(`([kMGT])B`) 133 numberString := reUnits.ReplaceAll([]byte(text), []byte("${1}iB")) 134 number, err := humanize.ParseBytes(string(numberString)) 135 if err != nil { 136 return 0, errFailedToDetectRomSize 137 } 138 139 return number, nil 140 } 141 142 // assemble command for ifdtool 143 func ifdtoolCmd(platform string, arguments []string) []string { 144 cmd := []string{ifdtoolPath} 145 if platform != "" { 146 // TODO: Wanted to expand this to --platform 147 // but ifdtool has a bug in this long flag 148 // https://review.coreboot.org/c/coreboot/+/80432 149 cmd = append(cmd, []string{"-p", platform}[:]...) 150 } 151 cmd = append(cmd, arguments[:]...) 152 return cmd 153 } 154 155 // buildFirmware builds coreboot with all blobs and stuff 156 func (opts FirmwareStitchingOpts) buildFirmware(ctx context.Context, client *dagger.Client, dockerfileDirectoryPath string) (*dagger.Container, error) { 157 // Check that all files have unique filenames (they are copied into the same dir) 158 copiedFiles := map[string]string{} 159 for _, entry := range opts.IfdtoolEntries { 160 filename := filepath.Base(entry.Path) 161 if _, ok := copiedFiles[filename]; ok { 162 slog.Error( 163 fmt.Sprintf("File '%s' and '%s' have the same filename", entry.Path, copiedFiles[filename]), 164 slog.String("suggestion", "Each file must have a unique name because they get copied into single directory"), 165 slog.Any("error", os.ErrExist), 166 ) 167 return nil, os.ErrExist 168 } 169 copiedFiles[filename] = entry.Path 170 } 171 172 // Spin up container 173 containerOpts := container.SetupOpts{ 174 ContainerURL: opts.SdkURL, 175 MountContainerDir: ContainerWorkDir, 176 MountHostDir: opts.RepoPath, 177 WorkdirContainer: ContainerWorkDir, 178 } 179 myContainer, err := container.Setup(ctx, client, &containerOpts, dockerfileDirectoryPath) 180 if err != nil { 181 slog.Error( 182 "Failed to start a container", 183 slog.Any("error", err), 184 ) 185 return nil, err 186 } 187 188 // Copy all the files into container 189 pwd, err := os.Getwd() 190 if err != nil { 191 slog.Error( 192 "Could not get working directory, should not happen", 193 slog.String("suggestion", logging.ThisShouldNotHappenMessage), 194 slog.Any("error", err), 195 ) 196 return nil, err 197 } 198 newBaseFilePath := filepath.Join(ContainerWorkDir, filepath.Base(opts.BaseFilePath)) 199 myContainer = myContainer.WithFile( 200 newBaseFilePath, 201 client.Host().File(filepath.Join(pwd, opts.BaseFilePath)), 202 ) 203 oldBaseFilePath := opts.BaseFilePath 204 opts.BaseFilePath = newBaseFilePath 205 for entry := range opts.IfdtoolEntries { 206 newPath := filepath.Join(ContainerWorkDir, filepath.Base(opts.IfdtoolEntries[entry].Path)) 207 myContainer = myContainer.WithFile( 208 newPath, 209 client.Host().File(filepath.Join(pwd, opts.IfdtoolEntries[entry].Path)), 210 ) 211 opts.IfdtoolEntries[entry].Path = newPath 212 } 213 214 // Get the size of image (total size) 215 cmd := ifdtoolCmd(opts.Platform, []string{"--dump", opts.BaseFilePath}) 216 myContainerPrevious := myContainer 217 ifdtoolStdout, err := myContainer.WithExec(cmd).Stdout(ctx) 218 if err != nil { 219 slog.Error( 220 "Failed to dump Intel Firmware Descriptor (IFD)", 221 slog.Any("error", err), 222 ) 223 return myContainerPrevious, err 224 } 225 size, err := ExtractSizeFromString(ifdtoolStdout) 226 if err != nil { 227 slog.Error( 228 "Failed extract size from Intel Firmware Descriptor (IFD)", 229 slog.Any("error", err), 230 ) 231 return nil, err 232 } 233 var totalSize uint64 234 for _, i := range size { 235 totalSize += i 236 } 237 slog.Info( 238 fmt.Sprintf("Intel Firmware Descriptor (IFD) detected size: %s B", humanize.Comma(int64(totalSize))), 239 ) 240 241 // Read the base file 242 baseFile, err := os.ReadFile(oldBaseFilePath) 243 if err != nil { 244 return nil, err 245 } 246 baseFileSize := uint64(len(baseFile)) 247 slog.Info( 248 fmt.Sprintf("Size of '%s': %s B", filepath.Base(oldBaseFilePath), humanize.Comma(int64(baseFileSize))), 249 ) 250 if baseFileSize > totalSize { 251 err = errBaseFileBiggerThanIfd 252 slog.Error( 253 fmt.Sprintf("Provided base_file '%s' is bigger (%s B) than defined in IFD (%s B)", 254 filepath.Base(oldBaseFilePath), 255 humanize.Comma(int64(baseFileSize)), 256 humanize.Comma(int64(totalSize)), 257 ), 258 slog.Any("error", err), 259 ) 260 return nil, err 261 } 262 263 // Take baseFile content and expand it to correct size 264 // fill the empty space with 0xFF 265 blank := make([]byte, totalSize-baseFileSize) 266 for i := range blank { 267 blank[i] = 0xFF 268 } 269 firmwareImage := []byte{} 270 firmwareImage = append(firmwareImage, baseFile[:]...) 271 firmwareImage = append(firmwareImage, blank[:]...) 272 273 imageFilename := fmt.Sprintf("new_%s", filepath.Base(opts.BaseFilePath)) 274 slog.Info( 275 fmt.Sprintf( 276 "File '%s' is being expanded to ROM size %s B as '%s'", 277 filepath.Base(opts.BaseFilePath), 278 humanize.Comma(int64(len(firmwareImage))), 279 imageFilename, 280 ), 281 ) 282 firmwareImageFile, err := os.Create(imageFilename) 283 if err != nil { 284 return nil, err 285 } 286 _, err = firmwareImageFile.Write(firmwareImage) 287 if err != nil { 288 return nil, err 289 } 290 firmwareImageFile.Close() 291 myContainer = myContainer.WithFile( 292 filepath.Join(ContainerWorkDir, imageFilename), 293 client.Host().File(filepath.Join(pwd, imageFilename)), 294 ) 295 296 // Populate regions with ifdtool 297 for entry := range opts.IfdtoolEntries { 298 slog.Info( 299 fmt.Sprintf("Injecting '%s' into '%s' region in '%s'", 300 opts.IfdtoolEntries[entry].Path, 301 opts.IfdtoolEntries[entry].TargetRegion, 302 imageFilename, 303 ), 304 ) 305 306 // Inject binaries 307 cmd := ifdtoolCmd( 308 opts.Platform, 309 []string{ 310 "--inject", 311 fmt.Sprintf("%s:%s", 312 opts.IfdtoolEntries[entry].TargetRegion, 313 opts.IfdtoolEntries[entry].Path), 314 imageFilename, 315 }, 316 ) 317 myContainerPrevious = myContainer 318 myContainer, err = myContainer.WithExec(cmd).Sync(ctx) 319 if err != nil { 320 slog.Error("Failed to inject region") 321 return myContainerPrevious, err 322 } 323 324 // ifdtool makes a new file '<filename>.new' 325 imageFilenameNew := fmt.Sprintf("%s.new", imageFilename) 326 cmd = []string{"mv", "--force", imageFilenameNew, imageFilename} 327 myContainerPrevious = myContainer 328 myContainer, err = myContainer.WithExec(cmd).Sync(ctx) 329 if err != nil { 330 slog.Error( 331 fmt.Sprintf("Failed to rename '%s' to '%s'", imageFilenameNew, imageFilename), 332 ) 333 return myContainerPrevious, err 334 } 335 } 336 337 // Extract artifacts 338 return myContainer, container.GetArtifacts(ctx, myContainer, opts.CommonOpts.GetArtifacts()) 339 }