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  }