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  }