github.com/9elements/firmware-action/action@v0.0.0-20240514065043-044ed91c9ed8/recipes/coreboot.go (about)

     1  // SPDX-License-Identifier: MIT
     2  
     3  // Package recipes / coreboot
     4  package recipes
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"log/slog"
    11  	"os"
    12  	"path/filepath"
    13  	"reflect"
    14  	"runtime"
    15  	"strings"
    16  
    17  	"dagger.io/dagger"
    18  	"github.com/9elements/firmware-action/action/container"
    19  	"github.com/9elements/firmware-action/action/filesystem"
    20  	"github.com/9elements/firmware-action/action/logging"
    21  )
    22  
    23  // BlobDef is used to store information about a single blob.
    24  // This structure is not exposed to the user, it is filled in automatically based on user input.
    25  type BlobDef struct {
    26  	// Path to the blob (either file or directory)
    27  	Path string `validate:"required"`
    28  
    29  	// Blobs get renamed when moved to this string
    30  	DestinationFilename string `validate:"required"`
    31  
    32  	// Kconfig key specifying the filepath to the blob in defconfig
    33  	KconfigKey string `validate:"required"`
    34  
    35  	// Is blob a directory? If blob is file, set to FALSE
    36  	IsDirectory bool `validate:"required,boolean"`
    37  }
    38  
    39  // CorebootBlobs is used to store data specific to coreboot.
    40  // ANCHOR: CorebootBlobs
    41  type CorebootBlobs struct {
    42  	// ** List of supported blobs **
    43  	// NOTE: The blobs may not be added to the ROM, depends on provided defconfig.
    44  	//
    45  	// Gives the (relative) path to the payload.
    46  	// In a 'coreboot' build, the file will be placed at
    47  	//   `3rdparty/blobs/mainboard/$(MAINBOARDDIR)/payload`.
    48  	// The Kconfig `CONFIG_PAYLOAD_FILE` will be changed to point to the same path.
    49  	PayloadFilePath string `json:"payload_file_path" type:"blob"`
    50  
    51  	// Gives the (relative) path to the Intel Flash descriptor binary.
    52  	// In a 'coreboot' build, the file will be placed at
    53  	//   `3rdparty/blobs/mainboard/$(CONFIG_MAINBOARD_DIR)/descriptor.bin`.
    54  	// The Kconfig `CONFIG_IFD_BIN_PATH` will be changed to point to the same path.
    55  	IntelIfdPath string `json:"intel_ifd_path" type:"blob"`
    56  
    57  	// Gives the (relative) path to the Intel Management engine binary.
    58  	// In a 'coreboot' build, the file will be placed at
    59  	//   `3rdparty/blobs/mainboard/$(CONFIG_MAINBOARD_DIR)/me.bin`.
    60  	// The Kconfig `CONFIG_ME_BIN_PATH` will be changed to point to the same path.
    61  	IntelMePath string `json:"intel_me_path" type:"blob"`
    62  
    63  	// Gives the (relative) path to the Intel Gigabit Ethernet engine binary.
    64  	// In a 'coreboot' build, the file will be placed at
    65  	//   `3rdparty/blobs/mainboard/$(CONFIG_MAINBOARD_DIR)/gbe.bin`.
    66  	// The Kconfig `CONFIG_GBE_BIN_PATH` will be changed to point to the same path.
    67  	IntelGbePath string `json:"intel_gbe_path" type:"blob"`
    68  
    69  	// Gives the (relative) path to the Intel FSP binary.
    70  	// In a 'coreboot' build, the file will be placed at
    71  	//   `3rdparty/blobs/mainboard/$(CONFIG_MAINBOARD_DIR)/Fsp.fd`.
    72  	// The Kconfig `CONFIG_FSP_FD_PATH` will be changed to point to the same path.
    73  	FspBinaryPath string `json:"fsp_binary_path" type:"blob"`
    74  
    75  	// Gives the (relative) path to the Intel FSP header folder.
    76  	// In a 'coreboot' build, the files will be placed at
    77  	//   `3rdparty/blobs/mainboard/$(CONFIG_MAINBOARD_DIR)/Include`.
    78  	// The Kconfig `CONFIG_FSP_HEADER_PATH` will be changed to point to the same path.
    79  	FspHeaderPath string `json:"fsp_header_path" type:"blob"`
    80  
    81  	// Gives the (relative) path to the Video BIOS Table binary.
    82  	// In a 'coreboot' build, the files will be placed at
    83  	//   `3rdparty/blobs/mainboard/$(CONFIG_MAINBOARD_DIR)/vbt.bin`.
    84  	// The Kconfig `CONFIG_INTEL_GMA_VBT_FILE` will be changed to point to the same path.
    85  	VbtPath string `json:"vbt_path" type:"blob"`
    86  
    87  	// Gives the (relative) path to the Embedded Controller binary.
    88  	// In a 'coreboot' build, the files will be placed at
    89  	//   `3rdparty/blobs/mainboard/$(CONFIG_MAINBOARD_DIR)/ec.bin`.
    90  	// The Kconfig `CONFIG_EC_BIN_PATH` will be changed to point to the same path.
    91  	EcPath string `json:"ec_path" type:"blob"`
    92  }
    93  
    94  // ANCHOR_END: CorebootBlobs
    95  
    96  // CorebootOpts is used to store all data needed to build coreboot.
    97  // ANCHOR: CorebootOpts
    98  type CorebootOpts struct {
    99  	// List of IDs this instance depends on
   100  	Depends []string `json:"depends"`
   101  
   102  	// Common options like paths etc.
   103  	CommonOpts
   104  
   105  	// Gives the (relative) path to the defconfig that should be used to build the target.
   106  	DefconfigPath string `json:"defconfig_path" validate:"required,filepath"`
   107  
   108  	// Coreboot specific options
   109  	Blobs CorebootBlobs `json:"blobs"`
   110  }
   111  
   112  // ANCHOR_END: CorebootOpts
   113  
   114  // GetDepends is used to return list of dependencies
   115  func (opts CorebootOpts) GetDepends() []string {
   116  	return opts.Depends
   117  }
   118  
   119  // GetArtifacts returns list of wanted artifacts from container
   120  func (opts CorebootOpts) GetArtifacts() *[]container.Artifacts {
   121  	return opts.CommonOpts.GetArtifacts()
   122  }
   123  
   124  // corebootProcessBlobs is used to fill figure out blobs from provided data.
   125  func corebootProcessBlobs(opts CorebootBlobs) ([]BlobDef, error) {
   126  	blobMap := map[string]BlobDef{
   127  		// Payload
   128  		// docs: https://doc.coreboot.org/payloads.html
   129  		"payload_file_path": {
   130  			DestinationFilename: "payload",
   131  			KconfigKey:          "CONFIG_PAYLOAD_FILE",
   132  			IsDirectory:         false,
   133  		},
   134  		// Intel IFD (Intel Flash Descriptor)
   135  		// docs: https://doc.coreboot.org/util/ifdtool/layout.html
   136  		"intel_ifd_path": {
   137  			DestinationFilename: "descriptor.bin",
   138  			KconfigKey:          "CONFIG_IFD_BIN_PATH",
   139  			IsDirectory:         false,
   140  		},
   141  		// Intel ME (Intel Management Engine)
   142  		"intel_me_path": {
   143  			DestinationFilename: "me.bin",
   144  			KconfigKey:          "CONFIG_ME_BIN_PATH",
   145  			IsDirectory:         false,
   146  		},
   147  		// Intel GbE (Intel Gigabit Ethernet)
   148  		"intel_gbe_path": {
   149  			DestinationFilename: "gbe.bin",
   150  			KconfigKey:          "CONFIG_GBE_BIN_PATH",
   151  			IsDirectory:         false,
   152  		},
   153  		// Intel FSP binary (Intel Firmware Support Package)
   154  		"fsp_binary_path": {
   155  			DestinationFilename: "Fsp.fd",
   156  			KconfigKey:          "CONFIG_FSP_FD_PATH",
   157  			IsDirectory:         false,
   158  		},
   159  		// Intel FSP header (Intel Firmware Support Package)
   160  		"fsp_header_path": {
   161  			DestinationFilename: "Include",
   162  			KconfigKey:          "CONFIG_FSP_HEADER_PATH",
   163  			IsDirectory:         true,
   164  		},
   165  		// VBT (Video BIOS Table)
   166  		"vbt_path": {
   167  			DestinationFilename: "vbt.bin",
   168  			KconfigKey:          "CONFIG_INTEL_GMA_VBT_FILE",
   169  			IsDirectory:         false,
   170  		},
   171  		// EC (Embedded Controller)
   172  		"ec_path": {
   173  			DestinationFilename: "ec.bin",
   174  			KconfigKey:          "CONFIG_EC_BIN_PATH",
   175  			IsDirectory:         false,
   176  		},
   177  	}
   178  	blobs := []BlobDef{}
   179  
   180  	blob := reflect.ValueOf(opts)
   181  	for i := 0; i < blob.Type().NumField(); i++ {
   182  		t := blob.Type().Field(i)
   183  
   184  		jsonTag := t.Tag.Get("json")
   185  		jsonType := t.Tag.Get("type")
   186  		if jsonTag != "" && jsonType == "blob" {
   187  			newBlob := blobMap[jsonTag]
   188  			newBlob.Path = blob.Field(i).Interface().(string)
   189  			if newBlob.Path != "" {
   190  				blobs = append(blobs, newBlob)
   191  			}
   192  		}
   193  	}
   194  	return blobs, nil
   195  }
   196  
   197  // buildFirmware builds coreboot with all blobs and stuff
   198  func (opts CorebootOpts) buildFirmware(ctx context.Context, client *dagger.Client, dockerfileDirectoryPath string) (*dagger.Container, error) {
   199  	// Spin up container
   200  	containerOpts := container.SetupOpts{
   201  		ContainerURL:      opts.SdkURL,
   202  		MountContainerDir: ContainerWorkDir,
   203  		MountHostDir:      opts.RepoPath,
   204  		WorkdirContainer:  ContainerWorkDir,
   205  	}
   206  	myContainer, err := container.Setup(ctx, client, &containerOpts, dockerfileDirectoryPath)
   207  	if err != nil {
   208  		slog.Error(
   209  			"Failed to start a container",
   210  			slog.Any("error", err),
   211  		)
   212  		return nil, err
   213  	}
   214  
   215  	// Copy over the defconfig file
   216  	defconfigBasename := filepath.Base(opts.DefconfigPath)
   217  	//   not sure why, but without the 'pwd' I am getting different results between CI and 'go test'
   218  	pwd, err := os.Getwd()
   219  	if err != nil {
   220  		slog.Error(
   221  			"Could not get working directory, should not happen",
   222  			slog.String("suggestion", logging.ThisShouldNotHappenMessage),
   223  			slog.Any("error", err),
   224  		)
   225  		return nil, err
   226  	}
   227  	myContainer = myContainer.WithFile(
   228  		filepath.Join(ContainerWorkDir, defconfigBasename),
   229  		client.Host().File(filepath.Join(pwd, opts.DefconfigPath)),
   230  	)
   231  
   232  	// Get value of CONFIG_MAINBOARD_DIR / MAINBOARD_DIR variable from dotconfig
   233  	//   to extract value of 'CONFIG_MAINBOARD_DIR', there must be '.config'
   234  	generateDotConfigCmd := []string{"make", fmt.Sprintf("KBUILD_DEFCONFIG=%s", defconfigBasename), "defconfig"}
   235  	myContainerPrevious := myContainer
   236  	mainboardDir, err := myContainer.
   237  		WithExec(generateDotConfigCmd).
   238  		WithExec([]string{"./util/scripts/config", "-s", "CONFIG_MAINBOARD_DIR"}).
   239  		Stdout(ctx)
   240  	if err != nil {
   241  		slog.Error(
   242  			"Failed to get value of MAINBOARD_DIR from .config",
   243  			slog.Any("error", err),
   244  		)
   245  		return myContainerPrevious, err
   246  	}
   247  	//   strip newline from mainboardDir
   248  	mainboardDir = strings.Replace(mainboardDir, "\n", "", -1)
   249  
   250  	// Assemble commands to build
   251  	buildSteps := [][]string{
   252  		// remove existing config if exists
   253  		// -f: ignore nonexistent files
   254  		{"rm", "-f", ".config"},
   255  		// generate dotconfig from defconfig
   256  		generateDotConfigCmd,
   257  	}
   258  
   259  	// Handle blobs
   260  	// Firstly copy all the blobs into building container.
   261  	// Then use './util/scripts/config' script in coreboot repository to update configuration
   262  	//   options for said blobs (this must run inside container).
   263  	blobs, err := corebootProcessBlobs(opts.Blobs)
   264  	if err != nil {
   265  		slog.Error(
   266  			"Failed to process all blobs",
   267  			slog.Any("error", err),
   268  		)
   269  		return nil, err
   270  	}
   271  	for blob := range blobs {
   272  		// Path to local file on host
   273  		src := filepath.Join(
   274  			pwd,
   275  			blobs[blob].Path,
   276  		)
   277  		// Path to file in container
   278  		dst := filepath.Join(
   279  			filepath.Join("3rdparty/blobs/mainboard", mainboardDir),
   280  			blobs[blob].DestinationFilename,
   281  		)
   282  
   283  		// Copy into container
   284  		if err = filesystem.CheckFileExists(src); !errors.Is(err, os.ErrExist) {
   285  			return nil, err
   286  		}
   287  		if blobs[blob].IsDirectory {
   288  			// Directory
   289  			slog.Info(fmt.Sprintf("Copying directory '%s' to container at '%s'", src, dst))
   290  			myContainer = myContainer.WithExec([]string{"mkdir", "-p", dst})
   291  			// myContainer = myContainer.WithMountedDirectory(
   292  			// can't use WithMountedDirectory because the repo (aka working directory)
   293  			//   is already mounted with WithMountedDirectory
   294  			//   this nesting causes problems
   295  			myContainer = myContainer.WithDirectory(
   296  				dst,
   297  				client.Host().Directory(src),
   298  			)
   299  		} else {
   300  			// File
   301  			myContainer = myContainer.WithFile(
   302  				dst,
   303  				client.Host().File(src),
   304  			)
   305  		}
   306  
   307  		// Fix defconfig
   308  		buildSteps = append(
   309  			buildSteps,
   310  			// update coreboot config value related to blob to actual path of the blob
   311  			[]string{"./util/scripts/config", "--set-str", blobs[blob].KconfigKey, dst},
   312  		)
   313  	}
   314  
   315  	buildSteps = append(
   316  		buildSteps,
   317  		// compile
   318  		[]string{"make", "-j", fmt.Sprintf("%d", runtime.NumCPU())},
   319  		// for documenting purposes
   320  		[]string{"make", "savedefconfig"},
   321  	)
   322  
   323  	// Setup environment variables in the container
   324  	envVars := map[string]string{}
   325  	for key, value := range envVars {
   326  		myContainer = myContainer.WithEnvVariable(key, value)
   327  	}
   328  
   329  	// Build
   330  	for step := range buildSteps {
   331  		myContainerPrevious := myContainer
   332  		myContainer, err = myContainer.
   333  			WithExec(buildSteps[step]).
   334  			Sync(ctx)
   335  		if err != nil {
   336  			slog.Error(
   337  				"Failed to build coreboot",
   338  				slog.Any("error", err),
   339  			)
   340  			return myContainerPrevious, fmt.Errorf("coreboot build failed: %w", err)
   341  		}
   342  	}
   343  
   344  	// Extract artifacts
   345  	return myContainer, container.GetArtifacts(ctx, myContainer, opts.CommonOpts.GetArtifacts())
   346  }