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 }