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 }