github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/bob/aggregate.go (about) 1 package bob 2 3 import ( 4 "fmt" 5 "path/filepath" 6 "strings" 7 8 "github.com/Benchkram/bob/bobtask" 9 "github.com/Benchkram/bob/pkg/usererror" 10 11 "github.com/logrusorgru/aurora" 12 13 "github.com/Benchkram/errz" 14 15 "github.com/Benchkram/bob/bob/bobfile" 16 "github.com/Benchkram/bob/pkg/filepathutil" 17 "github.com/hashicorp/go-version" 18 ) 19 20 // find bobfiles recursively. 21 func (b *B) find() (bobfiles []string, err error) { 22 defer errz.Recover(&err) 23 24 list, err := filepathutil.ListRecursive(b.dir) 25 errz.Fatal(err) 26 27 for _, file := range list { 28 if bobfile.IsBobfile(file) { 29 bobfiles = append(bobfiles, file) 30 } 31 } 32 33 return bobfiles, nil 34 } 35 36 func (b *B) PrintVersionCompatibility(bobfile *bobfile.Bobfile) { 37 binVersion, _ := version.NewVersion(Version) 38 39 for _, boblet := range bobfile.Bobfiles() { 40 if boblet.Version != "" { 41 bobletVersion, _ := version.NewVersion(boblet.Version) 42 43 if binVersion.Core().Segments64()[0] != bobletVersion.Core().Segments64()[0] { 44 fmt.Println(aurora.Red(fmt.Sprintf("Warning: major version mismatch: Your bobfile's major version (%s, '%s') is different from the CLI version (%s). This might lead to unexpected errors.", boblet.Version, boblet.Dir(), binVersion)).String()) 45 continue 46 } 47 48 if binVersion.LessThan(bobletVersion) { 49 fmt.Println(aurora.Red(fmt.Sprintf("Warning: possible version incompatibility: Your bobfile's version (%s, '%s') is higher than the CLI version (%s). Some features might not work as expected.", boblet.Version, boblet.Dir(), binVersion)).String()) 50 continue 51 } 52 } 53 } 54 } 55 56 // Aggregate determine and read Bobfiles recursively into memory 57 // and returns a single Bobfile containing all tasks & runs. 58 func (b *B) Aggregate() (aggregate *bobfile.Bobfile, err error) { 59 defer errz.Recover(&err) 60 61 bobfiles, err := b.find() 62 errz.Fatal(err) 63 64 // Read & Find Bobfiles 65 bobs := []*bobfile.Bobfile{} 66 for _, bf := range bobfiles { 67 boblet, err := bobfile.BobfileRead(filepath.Dir(bf)) 68 errz.Fatal(err) 69 70 if boblet.Dir() == b.dir { 71 aggregate = boblet 72 } 73 74 for variable, value := range boblet.Variables { 75 for key, task := range boblet.BTasks { 76 // TODO: Create and use envvar sanitizer 77 78 task.AddEnvironment(strings.ToUpper(variable), value) 79 80 boblet.BTasks[key] = task 81 } 82 } 83 84 bobs = append(bobs, boblet) 85 } 86 87 if aggregate == nil { 88 return nil, usererror.Wrap(ErrCouldNotFindTopLevelBobfile) 89 } 90 91 aggregate.SetBobfiles(bobs) 92 93 // Merge tasks into one Bobfile 94 for _, bobfile := range bobs { 95 // Skip the aggregate 96 if bobfile.Dir() == aggregate.Dir() { 97 continue 98 } 99 100 for taskname, task := range bobfile.BTasks { 101 dir := bobfile.Dir() 102 103 // Use a relative path as task prefix. 104 prefix := strings.TrimPrefix(dir, b.dir) 105 taskname := addTaskPrefix(prefix, taskname) 106 107 // fmt.Printf("aggreagted [dir:%s, bdir:%s prefix:%s] taskname %s\n", prefix, dir, b.dir, taskname) 108 109 // Alter the taskname. 110 task.SetName(taskname) 111 112 // Rewrite dependent tasks to global scope. 113 dependsOn := []string{} 114 for _, dependentTask := range task.DependsOn { 115 dependsOn = append(dependsOn, addTaskPrefix(prefix, dependentTask)) 116 } 117 task.DependsOn = dependsOn 118 119 aggregate.BTasks[taskname] = task 120 } 121 } 122 123 // Merge runs into one Bobfile 124 for _, bobfile := range bobs { 125 // Skip the aggregate 126 if bobfile.Dir() == aggregate.Dir() { 127 continue 128 } 129 130 for runname, run := range bobfile.RTasks { 131 dir := bobfile.Dir() 132 133 // Use a relative path as task prefix. 134 prefix := strings.TrimPrefix(dir, b.dir) 135 name := addTaskPrefix(prefix, runname) 136 137 // Alter the runname. 138 run.SetName(name) 139 140 // Rewrite dependents to global scope. 141 dependsOn := []string{} 142 for _, dependent := range run.DependsOn { 143 dependsOn = append(dependsOn, addTaskPrefix(prefix, dependent)) 144 } 145 run.DependsOn = dependsOn 146 147 aggregate.RTasks[name] = run 148 } 149 } 150 151 // TODO: Gather missing tasks from remote & Unpack? 152 153 // Gather environment from dependent tasks. 154 // 155 // Each export is translated into environment variables named: 156 // `second-level/openapi => SECOND_LEVEL_OPENAPI` 157 // hyphens`-` are translated to underscores`_`. 158 // 159 // The file is prefixed with all paths to make it relative to dir of the the top Bobfile: 160 // `openapi.yaml => sencond-level/openapi.yaml` 161 // 162 // TODO: Exports should be part of a packed file and should be evaluated when running a playbook or at least after Unpack(). 163 // Looks like this is the wrong place to presume that all child tasks are comming from child bobfiles 164 // must exist. 165 for i, task := range aggregate.BTasks { 166 for _, dependentTaskName := range task.DependsOn { 167 168 dependentTask, ok := aggregate.BTasks[dependentTaskName] 169 if !ok { 170 return nil, ErrTaskDoesNotExist 171 } 172 173 for exportname, export := range dependentTask.Exports { 174 // fmt.Printf("Task %s exports %s\n", dependentTaskName, export) 175 176 envvar := taskNameToEnvironment(dependentTaskName, exportname) 177 178 value := filepath.Join(dependentTask.Dir(), string(export)) 179 180 // Make the path relative to the aggregates dir. 181 dir := aggregate.Dir() 182 if !strings.HasSuffix(dir, "/") { 183 dir = dir + "/" 184 } 185 value = strings.TrimPrefix(value, dir) 186 187 // println(envvar, value) 188 189 task.AddEnvironment(envvar, value) 190 191 aggregate.BTasks[i] = task 192 } 193 } 194 } 195 196 // Assure tasks are correctly initialised. 197 for i, task := range aggregate.BTasks { 198 task.WithLocalstore(b.local) 199 task.WithBuildinfoStore(b.buildInfoStore) 200 task.SetBuilder(b.dir) // TODO: todoproject, use project name instead of dir 201 202 // a task must always-rebuild when caching is disabled 203 if !b.enableCaching { 204 task.SetRebuildStrategy(bobtask.RebuildAlways) 205 } 206 aggregate.BTasks[i] = task 207 } 208 209 return aggregate, aggregate.Verify() 210 } 211 212 func addTaskPrefix(prefix, taskname string) string { 213 taskname = filepath.Join(prefix, taskname) 214 taskname = strings.TrimPrefix(taskname, "/") 215 return taskname 216 } 217 218 // taskNameToEnvironment 219 // 220 // Each taskname is translated into environment variables like: 221 // `second-level/openapi_exportname => SECOND_LEVEL_OPENAPI_EXPORTNAME` 222 // Hyphens`-` are translated to underscores`_`. 223 func taskNameToEnvironment(taskname string, exportname string) string { 224 225 splits := strings.Split(taskname, "/") 226 splits = append(splits, exportname) 227 228 envvar := strings.Join(splits, "_") 229 envvar = strings.ReplaceAll(envvar, "-", "_") 230 envvar = strings.ReplaceAll(envvar, ".", "_") 231 envvar = strings.ToUpper(envvar) 232 233 return envvar 234 }