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

     1  // SPDX-License-Identifier: MIT
     2  
     3  // Package recipes yay!
     4  package recipes
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"log/slog"
    11  	"os"
    12  	"slices"
    13  	"sync"
    14  
    15  	"dagger.io/dagger"
    16  	"github.com/9elements/firmware-action/action/container"
    17  	"github.com/heimdalr/dag"
    18  )
    19  
    20  // Errors for recipes
    21  var (
    22  	ErrFailedValidation          = errors.New("config failed validation")
    23  	ErrTargetMissing             = errors.New("no target specified")
    24  	ErrTargetInvalid             = errors.New("unsupported target")
    25  	ErrBuildFailed               = errors.New("build failed")
    26  	ErrDependencyTreeUndefDep    = errors.New("module has invalid dependency")
    27  	ErrDependencyTreeUnderTarget = errors.New("target not found in dependency tree")
    28  )
    29  
    30  // ContainerWorkDir specifies directory in container used as work directory
    31  var ContainerWorkDir = "/workdir"
    32  
    33  func forestAddVertex(forest *dag.DAG, key string, value FirmwareModule, dependencies [][]string) ([][]string, error) {
    34  	err := forest.AddVertexByID(key, key)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  	for _, dep := range value.GetDepends() {
    39  		dependencies = append(dependencies, []string{key, dep})
    40  	}
    41  	return dependencies, nil
    42  }
    43  
    44  // Build recipes, possibly recursively
    45  func Build(
    46  	ctx context.Context,
    47  	target string,
    48  	recursive bool,
    49  	interactive bool,
    50  	config *Config,
    51  	executor func(context.Context, string, *Config, bool) error,
    52  ) ([]string, error) {
    53  	dependencyForest := dag.NewDAG()
    54  	dependencies := [][]string{}
    55  	var err error
    56  
    57  	// Create the forest (forest = multiple independent trees)
    58  	//   Add all items as vertexes into the tree
    59  	for key, value := range config.AllModules() {
    60  		dependencies, err = forestAddVertex(dependencyForest, key, value, dependencies)
    61  		if err != nil {
    62  			return nil, err
    63  		}
    64  	}
    65  
    66  	// Add edges
    67  	//   Edges must be added after all vertexes were are added
    68  	for _, dep := range dependencies {
    69  		err = dependencyForest.AddEdge(dep[0], dep[1])
    70  		if err != nil {
    71  			return nil, fmt.Errorf("%w: %w", ErrDependencyTreeUndefDep, err)
    72  		}
    73  	}
    74  
    75  	// Check target is in Forest
    76  	_, err = dependencyForest.GetVertex(target)
    77  	if err != nil {
    78  		return nil, fmt.Errorf("%w: %w", ErrDependencyTreeUnderTarget, err)
    79  	}
    80  
    81  	// Create a queue in correct order (starting with leaves)
    82  	queue := []string{}
    83  	queueMutex := &sync.Mutex{} // Mutex to ensure concurrent access to queue is safe in the callback
    84  	flowCallback := func(d *dag.DAG, id string, _ []dag.FlowResult) (interface{}, error) {
    85  		v, err := d.GetVertex(id)
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  		queueMutex.Lock()
    90  		queue = append(queue, v.(string))
    91  		queueMutex.Unlock()
    92  		return nil, nil
    93  	}
    94  	_, err = dependencyForest.DescendantsFlow(target, nil, flowCallback)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	slices.Reverse(queue)
    99  
   100  	// Build each item in queue (if recursive)
   101  	slog.Info(fmt.Sprintf("Building queue: %v", queue))
   102  	if recursive {
   103  		builds := []string{}
   104  		slog.Info(fmt.Sprintf("Building '%s' recursively", target))
   105  		for _, item := range queue {
   106  			slog.Info(fmt.Sprintf("Building: %s", item))
   107  			err = executor(ctx, item, config, interactive)
   108  			if err != nil {
   109  				return nil, err
   110  			}
   111  			builds = append(builds, item)
   112  		}
   113  		return builds, nil
   114  	}
   115  	// else build only the target
   116  	slog.Info(fmt.Sprintf("Building '%s' NOT recursively", target))
   117  	return []string{target}, executor(ctx, target, config, interactive)
   118  }
   119  
   120  // Execute a build step
   121  func Execute(ctx context.Context, target string, config *Config, interactive bool) error {
   122  	// Setup dagger client
   123  	client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
   124  	if err != nil {
   125  		return err
   126  	}
   127  	defer client.Close()
   128  
   129  	// Find requested target
   130  	modules := config.AllModules()
   131  	if _, ok := modules[target]; ok {
   132  		myContainer, err := modules[target].buildFirmware(ctx, client, "")
   133  		if err != nil && interactive {
   134  			// If error, try to open SSH
   135  			opts := container.NewSettingsSSH(container.WithWaitPressEnter())
   136  			sshErr := container.OpenSSH(ctx, client, myContainer, ContainerWorkDir, opts)
   137  			return errors.Join(err, sshErr)
   138  		}
   139  		return err
   140  	}
   141  	return ErrTargetMissing
   142  }