github.com/9elements/firmware-action/action@v0.0.0-20240514065043-044ed91c9ed8/main.go (about) 1 // SPDX-License-Identifier: MIT 2 3 // Package main implements the core logic of running composable Dagger pipelines 4 // Documentation [is hosted in GitHub pages](https://9elements.github.io/firmware-action/) 5 package main 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "log/slog" 13 "os" 14 "regexp" 15 16 "github.com/9elements/firmware-action/action/filesystem" 17 "github.com/9elements/firmware-action/action/logging" 18 "github.com/9elements/firmware-action/action/recipes" 19 "github.com/alecthomas/kong" 20 "github.com/sethvargo/go-githubactions" 21 ) 22 23 func main() { 24 logging.InitLogger(slog.LevelInfo) 25 26 if err := run(context.Background()); err != nil { 27 slog.Error( 28 "firmware-action failed", 29 slog.Any("error", err), 30 ) 31 os.Exit(1) 32 } 33 } 34 35 const firmwareActionVersion = "v0.2.0" 36 37 // CLI (Command Line Interface) holds data from environment 38 var CLI struct { 39 JSON bool `default:"false" help:"switch to JSON stdout and stderr output"` 40 Indent bool `default:"false" help:"enable indentation for JSON output"` 41 Debug bool `default:"false" help:"increase verbosity"` 42 43 Config string `type:"path" required:"" default:"${config_file}" help:"Path to configuration file"` 44 45 Build struct { 46 Target string `required:"" help:"Select which target to build, use ID from configuration file"` 47 Recursive bool `help:"Build recursively with all dependencies and payloads"` 48 Interactive bool `help:"Open interactive SSH into container if build fails"` 49 } `cmd:"build" help:"Build a target defined in configuration file"` 50 51 GenerateConfig struct{} `cmd:"generate-config" help:"Generate empty configuration file"` 52 Version struct{} `cmd:"version" help:"Print version and exit"` 53 } 54 55 func run(ctx context.Context) error { 56 // Get arguments 57 mode, err := getInputsFromEnvironment() 58 if err != nil { 59 return err 60 } 61 if mode == "" { 62 // Exit on "version" or "generate-config" 63 return nil 64 } 65 66 // Properly initialize logging 67 level := slog.LevelInfo 68 if CLI.Debug { 69 level = slog.LevelDebug 70 } 71 logging.InitLogger( 72 level, 73 logging.WithJSON(CLI.JSON), 74 logging.WithIndent(CLI.Indent), 75 ) 76 slog.Info( 77 fmt.Sprintf("Running in %s mode", mode), 78 slog.String("input/config", CLI.Config), 79 slog.String("input/target", CLI.Build.Target), 80 slog.Bool("input/recursive", CLI.Build.Recursive), 81 slog.Bool("input/interactive", CLI.Build.Interactive), 82 ) 83 84 // Parse configuration file 85 var myConfig *recipes.Config 86 myConfig, err = recipes.ReadConfig(CLI.Config) 87 if err != nil { 88 return err 89 } 90 91 // Lets build stuff 92 _, err = recipes.Build( 93 ctx, 94 CLI.Build.Target, 95 CLI.Build.Recursive, 96 CLI.Build.Interactive, 97 myConfig, 98 recipes.Execute, 99 ) 100 return err 101 } 102 103 func getInputsFromEnvironment() (string, error) { 104 // Check for GitHub 105 // https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables 106 _, exists := os.LookupEnv("GITHUB_ACTIONS") 107 if exists { 108 return parseGithub() 109 } 110 111 // Check for GitLab, ... (possibly add other CIs) 112 // TODO 113 114 // Use command line interface 115 return parseCli() 116 } 117 118 func parseCli() (string, error) { 119 // Get inputs from command line options 120 ctx := kong.Parse( 121 &CLI, 122 kong.Description("Utility to create firmware images for several open source firmware solutions"), 123 kong.UsageOnError(), 124 kong.Vars{ 125 "config_file": "firmware-action.json", 126 }, 127 ) 128 mode := "CLI" 129 130 switch ctx.Command() { 131 case "build": 132 // This is handled elsewhere 133 return mode, nil 134 135 case "generate-config": 136 // Check if config file exists 137 err := filesystem.CheckFileExists(CLI.Config) 138 if !errors.Is(err, os.ErrNotExist) { 139 // The file exists, or is directory, or some other problem 140 slog.Error( 141 fmt.Sprintf("Can't generate configuration file at: %s", CLI.Config), 142 slog.Any("error", err), 143 ) 144 return "", err 145 } 146 147 // Create empty config 148 myConfig := recipes.Config{ 149 Coreboot: map[string]recipes.CorebootOpts{"coreboot-example": {}}, 150 Linux: map[string]recipes.LinuxOpts{"linux-example": {}}, 151 Edk2: map[string]recipes.Edk2Opts{"edk2-example": {}}, 152 FirmwareStitching: map[string]recipes.FirmwareStitchingOpts{"stitching-example": {}}, 153 } 154 155 // Convert to JSON 156 jsonString, err := json.MarshalIndent(myConfig, "", " ") 157 if err != nil { 158 slog.Error( 159 "Unable to convert the config struct into a JSON string", 160 slog.String("suggestion", logging.ThisShouldNotHappenMessage), 161 slog.Any("error", err), 162 ) 163 return "", err 164 } 165 166 // Write to file 167 slog.Info(fmt.Sprintf("Generating configuration file at: %s", CLI.Config)) 168 if err := os.WriteFile(CLI.Config, jsonString, 0o666); err != nil { 169 slog.Error( 170 "Unable to write generated configuration into file", 171 slog.Any("error", err), 172 ) 173 return "", err 174 } 175 return "", nil 176 177 case "version": 178 // Print version and exit 179 fmt.Println(firmwareActionVersion) 180 return "", nil 181 182 default: 183 // This should not happen 184 err := errors.New("unsupported command") 185 slog.Error( 186 "Supplied unsupported command", 187 slog.String("suggestion", logging.ThisShouldNotHappenMessage), 188 slog.Any("error", err), 189 ) 190 return mode, err 191 } 192 } 193 194 func parseGithub() (string, error) { 195 // Get inputs from GitHub environment 196 action := githubactions.New() 197 regexTrue := regexp.MustCompile(`(?i)true`) 198 199 CLI.Config = action.GetInput("config") 200 CLI.Build.Target = action.GetInput("target") 201 CLI.Build.Recursive = regexTrue.MatchString(action.GetInput("recursive")) 202 CLI.JSON = regexTrue.MatchString(action.GetInput("json")) 203 204 return "GitHub", nil 205 }