github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/bob/playbook/build.go (about) 1 package playbook 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "sort" 9 "time" 10 11 "github.com/Benchkram/bob/bobtask" 12 "github.com/Benchkram/bob/pkg/boblog" 13 "github.com/Benchkram/errz" 14 "github.com/logrusorgru/aurora" 15 ) 16 17 var colorPool = []aurora.Color{ 18 1, 19 aurora.BlueFg, 20 aurora.GreenFg, 21 aurora.CyanFg, 22 aurora.MagentaFg, 23 aurora.YellowFg, 24 aurora.RedFg, 25 } 26 var round = 10 * time.Millisecond 27 28 // Build the playbook starting at root. 29 func (p *Playbook) Build(ctx context.Context) (err error) { 30 done := make(chan error) 31 32 { 33 tasks := []string{} 34 for _, t := range p.Tasks { 35 tasks = append(tasks, t.Name()) 36 } 37 sort.Strings(tasks) 38 39 // Adjust padding of first column based on the taskname length. 40 // Also assign fixed color to the tasks. 41 p.namePad = 0 42 for i, name := range tasks { 43 if len(name) > p.namePad { 44 p.namePad = len(name) 45 } 46 47 color := colorPool[i%len(colorPool)] 48 p.Tasks[name].Task.SetColor(color) 49 } 50 p.namePad += 14 51 52 dependencies := len(tasks) - 1 53 rootName := p.Tasks[p.root].ColoredName() 54 boblog.Log.V(1).Info(fmt.Sprintf("Running task %s with %d dependencies", rootName, dependencies)) 55 } 56 57 processedTasks := []*bobtask.Task{} 58 59 go func() { 60 // TODO: Run a worker pool so that multiple tasks can run in parallel. 61 62 c := p.TaskChannel() 63 for t := range c { 64 // copy for processing 65 task := t 66 processedTasks = append(processedTasks, &task) 67 68 err := p.build(ctx, &task) 69 if err != nil { 70 done <- err 71 break 72 } 73 } 74 75 close(done) 76 }() 77 78 err = p.Play() 79 errz.Fatal(err) 80 81 err = <-done 82 if err != nil { 83 p.Done() 84 } 85 errz.Fatal(err) 86 87 // iterate through tasks and log 88 // skipped input files. 89 var skippedInputs int 90 for _, task := range processedTasks { 91 skippedInputs = logSkippedInputs( 92 skippedInputs, 93 task.ColoredName(), 94 task.LogSkippedInput(), 95 ) 96 } 97 98 // summary 99 boblog.Log.V(1).Info("") 100 boblog.Log.V(1).Info(aurora.Bold("● ● ● ●").BrightGreen().String()) 101 t := fmt.Sprintf("Ran %d tasks in %s ", len(processedTasks), p.ExecutionTime().Round(round)) 102 boblog.Log.V(1).Info(aurora.Bold(t).BrightGreen().String()) 103 for _, t := range processedTasks { 104 stat, err := p.TaskStatus(t.Name()) 105 if err != nil { 106 fmt.Println(err) 107 continue 108 } 109 110 execTime := "" 111 status := stat.State() 112 if status != StateNoRebuildRequired { 113 execTime = fmt.Sprintf("\t(%s)", stat.ExecutionTime().Round(round)) 114 } 115 116 taskName := t.Name() 117 boblog.Log.V(1).Info(fmt.Sprintf(" %-*s\t%s%s", p.namePad, taskName, status.Summary(), execTime)) 118 } 119 boblog.Log.V(1).Info("") 120 121 return err 122 } 123 124 // didWriteBuildOutput assures that a new line is added 125 // before writing state or logs of a task to stdout. 126 var didWriteBuildOutput bool 127 128 // build a single task and update the playbook state after completion. 129 func (p *Playbook) build(ctx context.Context, task *bobtask.Task) (err error) { 130 defer errz.Recover(&err) 131 132 var taskSuccessFul bool 133 var taskErr error 134 defer func() { 135 if !taskSuccessFul { 136 errr := p.TaskFailed(task.Name(), taskErr) 137 if errr != nil { 138 boblog.Log.Error(errr, "Setting the task state to failed, failed.") 139 } 140 } 141 }() 142 143 coloredName := task.ColoredName() 144 145 done := make(chan struct{}) 146 defer close(done) 147 148 go func() { 149 select { 150 case <-done: 151 case <-ctx.Done(): 152 if errors.Is(ctx.Err(), context.Canceled) { 153 boblog.Log.V(1).Info(fmt.Sprintf("%-*s\t%s", p.namePad, coloredName, StateCanceled)) 154 _ = p.TaskCanceled(task.Name()) 155 } 156 } 157 }() 158 159 hashIn, err := task.HashIn() 160 if err != nil { 161 return err 162 } 163 164 rebuildRequired, rebuildCause, err := p.TaskNeedsRebuild(task.Name(), hashIn) 165 errz.Fatal(err) 166 167 // task might need a rebuild due to a input change. 168 // but could still be possible to load the targets from the artifact store. 169 // If a task needs a rebuild due to a dependency change => rebuild. 170 if rebuildRequired && rebuildCause != DependencyChanged && rebuildCause != TaskForcedRebuild { 171 success, err := task.ArtifactUnpack(hashIn) 172 errz.Fatal(err) 173 if success { 174 rebuildRequired = false 175 } 176 } 177 178 if !rebuildRequired { 179 status := StateNoRebuildRequired 180 boblog.Log.V(2).Info(fmt.Sprintf("%-*s\t%s", p.namePad, coloredName, status.Short())) 181 taskSuccessFul = true 182 return p.TaskNoRebuildRequired(task.Name()) 183 } 184 185 if !didWriteBuildOutput { 186 boblog.Log.V(1).Info("") 187 didWriteBuildOutput = true 188 } 189 boblog.Log.V(1).Info(fmt.Sprintf("%-*s\trunning task...", p.namePad, coloredName)) 190 191 err = task.Clean() 192 errz.Fatal(err) 193 194 err = task.Run(ctx, p.namePad) 195 if err != nil { 196 taskSuccessFul = false 197 taskErr = err 198 } 199 errz.Fatal(err) 200 201 taskSuccessFul = true 202 203 err = task.VerifyAfter() 204 errz.Fatal(err) 205 206 target, err := task.Target() 207 if err != nil { 208 errz.Fatal(err) 209 } 210 211 // Check targets are created correctly. 212 // On success the target hash is computed 213 // inside TaskCompleted(). 214 if target != nil { 215 if !target.Exists() { 216 boblog.Log.V(1).Info(fmt.Sprintf("%-*s\t%s\t(invalid targets)", p.namePad, coloredName, StateFailed)) 217 err = p.TaskFailed(task.Name(), fmt.Errorf("targets not created")) 218 if err != nil { 219 if errors.Is(err, ErrFailed) { 220 return err 221 } 222 } 223 } 224 } 225 226 err = p.TaskCompleted(task.Name()) 227 errz.Fatal(err) 228 229 taskStatus, err := p.TaskStatus(task.Name()) 230 errz.Fatal(err) 231 232 state := taskStatus.State() 233 boblog.Log.V(1).Info(fmt.Sprintf("%-*s\t%s", p.namePad, coloredName, "..."+state.Short())) 234 235 return nil 236 } 237 238 const maxSkippedInputs = 5 239 240 // logSkippedInputs until max is reached 241 func logSkippedInputs(count int, taskname string, skippedInputs []string) int { 242 if len(skippedInputs) == 0 { 243 return count 244 } 245 if count >= maxSkippedInputs { 246 return maxSkippedInputs 247 } 248 249 for _, f := range skippedInputs { 250 count = count + 1 251 boblog.Log.V(1).Info(fmt.Sprintf("skipped %s '%s' %s", taskname, f, os.ErrPermission)) 252 253 if count >= maxSkippedInputs { 254 boblog.Log.V(1).Info(fmt.Sprintf("skipped %s %s", taskname, "& more...")) 255 break 256 } 257 } 258 259 return count 260 }