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  }