github.com/zyedidia/knit@v1.1.2-0.20230901152954-f7d4e39a0e24/knit.go (about) 1 package knit 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 "sync" 11 "unicode" 12 "unicode/utf8" 13 14 "github.com/adrg/xdg" 15 lua "github.com/zyedidia/gopher-lua" 16 "github.com/zyedidia/knit/rules" 17 ) 18 19 // Flags for modifying the behavior of Knit. 20 type Flags struct { 21 Knitfile string 22 Ncpu int 23 DryRun bool 24 RunDir string 25 Always bool 26 Quiet bool 27 Style string 28 CacheDir string 29 Hash bool 30 Updated []string 31 Shell string 32 KeepGoing bool 33 Tool string 34 ToolArgs []string 35 } 36 37 // Flags that may be automatically set in a .knit.toml file. 38 type UserFlags struct { 39 Knitfile *string 40 Ncpu *int 41 DryRun *bool 42 RunDir *string `toml:"directory"` 43 Always *bool 44 Quiet *bool 45 Style *string 46 CacheDir *string `toml:"cache"` 47 Hash *bool 48 Updated *[]string 49 Shell *string 50 KeepGoing *bool 51 } 52 53 // Capitalize the first rune of a string. 54 func title(s string) string { 55 r, size := utf8.DecodeRuneInString(s) 56 return string(unicode.ToTitle(r)) + s[size:] 57 } 58 59 func rel(basepath, targpath string) (string, error) { 60 slash := strings.HasSuffix(targpath, "/") 61 rel, err := filepath.Rel(basepath, targpath) 62 if err != nil { 63 return filepath.Join(basepath, targpath), nil 64 } 65 if slash { 66 rel += "/" 67 } 68 return rel, err 69 } 70 71 type assign struct { 72 name string 73 value string 74 } 75 76 // Parses 'args' for expressions of the form 'key=value'. Assignments that are 77 // found are returned, along with the remaining arguments. 78 func makeAssigns(args []string) ([]assign, []string) { 79 assigns := make([]assign, 0, len(args)) 80 other := make([]string, 0) 81 for _, a := range args { 82 before, after, found := strings.Cut(a, "=") 83 if found { 84 assigns = append(assigns, assign{ 85 name: before, 86 value: after, 87 }) 88 } else { 89 other = append(other, a) 90 } 91 } 92 return assigns, other 93 } 94 95 func pathJoin(dir, target string) string { 96 if filepath.IsAbs(target) { 97 return target 98 } 99 p := filepath.Join(dir, target) 100 if strings.HasSuffix(target, "/") { 101 p += "/" 102 } 103 return p 104 } 105 106 // Changes the working directory to 'dir' and changes all targets to be 107 // relative to that directory. 108 func goToKnitfile(vm *LuaVM, dir string, targets []string) error { 109 wd, err := os.Getwd() 110 if err != nil { 111 return err 112 } 113 adir, err := filepath.Abs(dir) 114 if err != nil { 115 return err 116 } 117 for i, t := range targets { 118 r, err := rel(adir, wd) 119 if err != nil { 120 return err 121 } 122 if r != "" && r != "." { 123 targets[i] = pathJoin(r, t) 124 } 125 } 126 127 return os.Chdir(dir) 128 } 129 130 var ErrNothingToDo = errors.New("nothing to be done") 131 var ErrQuiet = errors.New("quiet") 132 133 type ErrMessage struct { 134 msg string 135 } 136 137 func (e *ErrMessage) Error() string { 138 return e.msg 139 } 140 141 // Given the return value from the Lua evaluation of the Knitfile, returns all 142 // the buildsets and a list of the directories in order of priority. If the 143 // Knitfile requested to return an error (with a string), or quietly (with 144 // nil), returns an appropriate error. 145 func getBuildSets(lval lua.LValue) (map[string]*LBuildSet, error) { 146 bsets := map[string]*LBuildSet{ 147 ".": { 148 Dir: ".", 149 rset: LRuleSet{}, 150 }, 151 } 152 153 var addBuildSet func(bs LBuildSet) 154 addBuildSet = func(bs LBuildSet) { 155 if b, ok := bsets[bs.Dir]; ok { 156 b.rset = append(b.rset, bs.rset...) 157 } else { 158 bsets[bs.Dir] = &bs 159 } 160 161 // TODO: can there be a buildset cycle? 162 for _, bset := range bs.bsets { 163 addBuildSet(bset) 164 } 165 } 166 167 switch v := lval.(type) { 168 case lua.LString: 169 return nil, &ErrMessage{msg: string(v)} 170 case *lua.LNilType: 171 return nil, ErrQuiet 172 case *lua.LUserData: 173 switch u := v.Value.(type) { 174 case LBuildSet: 175 addBuildSet(u) 176 default: 177 return nil, fmt.Errorf("invalid return value: %v", lval) 178 } 179 default: 180 return nil, fmt.Errorf("invalid return value: %v", lval) 181 } 182 return bsets, nil 183 } 184 185 // Run searches for a Knitfile and executes it, according to args (a list of 186 // targets and assignments), and the flags. All output is written to 'out'. The 187 // path of the executed knitfile is returned, along with a possible error. 188 func Run(out io.Writer, args []string, flags Flags) (string, error) { 189 if flags.RunDir != "" { 190 err := os.Chdir(flags.RunDir) 191 if err != nil { 192 return "", err 193 } 194 } 195 196 vm := NewLuaVM(flags.Shell, flags) 197 198 cliAssigns, targets := makeAssigns(args) 199 envAssigns, _ := makeAssigns(os.Environ()) 200 201 vm.MakeTable("cli", cliAssigns) 202 vm.MakeTable("env", envAssigns) 203 204 file, dir, err := FindBuildFile(flags.Knitfile) 205 if err != nil { 206 return "", err 207 } 208 knitpath := filepath.Join(dir, file) 209 if file == "" { 210 def, ok := DefaultBuildFile() 211 if ok { 212 file = def 213 } 214 } else if dir != "" { 215 for i, u := range flags.Updated { 216 p, err := rel(dir, u) 217 if err != nil { 218 return knitpath, err 219 } 220 flags.Updated[i] = p 221 } 222 err = goToKnitfile(vm, dir, targets) 223 if err != nil { 224 return knitpath, err 225 } 226 } 227 228 if file == "" { 229 return knitpath, fmt.Errorf("%s does not exist", flags.Knitfile) 230 } 231 232 lval, err := vm.DoFile(file) 233 if err != nil { 234 return knitpath, err 235 } 236 237 bsets, err := getBuildSets(lval) 238 if err != nil { 239 return knitpath, err 240 } 241 242 var rulesets []*rules.RuleSet 243 var main *rules.RuleSet 244 245 for k, v := range bsets { 246 rs := rules.NewRuleSet(k) 247 for _, lr := range v.rset { 248 err := rules.ParseInto(lr.Contents, rs, lr.File, lr.Line) 249 if err != nil { 250 return knitpath, err 251 } 252 } 253 if k == "." { 254 main = rs 255 } else { 256 rulesets = append(rulesets, rs) 257 } 258 } 259 260 if main == nil { 261 return knitpath, fmt.Errorf("no buildset for the root directory found") 262 } 263 264 rs := rules.MergeRuleSets(main, rulesets) 265 266 alltargets := rs.AllTargets() 267 268 if len(targets) == 0 { 269 targets = []string{rs.MainTarget()} 270 } 271 rootTargets := make([]string, 0, len(targets)) 272 273 if len(targets) == 0 { 274 return knitpath, errors.New("no targets") 275 } 276 277 for _, t := range targets { 278 if t != "" { 279 rootTargets = append(rootTargets, filepath.Base(t)) 280 } 281 } 282 283 rs.Add(rules.NewDirectRuleBase([]string{":build"}, targets, nil, rules.AttrSet{ 284 Virtual: true, 285 NoMeta: true, 286 Rebuild: true, 287 })) 288 289 rs.Add(rules.NewDirectRuleBase([]string{":build-root"}, rootTargets, nil, rules.AttrSet{ 290 Virtual: true, 291 NoMeta: true, 292 Rebuild: true, 293 })) 294 295 rs.Add(rules.NewDirectRuleBase([]string{":all"}, alltargets, nil, rules.AttrSet{ 296 Virtual: true, 297 NoMeta: true, 298 Rebuild: true, 299 })) 300 301 updated := make(map[string]bool) 302 for _, u := range flags.Updated { 303 updated[u] = true 304 } 305 306 graph, err := rules.NewGraph(rs, ":build", updated) 307 if err != nil { 308 g, rerr := rules.NewGraph(rs, ":build-root", updated) 309 if rerr != nil { 310 return knitpath, err 311 } 312 graph = g 313 } 314 315 err = graph.ExpandRecipes(vm) 316 if err != nil { 317 return knitpath, err 318 } 319 320 var db *rules.Database 321 if flags.CacheDir == "." || flags.CacheDir == "" { 322 db = rules.NewDatabase(filepath.Join(".knit", file)) 323 } else { 324 wd, err := os.Getwd() 325 if err != nil { 326 return knitpath, err 327 } 328 dir := flags.CacheDir 329 if dir == "$cache" { 330 dir = filepath.Join(xdg.CacheHome, "knit") 331 } 332 db = rules.NewCacheDatabase(dir, filepath.Join(wd, file)) 333 } 334 335 var w io.Writer = out 336 if flags.Quiet { 337 w = io.Discard 338 } 339 340 if flags.Tool != "" { 341 var t rules.Tool 342 switch flags.Tool { 343 case "list": 344 t = &rules.ListTool{W: w} 345 case "graph": 346 t = &rules.GraphTool{W: w} 347 case "clean": 348 t = &rules.CleanTool{W: w, NoExec: flags.DryRun, Db: db} 349 case "targets": 350 t = &rules.TargetsTool{W: w} 351 case "compdb": 352 t = &rules.CompileDbTool{W: w} 353 case "commands": 354 t = &rules.CommandsTool{W: w} 355 case "status": 356 t = &rules.StatusTool{W: w, Db: db, Hash: flags.Hash} 357 case "path": 358 t = &rules.PathTool{W: w, Path: knitpath} 359 case "db": 360 t = &rules.DbTool{W: w, Db: db} 361 default: 362 return knitpath, fmt.Errorf("unknown tool: %s", flags.Tool) 363 } 364 365 err = t.Run(graph, flags.ToolArgs) 366 if err != nil { 367 return knitpath, err 368 } 369 370 return knitpath, db.Save() 371 } 372 373 if flags.Ncpu <= 0 { 374 return knitpath, errors.New("you must enable at least 1 core") 375 } 376 377 var printer rules.Printer 378 switch flags.Style { 379 case "steps": 380 printer = &StepPrinter{w: w} 381 case "progress": 382 printer = &ProgressPrinter{ 383 w: w, 384 tasks: make(map[string]string), 385 } 386 default: 387 printer = &BasicPrinter{w: w} 388 } 389 390 lock := sync.Mutex{} 391 ex := rules.NewExecutor(".", db, flags.Ncpu, printer, func(msg string) { 392 lock.Lock() 393 fmt.Fprintln(out, msg) 394 lock.Unlock() 395 }, rules.Options{ 396 NoExec: flags.DryRun, 397 Shell: flags.Shell, 398 AbortOnError: !flags.KeepGoing, 399 BuildAll: flags.Always, 400 Hash: flags.Hash, 401 }) 402 403 rebuilt, execerr := ex.Exec(graph) 404 405 err = db.Save() 406 if err != nil { 407 return knitpath, err 408 } 409 if execerr != nil { 410 return knitpath, execerr 411 } 412 if !rebuilt { 413 return knitpath, fmt.Errorf("'%s': %w", strings.Join(targets, " "), ErrNothingToDo) 414 } 415 return knitpath, nil 416 }