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

     1  // SPDX-License-Identifier: MIT
     2  
     3  // Package recipes / linux
     4  package recipes
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"log/slog"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  
    16  	"dagger.io/dagger"
    17  	"github.com/9elements/firmware-action/action/container"
    18  	"github.com/9elements/firmware-action/action/logging"
    19  )
    20  
    21  var errUnknownArchCrossCompile = errors.New("unknown architecture for cross-compilation")
    22  
    23  // LinuxSpecific is used to store data specific to linux
    24  // ANCHOR: LinuxSpecific
    25  type LinuxSpecific struct {
    26  	// TODO: either use or remove
    27  	GccVersion string `json:"gcc_version"`
    28  }
    29  
    30  // ANCHOR_END: LinuxSpecific
    31  
    32  // LinuxOpts is used to store all data needed to build linux
    33  type LinuxOpts struct {
    34  	// List of IDs this instance depends on
    35  	// Example: [ "MyLittleCoreboot", "MyLittleEdk2"]
    36  	Depends []string `json:"depends"`
    37  
    38  	// Common options like paths etc.
    39  	CommonOpts
    40  
    41  	// Specifies target architecture, such as 'x86' or 'arm64'.
    42  	// Supported options:
    43  	//   - 'x86'
    44  	//   - 'x86_64'
    45  	//   - 'arm'
    46  	//   - 'arm64'
    47  	Arch string `json:"arch"`
    48  
    49  	// Gives the (relative) path to the defconfig that should be used to build the target.
    50  	DefconfigPath string `json:"defconfig_path" validate:"required,filepath"`
    51  
    52  	// Coreboot specific options
    53  	LinuxSpecific
    54  }
    55  
    56  // GetDepends is used to return list of dependencies
    57  func (opts LinuxOpts) GetDepends() []string {
    58  	return opts.Depends
    59  }
    60  
    61  // GetArtifacts returns list of wanted artifacts from container
    62  func (opts LinuxOpts) GetArtifacts() *[]container.Artifacts {
    63  	return opts.CommonOpts.GetArtifacts()
    64  }
    65  
    66  // buildFirmware builds linux kernel
    67  //
    68  //	docs: https://www.kernel.org/doc/html/latest/kbuild/index.html
    69  func (opts LinuxOpts) buildFirmware(ctx context.Context, client *dagger.Client, dockerfileDirectoryPath string) (*dagger.Container, error) {
    70  	// Spin up container
    71  	containerOpts := container.SetupOpts{
    72  		ContainerURL:      opts.SdkURL,
    73  		MountContainerDir: ContainerWorkDir,
    74  		MountHostDir:      opts.RepoPath,
    75  		WorkdirContainer:  ContainerWorkDir,
    76  	}
    77  	myContainer, err := container.Setup(ctx, client, &containerOpts, dockerfileDirectoryPath)
    78  	if err != nil {
    79  		slog.Error(
    80  			"Failed to start a container",
    81  			slog.Any("error", err),
    82  		)
    83  		return nil, err
    84  	}
    85  
    86  	// Copy over the defconfig file
    87  	defconfigBasename := filepath.Base(opts.DefconfigPath)
    88  	if strings.Contains(defconfigBasename, ".defconfig") {
    89  		// 'make $defconfigBasename' will fail for Linux kernel if the $defconfigBasename
    90  		// contains '.defconfig' string ...
    91  		// it will just fail with generic error (defconfigBasename="linux.defconfig"):
    92  		//   make[1]: *** No rule to make target 'linux.defconfig'.  Stop.
    93  		//   make: *** [Makefile:704: linux.defconfig] Error 2
    94  		// but defconfigBasename="linux_defconfig" works fine
    95  		// Don't know why, just return error and let user deal with it.
    96  		return nil, fmt.Errorf(
    97  			"filename '%s' specified by defconfig_path must not contain '.defconfig' in the name",
    98  			defconfigBasename,
    99  		)
   100  	}
   101  	//   not sure why, but without the 'pwd' I am getting different results between CI and 'go test'
   102  	pwd, err := os.Getwd()
   103  	if err != nil {
   104  		slog.Error(
   105  			"Could not get working directory, should not happen",
   106  			slog.String("suggestion", logging.ThisShouldNotHappenMessage),
   107  			slog.Any("error", err),
   108  		)
   109  		return nil, err
   110  	}
   111  	myContainer = myContainer.WithFile(
   112  		filepath.Join(ContainerWorkDir, defconfigBasename),
   113  		client.Host().File(filepath.Join(pwd, opts.DefconfigPath)),
   114  	)
   115  
   116  	// Setup environment variables in the container
   117  	//   Handle cross-compilation: Map architecture to cross-compiler
   118  	crossCompile := map[string]string{
   119  		"x86":    "i686-linux-gnu-",
   120  		"x86_64": "",
   121  		"arm":    "arm-linux-gnueabi-",
   122  		"arm64":  "aarch64-linux-gnu-",
   123  	}
   124  	envVars := map[string]string{
   125  		"ARCH": opts.Arch,
   126  	}
   127  
   128  	val, ok := crossCompile[opts.Arch]
   129  	if !ok {
   130  		err = errUnknownArchCrossCompile
   131  		slog.Error(
   132  			"Selected unknown cross compilation target architecture",
   133  			slog.Any("error", err),
   134  		)
   135  		return nil, err
   136  	}
   137  	if val != "" {
   138  		envVars["CROSS_COMPILE"] = val
   139  	}
   140  
   141  	for key, value := range envVars {
   142  		myContainer = myContainer.WithEnvVariable(key, value)
   143  	}
   144  
   145  	// Assemble commands to build
   146  	// TODO: make independent on OS
   147  	buildSteps := [][]string{
   148  		// remove existing config if exists
   149  		//   -f: ignore nonexistent files
   150  		{"rm", "-f", ".config"},
   151  		// x86_64 reuses x86
   152  		{"ln", "--symbolic", "--relative", "arch/x86", "arch/x86_64"},
   153  		// the symlink simplifies this command
   154  		{"cp", defconfigBasename, fmt.Sprintf("arch/%s/configs/%s", opts.Arch, defconfigBasename)},
   155  		// generate dotconfig from defconfig
   156  		{"make", defconfigBasename},
   157  		// compile
   158  		{"make", "-j", fmt.Sprintf("%d", runtime.NumCPU())},
   159  		// for documenting purposes
   160  		{"make", "savedefconfig"},
   161  	}
   162  
   163  	// Execute build commands
   164  	var myContainerPrevious *dagger.Container
   165  	for step := range buildSteps {
   166  		myContainerPrevious = myContainer
   167  		myContainer, err = myContainer.
   168  			WithExec(buildSteps[step]).
   169  			Sync(ctx)
   170  		if err != nil {
   171  			slog.Error(
   172  				"Failed to build linux",
   173  				slog.Any("error", err),
   174  			)
   175  			return myContainerPrevious, fmt.Errorf("linux build failed: %w", err)
   176  		}
   177  	}
   178  
   179  	// Extract artifacts
   180  	return myContainer, container.GetArtifacts(ctx, myContainer, opts.GetArtifacts())
   181  }