Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,15 @@ jobs:
uses: ./
# uses: 9elements/firmware-action
with:
config: 'tests/example_config__coreboot.json'
config: |-
tests/example_config__coreboot.json
tests/example_config__uroot.json
target: 'coreboot-example'
recursive: 'false'
compile: ${{ needs.changes.outputs.compile }}
env:
COREBOOT_VERSION: ${{ matrix.coreboot-version }}
UROOT_VERSION: "dummy"

- name: Get artifacts
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -229,13 +232,16 @@ jobs:
uses: ./
# uses: 9elements/firmware-action
with:
config: 'tests/example_config__linux.json'
config: |-
tests/example_config__uroot.json
tests/example_config__linux.json
target: 'linux-example'
recursive: 'false'
compile: ${{ needs.changes.outputs.compile }}
env:
LINUX_VERSION: ${{ matrix.linux-version }}
SYSTEM_ARCH: ${{ matrix.arch }}
UROOT_VERSION: "dummy"

- name: Get artifacts
uses: actions/upload-artifact@v4
Expand Down
23 changes: 16 additions & 7 deletions cmd/firmware-action/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"log/slog"
"os"
"regexp"
"strings"

"github.com/9elements/firmware-action/cmd/firmware-action/filesystem"
"github.com/9elements/firmware-action/cmd/firmware-action/logging"
Expand Down Expand Up @@ -42,7 +43,7 @@ var CLI struct {
Indent bool `default:"false" help:"enable indentation for JSON output"`
Debug bool `default:"false" help:"increase verbosity"`

Config string `type:"path" required:"" default:"${config_file}" help:"Path to configuration file"`
Config []string `type:"path" required:"" default:"${config_file}" help:"Path to configuration file, supports multiple flags to use multiple configuration files"`

Build struct {
Target string `required:"" help:"Select which target to build, use ID from configuration file"`
Expand Down Expand Up @@ -76,7 +77,7 @@ func run(ctx context.Context) error {
)
slog.Info(
fmt.Sprintf("Running in %s mode", mode),
slog.String("input/config", CLI.Config),
slog.Any("input/config", CLI.Config),
slog.String("input/target", CLI.Build.Target),
slog.Bool("input/recursive", CLI.Build.Recursive),
)
Expand Down Expand Up @@ -108,7 +109,7 @@ func run(ctx context.Context) error {
patterSub := regexp.MustCompile(`^\-[\d\w]* `)
slog.Warn(
"Git submodule seems to be uninitialized",
slog.String("suggestion", "run 'git submodule update --depth 0 --init --recursive --checkout'"),
slog.String("suggestion", "run 'git submodule update --depth 1 --init --recursive --checkout'"),
slog.String("offending_submodule", patterSub.ReplaceAllString(v, "")),
)
}
Expand All @@ -117,7 +118,7 @@ submodule_out:

// Parse configuration file
var myConfig *recipes.Config
myConfig, err = recipes.ReadConfig(CLI.Config)
myConfig, err = recipes.ReadConfigs(CLI.Config)
if err != nil {
return err
}
Expand Down Expand Up @@ -188,8 +189,16 @@ func parseCli() (string, error) {
return mode, nil

case "generate-config":
// Check if at least one configuration file was supplied
if len(CLI.Config) == 0 {
slog.Error(
"No configuration file was supplied",
slog.Any("error", os.ErrNotExist),
)
return "", os.ErrNotExist
}
// Check if config file exists
err := filesystem.CheckFileExists(CLI.Config)
err := filesystem.CheckFileExists(CLI.Config[0])
if !errors.Is(err, os.ErrNotExist) {
// The file exists, or is directory, or some other problem
slog.Error(
Expand Down Expand Up @@ -222,7 +231,7 @@ func parseCli() (string, error) {

// Write to file
slog.Info(fmt.Sprintf("Generating configuration file at: %s", CLI.Config))
if err := os.WriteFile(CLI.Config, jsonString, 0o666); err != nil {
if err := os.WriteFile(CLI.Config[0], jsonString, 0o666); err != nil {
slog.Error(
"Unable to write generated configuration into file",
slog.Any("error", err),
Expand Down Expand Up @@ -253,7 +262,7 @@ func parseGithub() (string, error) {
action := githubactions.New()
regexTrue := regexp.MustCompile(`(?i)true`)

CLI.Config = action.GetInput("config")
CLI.Config = strings.Split(action.GetInput("config"), "\n")
CLI.Build.Target = action.GetInput("target")
CLI.Build.Recursive = regexTrue.MatchString(action.GetInput("recursive"))
CLI.JSON = regexTrue.MatchString(action.GetInput("json"))
Expand Down
113 changes: 93 additions & 20 deletions cmd/firmware-action/recipes/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"log/slog"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"

Expand Down Expand Up @@ -206,30 +207,82 @@ type Config struct {
// AllModules method returns slice with all modules
func (c Config) AllModules() map[string]FirmwareModule {
modules := make(map[string]FirmwareModule)
for key, value := range c.Coreboot {
modules[key] = value
}
for key, value := range c.Linux {
modules[key] = value
}
for key, value := range c.Edk2 {
modules[key] = value
}
for key, value := range c.FirmwareStitching {
modules[key] = value
}
for key, value := range c.URoot {
modules[key] = value
}
for key, value := range c.Universal {
modules[key] = value
}
for key, value := range c.UBoot {
modules[key] = value

configValue := reflect.ValueOf(c)

for i := range configValue.Type().NumField() {
fieldValue := configValue.Field(i)

// Check if the field is a map.
if fieldValue.Kind() == reflect.Map {
// Iterate over the keys in the map.
for _, key := range fieldValue.MapKeys() {
value := fieldValue.MapIndex(key)

// Type-assert the value to FirmwareModule.
if module, ok := value.Interface().(FirmwareModule); ok {
modules[key.String()] = module
} else {
slog.Error(
fmt.Sprintf("Value for key '%v' in config does not implement FirmwareModule", key),
slog.String("suggestion", logging.ThisShouldNotHappenMessage),
)
}
}
}
}

return modules
}

// Merge method will take other Config instance and adopt all of its modules
func (c Config) Merge(other Config) (Config, error) {
merged := Config{}

// Use reflection on the merged instance.
vMerged := reflect.ValueOf(&merged).Elem()
vC := reflect.ValueOf(c)
vOther := reflect.ValueOf(other)
t := vMerged.Type()

// Iterate over all fields of the struct.
for i := range t.NumField() {
fieldType := t.Field(i)
// Process only map fields.
if fieldType.Type.Kind() == reflect.Map {
// Create a new map for the merged result.
mergedMap := reflect.MakeMap(fieldType.Type)

// Get the map from c (receiver) and copy its key/value pairs.
mapC := vC.Field(i)
if mapC.IsValid() && !mapC.IsNil() {
for _, key := range mapC.MapKeys() {
mergedMap.SetMapIndex(key, mapC.MapIndex(key))
}
}

// Get the map from other and merge its entries.
mapOther := vOther.Field(i)
if mapOther.IsValid() && !mapOther.IsNil() {
for _, key := range mapOther.MapKeys() {
// If the key already exists, print a warning.
if existing := mergedMap.MapIndex(key); existing.IsValid() {
fmt.Printf("Warning: overriding key %v in field %s\n", key, fieldType.Name)
}
mergedMap.SetMapIndex(key, mapOther.MapIndex(key))
}
}
// Set the merged map into the new struct.
vMerged.Field(i).Set(mergedMap)
} else {
// For non-map fields, just copy the value from c.
vMerged.Field(i).Set(vC.Field(i))
}
}

return merged, nil
}

// FirmwareModule interface
type FirmwareModule interface {
GetDepends() []string
Expand Down Expand Up @@ -273,6 +326,26 @@ func FindAllEnvVars(text string) []string {
return result
}

// ReadConfigs is for reading and parsing multiple JSON configuration files into single Config struct
func ReadConfigs(filepaths []string) (*Config, error) {
var allConfigs Config
for _, filepath := range filepaths {
trimmedFilepath := strings.TrimSpace(filepath)
slog.Debug("Reading config",
slog.String("path", trimmedFilepath),
)
payload, err := ReadConfig(trimmedFilepath)
if err != nil {
return nil, err
}
allConfigs, err = allConfigs.Merge(*payload)
if err != nil {
return nil, err
}
}
return &allConfigs, nil
}

// ReadConfig is for reading and parsing JSON configuration file into Config struct
func ReadConfig(filepath string) (*Config, error) {
// Read JSON file
Expand Down
136 changes: 136 additions & 0 deletions cmd/firmware-action/recipes/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,142 @@ import (
"github.com/stretchr/testify/assert"
)

func TestAllModules(t *testing.T) {
testCases := []struct {
name string
opts Config
wantModules map[string]FirmwareModule
}{
{
name: "empty",
opts: Config{},
wantModules: map[string]FirmwareModule{},
},
{
name: "simple",
opts: Config{
Coreboot: map[string]CorebootOpts{
"coreboot-A": {},
},
},
wantModules: map[string]FirmwareModule{
"coreboot-A": CorebootOpts{},
},
},
{
name: "more complex",
opts: Config{
Coreboot: map[string]CorebootOpts{
"coreboot-A": {
DefconfigPath: "dummy",
},
},
Linux: map[string]LinuxOpts{
"linux-A": {
DefconfigPath: "dummy",
},
},
},
wantModules: map[string]FirmwareModule{
"coreboot-A": CorebootOpts{
DefconfigPath: "dummy",
},
"linux-A": LinuxOpts{
DefconfigPath: "dummy",
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
allModules := tc.opts.AllModules()
assert.True(t, reflect.DeepEqual(tc.wantModules, allModules))
})
}
}

func TestMerge(t *testing.T) {
testCases := []struct {
name string
optsA Config
optsB Config
wantConfig Config
wantErr error
}{
{
name: "empty",
optsA: Config{},
optsB: Config{},
wantConfig: Config{},
wantErr: nil,
},
{
name: "simple",
optsA: Config{
Coreboot: map[string]CorebootOpts{
"coreboot-A": {},
},
},
optsB: Config{},
wantConfig: Config{
Coreboot: map[string]CorebootOpts{
"coreboot-A": {},
},
},
wantErr: nil,
},
{
name: "more complex",
optsA: Config{
Coreboot: map[string]CorebootOpts{
"coreboot-A": {
DefconfigPath: "dummy",
},
},
},
optsB: Config{
Linux: map[string]LinuxOpts{
"linux-A": {
DefconfigPath: "dummy",
},
},
},
wantConfig: Config{
Coreboot: map[string]CorebootOpts{
"coreboot-A": {
DefconfigPath: "dummy",
},
},
Linux: map[string]LinuxOpts{
"linux-A": {
DefconfigPath: "dummy",
},
},
},
wantErr: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
bigConfig, err := tc.optsA.Merge(tc.optsB)

// Initialize empty maps in wantConfig so that deep equal works correctly
wantConfigValue := reflect.ValueOf(&tc.wantConfig).Elem()
for i := 0; i < wantConfigValue.NumField(); i++ {
fieldValue := wantConfigValue.Field(i)
if fieldValue.Kind() == reflect.Map && fieldValue.IsNil() {
fieldValue.Set(reflect.MakeMap(fieldValue.Type()))
}
}

// Compare the merged config with the expected one
assert.Equal(t, tc.wantConfig, bigConfig)
assert.True(t, reflect.DeepEqual(tc.wantConfig, bigConfig))
assert.ErrorIs(t, err, tc.wantErr)
})
}
}

func TestValidateConfig(t *testing.T) {
commonDummy := CommonOpts{
SdkURL: "ghcr.io/9elements/firmware-action/coreboot_4.19:main",
Expand Down
Loading
Loading